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

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 — правильний вибір для більшості невеликих застосунків. Причина проста: процесу, який на одному комп’ютері записує дані в один файл, не потрібен database server. Не потрібно контролювати окремий daemon, відкривати порт у firewall, періодично змінювати пароль або підтримувати в роботі другу машину. Запит є викликом функції, а не мережевим обміном, тому сторінка із сорока запитами виконує сорок викликів функцій.

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

Спочатку встановіть command line tool. Усі наведені нижче команди виконано на Ubuntu 24.04.

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

Команда виводить версію, яка починається з 3., а далі містить дату збірки та hash вихідного коду. Станом на October 2026 Ubuntu 24.04 постачається з SQLite 3.45.1. Ваш застосунок, імовірно, не використовує цей binary: більшість language runtime постачає власну копію SQLite library, часто новішу. Тому перед використанням нової функції перевірте версію, яку повертає ваш 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" використовує інтерфейс online backup у SQLite і працює з базою даних під час її використання. Litestream іде далі: він відстежує WAL і безперервно передає зміни до object storage. Це зменшує максимальну можливу втрату даних з доби приблизно до 1 секунди.

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 станом на October 2026. Пізніше вийшли новіші версії: v0.5.15 21 July 2026 року та v0.5.17 — найновіший tag від 31 August 2026 року. Змініть версію в обох рядках відповідно до поточного tag на сторінці releases. Якщо ваш VPS працює на arm64, замість цього завантажте відповідний пакет arm64.

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

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

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

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 секунду. Зачекайте та повторіть відновлення. Ця 1 секунда також є вашою точкою відновлення. У разі збою буде втрачено щонайбільше записи з останнього інтервалу синхронізації. Жодна конфігурація не може зменшити цей інтервал до 0.

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

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 зі змінних середовища. Додайте їх до systemd drop-in, власником якого є root, і встановіть режим 600.

Наведені вище значення snapshot є значеннями за замовчуванням, а значення retention часто стає несподіванкою. Retention визначає, як довго Litestream зберігає snapshot і пов’язані з ними файли. Отже, воно також визначає, наскільки далеко в минуле можна виконати відновлення. Двадцять чотири години означають, що помилкова міграція, яку ви помітили вранці Wednesday, уже не може бути відновлена зі стану Monday. У Litestream 0.5 retention є глобальним параметром у кореневому блоці snapshot, а не параметром окремої репліки. Щоб зберігати дані протягом тижня, змініть цей блок на retention: 168h і врахуйте додаткові витрати на storage. Рядок retention: усередині блоку репліки є синтаксисом серії 0.3. Litestream 0.5 ігнорує його без повідомлення про помилку та залишає значення за замовчуванням — 24 години.

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

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 поруч із базою даних. Тому вкажіть обліковий запис користувача, від імені якого вже працює ваш застосунок, — appuser у цих прикладах.

# /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. Тому пропускна здатність більше залежить від затримки диска під час малих операцій записування, ніж від CPU. Див. NVMe проти SATA SSD у сховищі VPS, щоб побачити різницю. Справжню проблему становлять довгі транзакції, оскільки вони ставлять усі інші операції записування в чергу.

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

VACUUM під час реплікації. Повний VACUUM переписує весь файл бази даних. Через це стає недійсним відстеження змін на рівні сторінок, від якого залежить Litestream. Документація Litestream не радить виконувати цю операцію на місці, коли реплікація активна. Натомість рекомендується VACUUM INTO. Ця команда записує оптимізовану копію в окремий файл і залишає оригінал без змін. Якщо потрібно виконати vacuum на місці, задокументована процедура довша за перезапуск: зупиніть реплікатор, виконайте vacuum, видаліть прихований каталог відстеження Litestream поруч із базою даних і наявні дані віддаленої репліки, а потім знову запустіть реплікатор, щоб він почав роботу з нового повного snapshot. Останній крок видаляє наявну історію відновлення.

Два реплікатори для однієї бази даних. Ніколи не запускайте два процеси 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