SQLite в production на VPS: настройки и ограничения
Узнайте, когда SQLite подходит для небольшого приложения на одном VPS: режим WAL, busy_timeout, репликация Litestream и ограничения одного процесса записи.
Когда SQLite подходит в качестве производственной базы данных на VPS
Запуск SQLite в производственной среде на VPS подходит для большинства небольших приложений. Причина проста: если один процесс на одной машине записывает данные в один файл, сервер базы данных не нужен. Не требуется контролировать отдельный демон, настраивать правила для порта, менять пароль или поддерживать вторую машину. Запрос выполняется как вызов функции, а не как сетевой обмен, поэтому страница с сорока запросами выполняет сорок вызовов функций.
Ограничения SQLite существенны и конкретны. Во всем файле базы данных одновременно может работать только один процесс записи, а сам файл нельзя совместно использовать между двумя машинами. Для одного VPS с одним приложением оба ограничения обычно приемлемы. Но как только приложение выходит за эти рамки, оба ограничения становятся критическими. В этом руководстве описаны настройки, обеспечивающие безопасную работу SQLite на сервере, непрерывное резервное копирование с помощью Litestream и момент, когда следует прекратить использование SQLite.
Сначала установите инструмент командной строки. Все приведенные ниже команды выполнялись в 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 (журналирование с опережающей записью) меняет этот порядок. Процесс записи добавляет новые страницы в отдельный файл -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 содержит зафиксированные страницы, которые еще не были обработаны контрольной точкой. Файл -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. У нее есть конкретная причина.
Тайм-аут ожидания блокировки устанавливает обработчик занятости, но SQLite не гарантирует его вызов.
Если SQLite определяет, что вызов обработчика занятости может привести к взаимной блокировке, он сразу возвращает приложению SQLITE_BUSY вместо вызова обработчика занятости.
Взаимная блокировка возникает при повышении типа транзакции. Одна только 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 versionv0.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.
Указанные выше параметры снимков являются значениями по умолчанию, но значение хранения часто вызывает вопросы. Срок хранения определяет, как долго Litestream сохраняет снимки и относящиеся к ним файлы. Следовательно, он также определяет, насколько далеко в прошлое можно выполнить восстановление. Срок 24 часа означает, что миграция, обнаруженная утром в 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. Поэтому пропускная способность сильнее зависит от задержки диска при малых операциях записи, чем от процессора. См. раздел NVMe и хранилище SATA SSD на VPS, где это различие рассмотрено подробнее. Настоящая проблема — длительные транзакции. Они ставят все остальные операции записи в очередь.
Аналитические запросы. SQLite — строковое хранилище, предназначенное для транзакций. Панель мониторинга, сканирующая сто миллионов строк, решает другую задачу и требует другого инструмента. В разделе DuckDB и SQLite для серверных задач описано, где проходит эта граница.
VACUUM при репликации. Полный VACUUM переписывает весь файл базы данных. Поэтому Litestream должен снова передать его целиком. Документация Litestream не рекомендует выполнять эту операцию непосредственно во время активной репликации. Остановите репликатор, выполните vacuum, запустите его снова и ожидайте создания нового полного снимка.
Два репликатора для одной базы данных. Никогда не запускайте два процесса Litestream для одной базы данных или одного назначения реплики. В документации прямо указано, что предотвращение такой конфигурации — ваша ответственность. В противном случае получится реплика, которую нельзя восстановить.
Что не входит в зону ответственности Litestream
Litestream защищает только файл базы данных. Загруженные файлы, конфигурация приложения, сертификаты TLS (безопасность транспортного уровня) и файлы юнитов по-прежнему требуют отдельного управления. Настройте его совместно с зашифрованными резервными копиями вне сервера с помощью restic по расписанию — тогда будут защищены обе части. Если машина новая, в разделе первые десять минут на новом VPS описаны настройка учетной записи пользователя и firewall, которые в этом руководстве предполагаются уже выполненными.
FAQ
Достаточно ли SQLite для приложения в production?
Для одного приложения на одном сервере — да, если включить режим WAL, задать тайм-аут ожидания освобождения блокировки и выполнять непрерывное резервное копирование. Важны структурные ограничения: одновременно может выполняться запись только из одного источника, а база должна находиться на одном хосте. Приложение, которое укладывается в эти ограничения, получает базу данных без сетевого перехода и отдельного процесса для мониторинга. Если приложение выходит за эти ограничения, нужна база данных в архитектуре клиент–сервер. Никакая настройка не изменит это.
Почему после задания busy_timeout я по-прежнему получаю database is locked?
Потому что SQLite не вызывает обработчик ожидания блокировки, если ожидание может привести к взаимной блокировке. Транзакция, начинающаяся с обычного BEGIN, является отложенной: начальная операция SELECT переводит её в транзакцию чтения, а последующая запись должна повысить её уровень. Если другая транзакция успела выполнить запись, SQLite немедленно возвращает SQLITE_BUSY, а не вызывает обработчик ожидания, поскольку снимок данных для чтения уже устарел. Начинайте любую транзакцию, которая будет выполнять запись, с BEGIN IMMEDIATE. Тогда блокировка записи захватывается заранее, и тайм-аут применяется.
Можно ли хранить базу данных SQLite в сетевом хранилище?
Не в сетевой файловой системе, такой как NFS или SMB. Режиму WAL требуется, чтобы все процессы совместно использовали память через файл -shm. В документации SQLite указано, что каждый процесс, использующий базу данных, должен работать на одном и том же хосте. Сетевое блочное устройство, подключённое вашим провайдером, — другое решение: Linux видит обычный диск с обычной файловой системой, и SQLite работает на нём.
Нужен ли Litestream, если резервные копии уже выполняются каждую ночь?
Это зависит от того, какой объём данных Вы можете позволить себе потерять. Ночное задание означает возможную потерю записей за период до двадцати четырех часов. Litestream синхронизирует данные примерно раз в секунду, поэтому при сбое Вы потеряете примерно последнюю секунду данных. Кроме того, это безопаснее, чем копировать файл базы данных с помощью cp, поскольку копия может быть создана в процессе записи в базу данных. Litestream защищает только базу данных, поэтому одновременно с ним выполняйте общее резервное копирование файлов.