SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-07

SQLite в продакшене на VPS: настройка и ограничения

Узнайте, как использовать SQLite для небольших проектов на одном VPS. Разбираем настройку WAL mode, busy_timeout, репликацию через Litestream и признаки перехода на SQL.

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

Когда SQLite подходит в качестве базы данных для продакшена на VPS

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

Ограничения SQLite конкретны и понятны. SQLite позволяет выполнять только одну операцию записи за раз для всего файла базы данных, и этот файл нельзя использовать совместно на двух машинах. Оба этих ограничения приемлемы для одного VPS, на котором запущено одно приложение. Однако оба они становятся критическими, как только вы перерастаете этот формат. В этом руководстве рассматриваются настройки, обеспечивающие безопасность SQLite на сервере, непрерывное резервное копирование с помощью Litestream, а также момент, когда следует отказаться от использования SQLite.

Сначала установите консольную утилиту. Все приведенные ниже команды были выполнены в Ubuntu 24.04.

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

Эта команда выведет версию, начинающуюся с 3., за которой последуют дата сборки и хеш исходного кода. По состоянию на июль 2026 года в Ubuntu 24.04 поставляется SQLite 3.45.1. Ваше приложение, скорее всего, не использует этот бинарный файл: большинство сред выполнения языков программирования включают собственную копию библиотеки SQLite, зачастую более новую. Поэтому перед использованием новых функций проверьте версию, которую сообщает драйвер вашей базы данных.

Почему режим WAL — это первое, что нужно изменить

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

Режим 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 означает, что изменение не удалось и вы по-прежнему используете rollback journal.

Режим 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 содержит зафиксированные (committed) страницы, которые еще не были перенесены в основной файл (checkpointed). Файл -shm — это индекс разделяемой памяти, который отображается каждым соединением, чтобы все они имели согласованное представление о содержимом WAL. Оба файла принадлежат базе данных и не являются временными. Если скопировать только app.db во время работы приложения, вы получите файл, в котором будут отсутствовать все последние коммиты. Если удалить app.db, оставив два других файла, SQLite применит эти устаревшие страницы WAL к любому новому файлу, который появится под этим именем. Именно так пользователи повреждают новую базу данных, пытаясь сбросить состояние существующей.

The connection settings every production app needs

Only journal_mode is stored in the database. Every other setting below is per connection, which means your application has to run it on each connection it opens, including every connection a pool creates in the background.

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

busy_timeout = 5000 tells SQLite to keep retrying a locked database for up to 5000 milliseconds before it returns database is locked. The default is 0, so by default SQLite fails instantly the first time two writers overlap. Setting this single value removes most of the lock errors that get blamed on SQLite itself.

synchronous = NORMAL is the right setting in WAL mode, and the trade is worth understanding. At FULL, SQLite calls fsync on the WAL at every commit. At NORMAL, it syncs at checkpoints instead. The SQLite documentation is blunt about what you give up: transactions are no longer durable after a power failure or a hard reset. The database cannot be corrupted by that power loss, you simply lose the last commits that had not reached the disk. On a VPS that is usually the right trade, because it takes an fsync out of the path of every single write.

foreign_keys = ON is off by default for backwards compatibility, and it is per connection. A schema full of REFERENCES clauses enforces nothing at all until each connection turns this on.

One more setting matters only later. SQLite checkpoints automatically once the WAL grows past 1000 pages, and the work is done by whichever connection happens to finish a transaction at that moment. That is fine on its own. It becomes a question when Litestream is running, because Litestream wants control over when checkpoints happen.

Почему database is locked все еще возникает после настройки busy_timeout

Это ошибка, из-за которой пользователи возвращаются к Postgres, и у нее есть одна конкретная причина.

Тайм-аут занятости (busy timeout) устанавливает обработчик занятости, но SQLite не гарантирует его вызов.

Если SQLite определяет, что вызов обработчика занятости может привести к взаимной блокировке (deadlock), он вернет приложению SQLITE_BUSY вместо вызова обработчика.

Взаимная блокировка, которую предотвращает SQLite, возникает при повышении уровня транзакции. Обычная команда BEGIN в SQLite означает BEGIN DEFERRED. Если первый оператор после нее — SELECT, вы находитесь в транзакции чтения. Когда позже в этой же транзакции требуется выполнить UPDATE, а другое соединение уже записало данные с момента начала вашего чтения, SQLite не может заставить вас ждать. Ваш снимок данных (snapshot) уже устарел, и ожидание привело бы к взаимной блокировке двух соединений. Документация прямо указывает на результат:

Последующие операторы записи повысят уровень транзакции до транзакции записи, если это возможно, или вернут SQLITE_BUSY.

Ваш тайм-аут в 5000 миллисекунд даже не учитывается. Ошибка приходит мгновенно, поэтому кажется, что настройка не сработала.

Решение состоит из одного слова.

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

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

Вторая причина ошибок блокировки менее очевидна: удержание транзакции записи открытой во время выполнения медленных операций. SQLite сериализует процессы записи, поэтому транзакция, которая открывается, выполняет вызов внешнего API по сети, а затем делает commit, будет блокировать всех остальных писателей на все время этого вызова. Считайте необходимые данные, закройте транзакцию, выполните медленную работу, а затем откройте короткую транзакцию записи для сохранения результата.

Непрерывное резервное копирование с помощью 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 по состоянию на июль 2026 года, а v0.5.15 вышла 21 июля 2026 года. Измените версию в обеих строках в соответствии с текущим тегом на странице релизов, и используйте соответствующий пакет arm64, если ваш VPS работает на архитектуре 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 был заменен на один блок replica, и конфигурация, содержащая две записи, теперь вызывает ошибку при запуске. Многие сторонние руководства до сих пор показывают старый массив, поэтому копируйте структуру выше, а не первый попавшийся пример из поиска. В серии 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 секунду, поэтому подождите и выполните восстановление снова. Эта одна секунда также является вашей точкой восстановления. При сбое теряются данные максимум за последний интервал синхронизации, и никакая конфигурация не позволит свести это значение к нулю.

Для реального хранилища замените блок replica на S3 URL. Это работает как с 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.

Указанные выше значения снимков состояния являются значениями по умолчанию, и значение срока хранения по умолчанию часто становится неожиданностью. Срок хранения (retention) определяет, как долго Litestream хранит снимки и относящиеся к ним файлы, а значит, и то, как далеко назад во времени вы можете восстановиться. Двадцать четыре часа означают, что неудачную миграцию, которую вы заметили в среду утром, уже невозможно будет откатить до состояния понедельника. Установите 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 (network file system) или SMB, может быть повреждена, и никакие pragma это не предотвратят. Здесь есть важное различие, которое часто упускают. Сетевое блочное устройство (network block device), которое большинство провайдеров VPS подключают в качестве дополнительного хранилища, для Linux выглядит как обычный диск с обычной файловой системой, и это допустимо. Смонтированный сетевой ресурс (file share) — нет.

Второй сервер приложений. Нет такой настройки, которая заставит это работать. Как только вам потребовалось два сервера для обслуживания одних и тех же данных, вам нужна база данных, работающая по сети. Примите решение об этом переходе, пока у вас есть время на планирование.

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

Аналитические запросы. SQLite — это построчное хранилище, созданное для транзакций. Сканирование ста миллионов строк в панели мониторинга — это другая задача для другого инструмента, и в DuckDB в сравнении с SQLite для серверных задач описано, где проходит эта граница.

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

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

Что не входит в зону ответственности Litestream

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

FAQ

Достаточно ли SQLite для промышленного приложения?

Для одного приложения на одном сервере — да, при условии, что вы включили режим WAL, установили busy timeout и настроили непрерывное резервное копирование. Ограничения здесь структурные: только один процесс записи одновременно и привязка к одному хосту. Если приложение вписывается в эти рамки, вы получаете базу данных без сетевых задержек и необходимости следить за отдельным процессом СУБД. Если приложение не вписывается, ему нужна клиент-серверная база данных, и никакая настройка этого не изменит.

Почему я всё ещё получаю database is locked после установки busy_timeout?

Потому что SQLite пропускает обработчик ожидания (busy handler), если ожидание может привести к взаимной блокировке (deadlock). Транзакция, начинающаяся с обычного BEGIN, является отложенной: начальный SELECT переводит её в режим чтения, а последующая запись требует повышения прав доступа. Если в этот момент другое соединение выполнило запись, SQLite немедленно возвращает SQLITE_BUSY вместо вызова вашего обработчика, так как ваш снимок данных для чтения уже устарел. Начинайте любую транзакцию, которая предполагает запись, с BEGIN IMMEDIATE: тогда блокировка записи будет запрошена сразу, и таймаут сработает корректно.

Можно ли хранить базу данных SQLite на сетевом хранилище?

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

Нужен ли мне Litestream, если я уже делаю ночные резервные копии?

Это зависит от того, каким объемом данных вы готовы пожертвовать. Ночное задание означает потерю данных за период до двадцати четырех часов. Litestream выполняет синхронизацию примерно раз в секунду, поэтому при сбое вы теряете данные лишь за последнюю секунду. Это также безопаснее, чем копирование файла базы данных через cp, так как при копировании можно захватить состояние базы в процессе записи. Litestream работает только с базой данных, поэтому продолжайте выполнять общее резервное копирование файлов параллельно.

#sqlite#wal#litestream#backups#production