SSD Nodes Learn 8GB RAM — $66/рік
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-01

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 є правильним виробничим сховищем даних на VPS

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

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

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

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

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

Чому режим WAL — це перша зміна, яку потрібно внести

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

Режим WAL (write-ahead log) змінює цей порядок. Операція запису додає нові сторінки до окремого файлу -wal і не змінює основну базу даних. Операції читання продовжують читати основний файл у межах знімка, створеного на момент їх початку. Тому читання не блокує запис, а запис не блокує читання. Пізніше контрольна точка копіює накопичені сторінки 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 до нового файлу, який з’явиться під цим іменем. Так можна пошкодити нову базу даних під час спроби скинути стару.

Налаштування підключення, потрібні кожному робочому застосунку

У базі даних зберігається лише 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. Вона має конкретну причину.

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

Другу причину помилок блокування помітити складніше: тривале виконання повільної операції всередині транзакції запису. 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. Змініть версію в обох рядках відповідно до поточного тегу на сторінці випусків. Якщо ваш 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 і пов’язані з ними файли. Отже, воно також визначає, наскільки далеко в минуле можна виконати відновлення. Двадцять чотири години означає, що помилкова міграція, виявлена вранці 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 встановлює модуль litestream, який читає /etc/litestream.yml.

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

У штатному виводі для кожної бази даних указується її назва з конфігурації, після чого відображаються лише періодичні рядки синхронізації. Помилка no such file or directory для шляху до бази даних означає, що шлях у конфігурації неправильний або процес не може прочитати цей файл. За замовчуванням модуль працює від імені 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 модуля вашої програми, і новий VPS завантажить базу даних, а на вже налаштованому сервері нічого не зробить. litestream replicate має відповідний прапорець -restore-if-db-not-exists, якщо ви хочете зберігати цю конфігурацію в одному місці.

Де SQLite дає збій на VPS

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

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

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

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

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

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

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

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

Що Litestream не охоплює

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

FAQ

Чи достатньо SQLite для робочого застосунку?

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

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

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

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

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

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

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

#sqlite#wal#litestream#backups#production