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

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

Узнайте, как использовать SQLite для небольших проектов на одном 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 — верное решение для большинства небольших приложений, и причина проста: одному процессу на одной машине, записывающему данные в один файл, не нужен сервер базы данных. Нет демона, за которым нужно следить, нет порта, который нужно закрывать файрволом, нет пароля, который нужно регулярно менять, и нет второй машины, которую нужно поддерживать в рабочем состоянии. Запрос — это вызов функции, а не сетевой обмен данными, поэтому страница, выполняющая сорок запросов, обходится вам в сорок вызовов функций.

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

Сначала установите инструмент командной строки. Все приведенные ниже команды были выполнены в 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, а затем редактирует саму базу данных. Для обеспечения безопасности 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 означает, что изменение не удалось и вы по-прежнему используете 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 содержит зафиксированные страницы, которые еще не были перенесены в основную базу данных (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 при каждой фиксации транзакции (commit). При NORMAL синхронизация происходит только во время контрольных точек (checkpoints). Документация SQLite прямо говорит о последствиях: транзакции перестают быть долговечными в случае сбоя питания или принудительной перезагрузки. База данных не будет повреждена из-за потери питания, вы просто потеряете последние транзакции, которые не успели записаться на диск. На VPS это обычно оправданный компромисс, так как он исключает вызов fsync из пути каждой операции записи.

foreign_keys = ON по умолчанию отключен для обеспечения обратной совместимости и настраивается для каждого соединения отдельно. Схема, содержащая множество предложений REFERENCES, не обеспечивает никаких ограничений, пока каждое соединение не активирует этот параметр.

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

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

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

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

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

Взаимная блокировка, которой он пытается избежать, возникает при повышении уровня транзакции. Обычная команда 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 по состоянию на октябрь 2026 года остается релизом, указанным на официальной странице установки для Linux. Позже вышли новые версии: v0.5.15 от 21 июля 2026 года и v0.5.17 (новейший тег) от 31 августа 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 был заменен на один блок 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 хранит снимки и относящиеся к ним файлы, а значит, и то, как далеко назад во времени вы можете восстановиться. Двадцать четыре часа означают, что неудачную миграцию, замеченную в среду утром, уже невозможно будет откатить до состояния понедельника. В Litestream 0.5 параметр retention является глобальной настройкой в корневом блоке snapshot, а не для каждой реплики отдельно. Чтобы установить срок в одну неделю, измените этот блок на retention: 168h и оплатите дополнительное место в хранилище. Строка retention: внутри блока replica — это синтаксис серии 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 устанавливает юнит 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, расположенными рядом с базой. Поэтому укажите учетную запись пользователя, от имени которой уже работает ваше приложение, в данных примерах это 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 в юните вашего приложения: тогда на новом VPS база данных будет загружена, а на уже существующем — никаких действий не потребуется. litestream replicate имеет соответствующий флаг -restore-if-db-not-exists, если вы предпочитаете хранить настройки в одном месте.

Где SQLite не подходит для VPS

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

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

Поэтому база данных, расположенная на смонтированном сетевом ресурсе NFS или SMB, может быть повреждена, и никакие прагмы не предотвратят это. Здесь есть важное различие, которое часто упускают из виду. Сетевое блочное устройство, которое большинство провайдеров VPS подключают в качестве дополнительного хранилища, для Linux выглядит как обычный диск с обычной файловой системой, и это допустимо. Смонтированный сетевой ресурс — нет.

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

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

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

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

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

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

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

FAQ

Подходит ли SQLite для промышленного приложения?

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

Почему я всё ещё получаю 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