SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-07

SQLite у production на VPS: налаштування й обмеження

Дізнайтеся, коли SQLite підходить для production на одному VPS, як налаштувати WAL і busy_timeout, додати Litestream та визначити критичні обмеження.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Правильний вибір SQLite як production-бази даних на VPS

Запускати SQLite у production на VPS — правильний вибір для більшості невеликих застосунків. Причина проста: одному процесу на одній машині, який записує дані в один файл, не потрібен сервер бази даних. Не потрібно контролювати окремий daemon, відкривати порт у firewall, поновлювати пароль або підтримувати в роботі другу машину. Запит є викликом функції, а не мережевим обміном, тому сторінка з сорока запитами створює сорок викликів функцій.

Обмеження чіткі та реальні. SQLite допускає лише одного writer одночасно для всього файлу бази даних, а сам файл не можна спільно використовувати між двома машинами. Для одного VPS, на якому працює один застосунок, обидва обмеження прийнятні. Щойно архітектура виходить за ці межі, обидва обмеження стають критичними. У цьому посібнику описано налаштування, які забезпечують безпечну роботу SQLite на сервері, безперервне резервне копіювання за допомогою Litestream і момент, коли слід зупинитися.

Спочатку встановіть інструмент командного рядка. Усі наведені нижче дії виконувалися в Ubuntu 24.04.

sudo apt update
sudo apt install -y sqlite3
sqlite3 --version

Команда виводить версію, яка починається з 3., а потім містить дату складання та hash вихідного коду. Станом на July 2026 Ubuntu 24.04 постачається з SQLite 3.45.1. Ваш застосунок, імовірно, не використовує цей binary: більшість language runtime постачає власну копію бібліотеки SQLite, часто новішу. Тому перевірте версію, яку повертає ваш database driver, перш ніж покладатися на нову функцію.

Чому режим WAL змінюють насамперед

За замовчуванням SQLite використовує журнал відкату. Перед зміною сторінки він копіює її вихідний вміст у файл -journal, а потім редагує базу даних безпосередньо. Для безпечного виконання цієї операції SQLite встановлює ексклюзивне блокування всього файлу, тому кожен читач очікує, доки завершиться будь-яка операція запису. На ноутбуці це непомітно. На вебсервері один повільний запис затримує кожен запит, який звертається до бази даних.

Режим WAL (write-ahead log) змінює цей порядок. Процес запису додає нові сторінки в окремий файл -wal, а основний файл бази даних не змінюється. Процеси читання продовжують читати основний файл у стані знімка, зафіксованого на момент початку операції. Тому читання не блокує запис, а запис не блокує читання. Пізніше під час checkpoint накопичені сторінки WAL копіюються назад до основної бази даних. Саме ця зміна значною мірою робить SQLite придатним для використання за вебзастосунком.

Увімкніть режим WAL і перевірте, що його збережено

mkdir -p ~/app
sqlite3 ~/app/app.db "PRAGMA journal_mode=WAL;"

Команда виводить wal. Це не декоративний текст. PRAGMA journal_mode повертає фактичний режим бази даних, тому відповідь delete означає, що зміна не вдалася, і база все ще використовує журнал відкату.

Режим WAL зберігається постійно. Це прапорець у заголовку бази даних, а не параметр підключення, тому його потрібно встановити один раз для кожного файла бази даних. Після цього його успадковує кожне підключення, зокрема після перезавантаження. Перевірте це через нове підключення.

sqlite3 ~/app/app.db "PRAGMA journal_mode;"

Тепер створіть таблицю та перевірте, що з’явилося на диску.

sqlite3 ~/app/app.db <<'SQL'
CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT NOT NULL);
INSERT INTO notes (body) VALUES ('first row');
SQL
ls -l ~/app/

Тепер є три файли: app.db, app.db-wal і app.db-shm. Файл -wal містить зафіксовані сторінки, для яких ще не виконано checkpoint. Файл -shm є індексом спільної пам’яті, який відображає кожне підключення, щоб усі вони узгоджено бачили вміст WAL. Обидва файли належать базі даних і не є тимчасовими файлами. Якщо під час роботи застосунку скопіювати лише app.db, отриманий файл не міститиме жодної недавньої зафіксованої зміни. Якщо видалити app.db, залишивши два інші файли, SQLite застосує застарілі сторінки WAL до будь-якого нового файла, який з’явиться під цим ім’ям. Саме так можна пошкодити нову базу даних під час спроби скинути стару.

Параметри підключення, потрібні кожному production-застосунку

У базі даних зберігається лише journal_mode. Усі інші наведені нижче параметри належать до окремого підключення. Це означає, що застосунок має виконувати їх для кожного відкритого підключення, зокрема для кожного підключення, яке пул створює у фоновому режимі.

PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;

busy_timeout = 5000 вказує SQLite повторювати спроби підключення до заблокованої бази даних протягом 5000 мілісекунд, перш ніж повернути database is locked. Значення за замовчуванням дорівнює 0. Тому за замовчуванням SQLite негайно завершує операцію з помилкою, щойно два процеси запису перетинаються в часі. Це єдине налаштування усуває більшість помилок блокування, які зазвичай помилково приписують самому SQLite.

synchronous = NORMAL — правильне налаштування для режиму WAL, але важливо розуміти компроміс. За значення FULL SQLite викликає fsync для WAL під час кожної фіксації транзакції. За значення NORMAL синхронізація виконується під час контрольних точок. Документація SQLite прямо описує, від чого ви відмовляєтеся: після збою живлення або апаратного перезавантаження транзакції більше не гарантовано зберігаються. Втрата живлення не пошкоджує базу даних. Втрачаються лише останні зафіксовані зміни, які ще не були записані на диск. Для VPS це зазвичай правильний компроміс, оскільки fsync більше не виконується для кожної окремої операції запису.

foreign_keys = ON за замовчуванням вимкнено для зворотної сумісності, і це налаштування діє для кожного підключення окремо. Схема, заповнена конструкціями REFERENCES, фактично нічого не забезпечує, доки кожне підключення не ввімкне цей параметр.

Є ще одне налаштування, яке стане важливим пізніше. SQLite автоматично виконує контрольну точку, коли WAL перевищує 1000 сторінок. Цю операцію виконує підключення, яке в цей момент завершує транзакцію. Саме по собі це нормально. Питання виникає, коли працює Litestream, оскільки Litestream має керувати часом виконання контрольних точок.

Чому database is locked продовжує виникати після налаштування busy_timeout

Саме через цю проблему користувачі повертаються до Postgres. Вона має конкретну причину.

Параметр busy_timeout встановлює обробник зайнятого ресурсу, але SQLite не гарантує, що викличе його.

Якщо SQLite визначає, що виклик обробника зайнятого ресурсу може призвести до взаємного блокування, він повертає застосунку SQLITE_BUSY, не викликаючи цей обробник.

Взаємне блокування, якого уникає SQLite, виникає під час підвищення рівня транзакції. Окремий BEGIN у SQLite означає BEGIN DEFERRED. Якщо першою командою після нього є SELECT, ви працюєте в транзакції читання. Коли пізніший UPDATE у цій самій транзакції має перетворити її на транзакцію запису, а інше підключення вже виконало запис після початку вашого читання, SQLite не може змусити вас чекати. Ваш знімок даних уже застарів, а очікування лише призвело б до взаємного блокування двох підключень. У документації результат описано безпосередньо:

Наступні команди запису підвищать транзакцію до транзакції запису, якщо це можливо, або повернуть SQLITE_BUSY.

Ваш 5000-мілісекундний тайм-аут взагалі не перевіряється. Помилка виникає негайно, тому здається, що параметр не спрацював.

Виправлення складається з одного слова.

BEGIN IMMEDIATE;
UPDATE notes SET body = 'edited' WHERE id = 1;
COMMIT;

BEGIN IMMEDIATE встановлює блокування запису на початку, ще до читання будь-яких даних. Підвищення рівня транзакції не відбувається, тому немає і взаємного блокування, якого потрібно уникати. Обробник зайнятого ресурсу застосовується, і підключення чекає своєї черги замість негайного завершення з помилкою. Для транзакцій лише читання залишайте режим deferred. Будь-яка транзакція, що містить запис, має бути immediate.

Друга причина помилок блокування менш очевидна: транзакція запису залишається відкритою під час повільної операції. SQLite серіалізує операції запису, тому транзакція, яка відкривається, викликає зовнішній API через мережу, а потім фіксує зміни, блокуватиме всі інші операції запису протягом усього цього виклику. Прочитайте потрібні дані, закрийте транзакцію, виконайте повільну операцію, а потім відкрийте коротку транзакцію запису, щоб зберегти результат.

Безперервне резервне копіювання за допомогою Litestream

Нічна копія може втратити записи за цілу добу, а запуск cp для активної бази даних SQLite може створити копію, яку неможливо відкрити. Безпечними є два варіанти. sqlite3 app.db ".backup /path/to/backup.db" використовує інтерфейс онлайн-резервного копіювання SQLite і працює з базою даних, яка використовується. Litestream іде далі: він відстежує WAL і безперервно передає зміни в об’єктне сховище. Це зменшує максимально можливу втрату даних з доби приблизно до однієї секунди.

Litestream — це один бінарний файл Go, який працює поруч із вашим застосунком. Він не розташовується між застосунком і базою даних. Застосунок записує дані в SQLite як і раніше, а Litestream читає WAL і завантажує зміни.

cd /tmp
curl -fsSL -O https://github.com/benbjohnson/litestream/releases/download/v0.5.14/litestream-0.5.14-linux-x86_64.deb
sudo dpkg -i litestream-0.5.14-linux-x86_64.deb
litestream version

v0.5.14 — це версія, яку офіційна сторінка встановлення для Linux документує станом на July 2026, а v0.5.15 вийшла 21 July 2026. Замініть версію в обох рядках на поточний тег зі сторінки releases. Якщо ваш VPS використовує arm64, візьміть відповідний пакет arm64.

Файл конфігурації розташований у /etc/litestream.yml. Спочатку налаштуйте локальну репліку у файл. Це дасть змогу перевірити весь цикл без облікових даних для хмарного сховища.

dbs:
  - path: /home/appuser/app/app.db
    replica:
      type: file
      path: /var/backups/litestream/app

Зверніть увагу, що поле називається replica, в однині. Litestream 0.5 замінив масив replicas із серії 0.3 одним блоком репліки. Конфігурація з двома записами тепер завершується помилкою під час запуску. У багатьох сторонніх інструкціях досі наведено старий масив, тому використовуйте наведену вище структуру, а не перший приклад із результатів пошуку. У серії 0.5 підкоманду litestream wal також перейменували на litestream ltx, оскільки формат резервних копій на диску змінився.

Перевірте синтаксис конфігурації, перш ніж щось увімкнути.

sudo litestream databases -config /etc/litestream.yml

Потім вручну перевірте повний цикл. Ця форма не використовує файл конфігурації та реплікує одну базу даних в один шлях.

mkdir -p /tmp/replica
litestream replicate ~/app/app.db file:///tmp/replica/app

Команда працює в передньому плані й продовжує виконуватися. В іншій оболонці додайте рядок і відновіть репліку в новий файл.

sqlite3 ~/app/app.db "INSERT INTO notes (body) VALUES ('written after replication started');"
litestream restore -o /tmp/restored.db file:///tmp/replica/app
sqlite3 /tmp/restored.db "SELECT count(*) FROM notes;"

Лічильник містить новий рядок. Якщо його немає, зміна ще не синхронізувалася: Litestream виконує передавання з інтервалом sync-interval, який за замовчуванням становить 1 second. Зачекайте та повторіть відновлення. Ця одна секунда також визначає точку відновлення. У разі аварії буде втрачено щонайбільше записи за останній інтервал синхронізації. Жодна конфігурація не може зменшити цей інтервал до нуля.

Для реального сховища замініть блок репліки на URL S3. Це працює з Amazon S3 та S3-сумісними об’єктними сховищами інших провайдерів.

dbs:
  - path: /home/appuser/app/app.db
    replica:
      url: s3://your-bucket-name/app
      region: us-east-1

snapshot:
  interval: 24h
  retention: 24h

Не зберігайте облікові дані в цьому файлі. Litestream читає LITESTREAM_ACCESS_KEY_ID і LITESTREAM_SECRET_ACCESS_KEY зі змінних середовища, тому додайте їх у drop-in systemd, який належить root і має режим 600.

Наведені вище значення snapshot є типовими, а значення retention часто стає несподіванкою. Retention визначає, як довго Litestream зберігає snapshot і файли, що їм належать. Отже, цей параметр також визначає, наскільки далеко в минуле можна виконати відновлення. Twenty-four hours означає, що помилкова міграція, яку ви помітили вранці Wednesday, уже не підлягає відновленню зі стану Monday. Встановіть retention: 168h на тиждень і врахуйте додаткові витрати на сховище.

Перевірте відновлення до того, як воно знадобиться

litestream restore -o /tmp/check.db /home/appuser/app/app.db
sqlite3 /tmp/check.db "PRAGMA integrity_check;"
sqlite3 /tmp/check.db "SELECT count(*) FROM notes;"

Отримавши шлях до бази даних, litestream restore знаходить відповідну репліку в /etc/litestream.yml і завантажує її. Для справного файлу PRAGMA integrity_check виводить ok. Будь-який інший результат означає, що відновлена копія непридатна для використання. Запускайте цю перевірку за розкладом за допомогою служби та таймера systemd і переглядайте результат. Поки ви хоча б один раз не відновили резервну копію, ви не знаєте, чи працює вона.

Запуск Litestream у systemd

Пакет Debian встановлює unit litestream, який читає /etc/litestream.yml.

sudo systemctl enable litestream
sudo systemctl start litestream
sudo journalctl -u litestream -f

У нормальному виводі для кожної бази даних із конфігурації спочатку вказується її назва, після чого вивід залишається порожнім, за винятком періодичних повідомлень про синхронізацію. Помилка no such file or directory для шляху до бази даних означає, що в конфігурації вказано неправильний шлях або процес не може його прочитати. За замовчуванням unit працює від імені root, хоча для цього завдання такі привілеї не потрібні. Litestream має мати змогу читати й записувати і базу даних, і каталог, у якому вона зберігається, оскільки працює з файлами -wal і -shm поруч із базою даних. Тому призначте йому обліковий запис, який уже використовує ваш застосунок.

# /etc/systemd/system/litestream.service.d/override.conf
[Service]
User=appuser
Group=appuser

Застосуйте ці зміни за допомогою sudo systemctl daemon-reload і sudo systemctl restart litestream. Налаштування окремого облікового запису служби з мінімально необхідними привілеями займає кілька хвилин. Це відрізняє агент резервного копіювання від другого процесу root на сервері.

Важливий один нюанс порядку запуску, якщо вам доведеться повністю відновлювати машину. Базу даних потрібно відновити до запуску застосунку. litestream restore приймає -if-db-not-exists, який завершується з кодом 0, якщо файл уже існує, тому його безпечно запускати під час кожного завантаження. Додайте його до рядка ExecStartPre у unit вашого застосунку. Тоді новий VPS завантажить базу даних, а на наявному сервері команда нічого не зробить. litestream replicate має відповідний прапорець -restore-if-db-not-exists, якщо ви хочете зберігати цю настройку в одному місці.

Де SQLite має обмеження на VPS

Мережеві файлові системи. Це обмеження неможливо усунути налаштуваннями. Режим WAL вимагає, щоб усі процеси, які використовують базу даних, мали спільний доступ до невеликої ділянки пам’яті. Її забезпечує файл -shm. У документації SQLite це правило сформульовано без винятків:

Усі процеси, які використовують базу даних, мають працювати на одному хості; WAL не працює через мережеву файлову систему.

Тому база даних на змонтованому NFS (network file system) або SMB-ресурсі може пошкодитися, і жодна pragma не запобіжить цьому. Тут є важлива відмінність, яку часто не враховують. Мережевий блоковий пристрій, який більшість VPS-провайдерів підключає як додаткове сховище, відображається в Linux як звичайний диск зі звичайною файловою системою. Це безпечно. Змонтований файловий ресурс працює інакше.

Другий сервер застосунків. Жодне налаштування не зробить таку схему безпечною. Якщо ті самі дані мають обслуговуватися двома машинами, потрібна база даних, яка підтримує роботу через мережу. Прийміть це рішення заздалегідь, поки є час на планування.

Високе навантаження на запис. Обмеження одним записувачем за раз є властивістю формату файлу, а не параметром, який можна налаштувати. Короткі операції запису виконуються швидко, оскільки кожна транзакція додається до WAL. Тому пропускна здатність більше залежить від затримки дрібних операцій запису на диску, ніж від процесора. Див. NVMe і SATA SSD-сховище на VPS, щоб побачити різницю. Справжня проблема — довгі транзакції, оскільки вони ставлять усі інші операції запису в чергу.

Аналітичні запити. SQLite — це рядкове сховище, розраховане на транзакції. Панель моніторингу, яка сканує сто мільйонів рядків, вирішує інше завдання й потребує іншого інструмента. У матеріалі DuckDB порівняно з SQLite для серверних завдань описано, де проходить ця межа.

VACUUM під час реплікації. Повний VACUUM перезаписує весь файл бази даних. Тому Litestream має повторно завантажити його повністю. Документація Litestream не рекомендує запускати цю операцію, поки реплікація активна. Зупиніть реплікатор, виконайте vacuum, запустіть його знову та очікуйте створення нового повного знімка.

Два реплікатори для однієї бази даних. Ніколи не запускайте два процеси Litestream для тієї самої бази даних або того самого призначення репліки. У документації прямо зазначено, що запобігання такій конфігурації є вашою відповідальністю. Інакше ви отримаєте репліку, яку неможливо відновити.

Що Litestream не захищає

Litestream захищає лише файл бази даних. Завантажені файли, конфігурація застосунку, сертифікати TLS (transport layer security) і unit-файли все одно потрібно опрацьовувати окремо. Налаштуйте його разом із зашифрованими зовнішніми резервними копіями за допомогою restic за розкладом — так будуть охоплені обидві частини. Якщо сервер новий, перші десять хвилин на новому VPS охоплюють налаштування облікового запису користувача та firewall, які цей посібник передбачає виконаними заздалегідь.

FAQ

Чи достатньо SQLite для production-застосунку?

Для одного застосунку на одному сервері — так, якщо ввімкнути режим WAL, встановити busy timeout і безперервно створювати резервні копії. Важливі обмеження мають структурний характер: одночасно може працювати лише один записувач, і база має розміщуватися на одному хості. Застосунок, який вкладається в ці обмеження, отримує базу даних без мережевого переходу та окремого процесу для моніторингу. Якщо застосунок виходить за ці межі, потрібна база даних у моделі клієнт–сервер, і жодне налаштування не змінить цього.

Чому я все ще отримую database is locked після встановлення busy_timeout?

Тому що SQLite пропускає busy handler, якщо очікування може спричинити deadlock. Транзакція, яка починається з простого BEGIN, є відкладеною: початковий SELECT переводить її в транзакцію читання, а подальша операція запису має підвищити її рівень. Якщо тим часом інше підключення виконало запис, SQLite одразу повертає SQLITE_BUSY замість виклику вашого busy handler, оскільки знімок читання вже застарів. Починайте кожну транзакцію, яка виконуватиме запис, з BEGIN IMMEDIATE, щоб блокування запису встановлювалося заздалегідь і застосовувався timeout.

Чи можна зберігати базу даних SQLite у мережевому сховищі?

Не на мережевій файловій системі, такій як NFS або SMB. Режим WAL потребує, щоб усі процеси спільно використовували пам’ять через файл -shm, а документація SQLite зазначає, що кожен процес, який використовує базу даних, має працювати на тому самому хості. Мережевий блочний пристрій, підключений вашим провайдером, — це інший випадок: Linux бачить звичайний диск зі звичайною файловою системою, і SQLite працює на ньому.

Чи потрібен мені Litestream, якщо я вже виконую нічні резервні копіювання?

Це залежить від обсягу даних, який ви можете дозволити собі втратити. Нічне завдання означає можливу втрату записів за період до двадцяти чотирьох годин. Litestream синхронізує дані приблизно раз на секунду, тому після збою ви втратите приблизно останню секунду даних. Він також безпечніший за копіювання файла бази даних за допомогою cp, оскільки так можна отримати копію бази під час виконання запису. Litestream охоплює лише базу даних, тому паралельно з ним налаштуйте загальне резервне копіювання файлів.

#sqlite#wal#litestream#backups#production