SQLite нередко недооценивают на продакшене, хотя для множества задач он идеален: встроенные CMS/панели, кэши, журналы событий, очереди, аналитика на один узел, edge‑функции. На VDS SQLite даёт стабильную предсказуемую производительность при минимальном операционном оверхеде — если правильно включить WAL, выставить busy_timeout, спроектировать транзакции и продумать бэкапы. Ниже — концентрат практики, проверенный в бою.
Когда SQLite на VDS — это хорошая идея
SQLite отлично подходит там, где:
- записи короткие, а транзакции быстрые (миллисекунды);
- большинство операций — чтение, а запись — всплесками;
- нет требования к горизонтальному шардингу/репликации на уровне СУБД;
- важна простота эксплуатации: один файл данных, нулевой DevOps‑сложности.
Но важно помнить, что одновременно писать может только один процесс (или одна транзакция) — это архитектурный предел SQLite. Режим WAL снимает конфликт между многими читателями и одним писателем, однако несколько писателей по‑прежнему конфликтуют.
Если вам не нужен root‑доступ и нагрузка невысокая, SQLite отлично живёт и на качественном виртуальном хостинге.
WAL: что это и почему он почти всегда обязателен
В режимах журналирования по умолчанию (ROLLBACK‑journal) читатели и писатели мешают друг другу. WAL (Write‑Ahead Logging) меняет порядок: запись уходит в WAL‑файл, читатели продолжают читать старые страницы без ожидания, а чекпоинт переносит изменения обратно в основной файл.
Плюсы WAL на VDS:
- чтения не блокируют запись и наоборот (при условии коротких транзакций);
- стабильнее латентность, меньше «ступенек» под нагрузкой;
- улучшенная устойчивость к сбоям при корректной настройке
synchronous.
Базовые команды и проверки:
sqlite3 app.db "PRAGMA journal_mode=WAL; PRAGMA wal_autocheckpoint=1000; PRAGMA synchronous=NORMAL;"
Комментарии к настройкам:
journal_mode=WAL— включает WAL‑журналирование;wal_autocheckpoint=1000— инициировать чекпоинт примерно каждые 1000 страниц WAL (подбирайте экспериментально: если WAL растёт слишком быстро — уменьшайте, если чекпоинты мешают — увеличивайте);synchronous=NORMAL— хороший баланс производительности и надёжности для большинства приложений на VDS.FULLдаёт максимум прочности, но дороже по I/O.
busy_timeout: спасение от «database is locked»
busy_timeout — это окно ожидания (в миллисекундах), в течение которого SQLite будет повторять попытку взять блокировку вместо мгновенной ошибки database is locked. По умолчанию 0 (не ждать).
Практические рекомендации:
- Для веб‑приложений ставьте 500–3000 мс: достаточно, чтобы переждать чужую короткую запись.
- Комбинируйте с транзакциями
BEGIN IMMEDIATEдля записей, чтобы сразу занять write‑lock и избежать поздних сюрпризов. - Следите за метриками: если часто упираетесь в timeout, значит транзакции слишком длинные или писателей слишком много.
PHP (PDO SQLite) — включаем WAL и busy_timeout
<?php
$dsn = 'sqlite:' . __DIR__ . '/app.db';
$db = new PDO($dsn, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
// Базовые PRAGMA на каждый коннект
$db->exec('PRAGMA journal_mode=WAL');
$db->exec('PRAGMA synchronous=NORMAL');
$db->exec('PRAGMA wal_autocheckpoint=1000');
$db->exec('PRAGMA busy_timeout=2000'); // 2 секунды
// Запись с немедленной блокировкой на запись
$db->beginTransaction();
$db->exec('BEGIN IMMEDIATE');
$stmt = $db->prepare('INSERT INTO events(ts, payload) VALUES(strftime("%s","now"), :p)');
$stmt->execute([':p' => json_encode(['ok' => true])]);
$db->commit();
Node.js — better-sqlite3 и sqlite3
Для Node.js чаще всего используют better-sqlite3 (синхронный, быстрый для коротких запросов) или классический sqlite3 (асинхронный). В обоих случаях включаем WAL и busy_timeout.
// better-sqlite3
const Database = require('better-sqlite3');
const db = new Database('app.db', { fileMustExist: false });
// Настройка PRAGMA на старте процесса
['PRAGMA journal_mode=WAL',
'PRAGMA synchronous=NORMAL',
'PRAGMA wal_autocheckpoint=1000',
'PRAGMA busy_timeout=2000']
.forEach(sql => db.pragma(sql.replace('PRAGMA ', '')));
// Паттерн короткой записи
const insert = db.prepare('INSERT INTO events(ts, payload) VALUES(strftime("%s","now"), ?)');
const write = (payload) => {
const tx = db.transaction((p) => {
insert.run(JSON.stringify(p));
});
tx(payload);
};
Если используется sqlite3 (asynchronous), задайте busyTimeout на уровне подключения:
const sqlite3 = require('sqlite3');
const { open } = require('sqlite');
(async () => {
const db = await open({
filename: 'app.db',
driver: sqlite3.Database
});
await db.exec('PRAGMA journal_mode=WAL');
await db.exec('PRAGMA synchronous=NORMAL');
await db.exec('PRAGMA wal_autocheckpoint=1000');
await db.exec('PRAGMA busy_timeout=2000');
})();

Транзакционные паттерны: один писатель, много читателей
SQLite допускает множество конкурентных читателей, но только одна активная транзакция записи. Поэтому главный принцип — удерживать write‑lock как можно меньше:
- Готовьте данные заранее (в памяти), пишите быстро, атомарно;
- Используйте
BEGIN IMMEDIATE, чтобы сразу занять write‑lock и не тратить время посреди транзакции; - Избегайте долгих SELECT перед записью в той же транзакции — разделяйте чтение и запись;
- Делайте батчи: лучше одна короткая транзакция с 100 вставками, чем 100 отдельных транзакций.
Если у вас несколько воркеров, пишущих одновременно, добавьте «гейт» (канал) на уровне приложения: очередь на один писатель. Для Node.js это может быть простой in‑process очередь; для PHP — отдельный процесс‑воркер, принимающий задания через очередь/IPC. Да, это доп.слой, но он окупается стабильностью.
Файловая система и VDS: что важно для производительности и durability
На VDS в 2025 году чаще встречаются ext4 и XFS. Оба подходят для SQLite, но есть нюансы:
- ext4: по умолчанию data=ordered, хорошие гарантии порядка записи. Отличный выбор для баз небольшого и среднего размера.
- XFS: стабилен под параллельной нагрузкой и на больших файлах; отлично работает на NVMe‑хранилище.
Рекомендации:
- Не храните базу на сетевых FS (NFS/SMB) — риск неожиданных блокировок и потери гарантий fsync. Держите базу на локальном блочном устройстве VDS.
- Проверьте правильность времени и часового пояса (chrony/systemd‑timesyncd), чтобы не сбить расписания бэкапов и TTL WAL‑логики.
- Увеличьте лимит дескрипторов для процессов, активно работающих с SQLite (и бэкап‑агентов).
# временно в shell
ulimit -n 65536
# постоянный лимит через systemd для вашего сервиса
# /etc/systemd/system/app.service
[Service]
LimitNOFILE=65536
Если подбираете ресурсы сервера под SQLite, пригодится материал про выбор CPU и RAM для VDS под нагрузку. Для повышения надёжности юнитов посмотрите и усиление сервисов systemd на VDS.
Контроль роста WAL и чекпоинты
Если WAL растёт без сдерживания, значит чекпоинты происходят редко или долго. Проверьте автопорог и инициируйте ручной чекпоинт в кроне или системном таймере.
sqlite3 app.db "PRAGMA wal_checkpoint(TRUNCATE);"
TRUNCATE после чекпоинта обрезает WAL‑файл (если это допустимо). Для интенсивной записи вместо агрессивного TRUNCATE оставьте PASSIVE или RESTART и увеличьте автопорог, чтобы реже дергать диск.
Производительность: ключевые PRAGMA
journal_mode=WAL— базис.synchronous=NORMAL— компромисс между IOPS и гарантией.wal_autocheckpoint— подберите экспериментально под ваш профиль.temp_store=MEMORY— меньше временных файлов, если хватает RAM.mmap_size— может ускорить чтение больших таблиц, но тестируйте.cache_size— отрицательное значение задаёт размер в КБ; полезно ограничить чтобы не выедать память.
sqlite3 app.db "PRAGMA temp_store=MEMORY; PRAGMA mmap_size=268435456; PRAGMA cache_size=-131072;"
Не переусердствуйте с кэшами: SQLite умеет эффективно работать и на умеренных значениях, а борьба за RAM на VDS иногда дороже выигрыша в латентности.
Durability: что реально сохраняется при сбоях
synchronous=FULL максимально жёстко синхронизирует журнал и базовый файл; NORMAL упрощает часть fsync и почти всегда безопасен при использовании WAL на корректных дисках. Реальная устойчивость зависит от стека: файловая система, драйвер диска, контроллер, виртуализация. Проверьте поведение на стенде с искусственными сбоями (отключение питания VM, kill -9 процесса) и убедитесь, что вы не теряете подтверждённые транзакции.
Если у вас SLA уровня критичных финансовых систем — рассмотрите внешнюю СУБД с репликацией. SQLite создан как встраиваемая БД и не стремится заменить кластерные решения.
Онлайн-бэкапы: .backup, VACUUM INTO, pg‑style «горячее копирование»
Для горячего бэкапа SQLite предусмотрен собственный API, доступный через CLI:
# Горячая копия без остановки сервиса
sqlite3 app.db ".backup 'backup/app-$(date +%F).db'"
# Начиная с 3.27: VACUUM INTO создаёт компактную копию
sqlite3 app.db "VACUUM INTO 'backup/app-compact.db'"
Это безопасно даже в WAL‑режиме: механизм использует внутренний backup‑API и не ломает активные транзакции. Имейте в виду, что VACUUM INTO сильнее нагружает диск, зато дефрагментирует файл.
Потоковая репликация WAL: Litestream
Litestream — агент, который отслеживает WAL и отправляет сегменты в удалённое хранилище (обычно S3‑совместимое). Это даёт точку восстановления во времени (PITR) и быструю перезаливку баз на новый сервер VDS при миграции или аварии.
Минимальный конфиг Litestream (пример):
# /etc/litestream.yml
dbs:
- path: /srv/app/app.db
replicas:
- url: s3://my-bucket/sqlite/app
endpoint: s3.example.internal
access-key-id: MYKEY
secret-access-key: MYSECRET
retention: 30d
Сервис systemd:
# /etc/systemd/system/litestream.service
[Unit]
Description=Litestream WAL shipping
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/litestream replicate
Restart=always
RestartSec=2s
User=litestream
Group=litestream
UMask=007
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
Стратегия восстановления:
- Остановить приложение, убедиться, что локальный процесс не держит файл.
- Восстановить базу из последнего snapshot и WAL‑журнала на нужную точку времени.
- Запустить приложение, проверить целостность (
PRAGMA integrity_check).
# Пример проверки целостности
sqlite3 app.db "PRAGMA integrity_check;"
При использовании Litestream обязательно резервируйте и сам файл базы, и снапшоты, и WAL‑сегменты. Проверьте восстановление на отдельной ВМ: «бэкап считается сделанным только после тестового restore».

Обслуживание: VACUUM, ANALYZE, индексы
Регулярные операции ускоряют запросы и держат размер базы под контролем:
ANALYZE— обновляет статистику планировщика;VACUUM— дефрагментация и переупаковка файла (дорогая операция);PRAGMA optimize— мягкая оптимизация на базе статистики.
sqlite3 app.db "ANALYZE; PRAGMA optimize;"
Если у вас много удалений, подумайте о PRAGMA incremental_vacuum и рациональной структуре индексов. Число индексов напрямую влияет на стоимость записи: каждая вставка обновляет все соответствующие индексы.
Наблюдаемость: что мерить и как понимать симптомы
Признаки перегрузки писателя:
- рост времени ответа на INSERT/UPDATE;
- участившиеся таймауты
busy_timeout; - заметный рост WAL с редкими чекпоинтами.
Команды диагностики:
sqlite3 app.db ".dbinfo"
sqlite3 app.db "PRAGMA wal_checkpoint(PASSIVE);"
sqlite3 app.db "PRAGMA page_size; PRAGMA page_count;"
Собирайте метрики на уровне приложения: число попыток записи, доля таймаутов/ретраев, размер базы, размер WAL. Журналы с замером времени транзакций резко упрощают поиск «долгих» мест.
Безопасность и права
Права на файлы базы и WAL должны позволять запись только сервисному пользователю приложения. Хорошая практика — выделенный системный пользователь/группа и umask 007 для исключения лишнего чтения. Бэкапы храните в отдельном каталоге с ограниченными правами. Если нужна защита «на покое», используйте шифрование на уровне диска/тома средствами ОС.
Частые ошибки и как их избежать
- Размещение базы на NFS/SMB: нестабильные блокировки и потеря гарантии flush — переносите на локальный диск.
- Длинные транзакции записи: дробите на батчи, поднимите
busy_timeout, используйтеBEGIN IMMEDIATE. - Отсутствие регулярных чекпоинтов: следите за ростом WAL и инициируйте
wal_checkpoint. - Отсутствие восстановимых бэкапов: внедрите .backup/VACUUM INTO и потоковую репликацию WAL, делайте тестовые restore.
- Грубые PRAGMA «на глаз»: каждую настройку проверяйте бенчмарком под своей нагрузкой.
Мини‑чек‑лист для продакшена на VDS
- Включить
journal_mode=WAL, выставитьsynchronous=NORMALилиFULLпод SLA. - Настроить
busy_timeout500–3000 мс, писать черезBEGIN IMMEDIATE, батчировать. - Подобрать
wal_autocheckpoint, поставить периодический чекпоинт. - Вести горячие бэкапы (.backup или VACUUM INTO) и/или Litestream; регулярно проверять восстановление.
- Держать базу на локальном ext4/XFS, поднять
LimitNOFILEу сервиса. - Мониторить латентность транзакций, размер WAL и частоту таймаутов.
Пример «с нуля»: инициализация базы и миграции
# Создать базу, включить WAL и базовые настройки
sqlite3 app.db "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA wal_autocheckpoint=1000;"
# Создать таблицы
sqlite3 app.db "CREATE TABLE IF NOT EXISTS events(id INTEGER PRIMARY KEY, ts INTEGER, payload TEXT);"
# Проверка
sqlite3 app.db ".tables"
# Тестовая вставка
sqlite3 app.db "INSERT INTO events(ts, payload) VALUES(strftime('%s','now'), '{\"hello\":true}');"
# Чтение
sqlite3 app.db "SELECT count(*), min(ts), max(ts) FROM events;"
Дальше подключайте приложение (PHP/Node.js), не забывая выполнять PRAGMA на соединении. Для миграций удобно хранить версионированные SQL‑файлы и вести таблицу migrations с применёнными версиями; миграции выполняйте в одной транзакции с BEGIN IMMEDIATE.
Итого
SQLite на VDS — это надёжно и быстро, когда спроектированы короткие записи, включён WAL, выставлен разумный busy_timeout, а бэкапы и чекпоинты идут по расписанию. Добавьте дисциплину транзакций, мониторинг и регулярное тестирование восстановления — и получите лёгкую в эксплуатации БД с очень низкой стоимостью владения. Для PHP и Node.js настройки просты, а выигрыш в предсказуемости — ощутим с первого дня.


