SQLite на одном сервере — это быстро, дёшево и до первого сбоя диска. Litestream закрывает ровно эту дыру: он читает журнал WAL и непрерывно копирует изменения базы в S3-совместимое хранилище, поэтому базу можно восстановить не только «на вчера», но и на конкретную минуту. Настроить это на VPS реально за полчаса: бинарник, конфиг на двадцать строк, юнит systemd, проверка восстановления. Ниже — рабочий порядок с командами, актуальный для Litestream 0.5.17 (релиз 31 августа 2026, лицензия Apache-2.0), с учётом того, что формат конфигурации в ветке 0.5 изменился и старые статьи с snapshot-interval внутри реплики уже не соответствуют документации.
Когда SQLite на VPS — правильный выбор, а когда нет
SQLite хорошо работает там, где приложение одно, а нагрузка на запись умеренная. Типичные примеры: телеграм-бот с состоянием, внутренний сервис, личный кабинет, сборщик метрик, небольшая CRM для отдела, сервис на несколько тысяч запросов в минуту с преимущественно чтением.
Плюсы, ради которых его берут: транзакции и целостность, нулевая настройка, скорость локального файла на NVMe, отсутствие отдельного сервера БД и связанной с ним обвязки — реплик, бэкапов, обновлений мажорных версий.
Пределы, за которые лучше не выходить:
- Один активный писатель. SQLite блокирует базу на запись целиком. Если два процесса в вашей архитектуре пишут одновременно и часто — будете ловить
database is locked, а не параллелизм. - Нет сетевого доступа. На файл базы нельзя смотреть по сети (NFS, SMB) — повреждение почти гарантировано.
- Нет встроенной репликации. Именно эту задачу и решает Litestream, но только в одну сторону: локальная база — источник, S3 — копия.
- Размер базы. До десятков гигабайт всё спокойно. Дальше время восстановления и VACUUM начинают мешать.
Если у вас несколько приложений пишут в одну базу с разных машин — SQLite не подходит, нужен PostgreSQL. Если пишет одно приложение, а нужен бэкап и восстановление на момент — Litestream это делает лучше, чем ночной cp файла.
Как работает связка: WAL, Litestream и S3
Схема простая, и её стоит держать в голове, потому что все ошибки настройки — про неё.
Первый слой — режим WAL в SQLite. В обычном режиме (journal) запись идёт в журнал отката и сразу в основной файл. В режиме WAL изменения сначала попадают в файл app.db-wal, а в основную базу переезжают при чекпоинте. Это даёт два эффекта: читатели не блокируют писателя, а все свежие изменения лежат отдельным файлом, который удобно читать снаружи.
Второй слой — Litestream. Он не встраивается в приложение и ничего не знает про ваш код. Он открывает базу, читает WAL и раз в sync-interval (по умолчанию 1 секунда) отправляет изменения в S3 в виде файлов LTX — по сути инкрементальных сегментов. Параллельно Litestream уплотняет их по уровням (30 секунд, 5 минут, час по умолчанию) и раз в сутки складывает полный снапшот.
Третий слой — восстановление. На новом сервере Litestream скачивает ближайший снапшот и накатывает на него сегменты WAL до нужного момента: до последнего (обычный restore) или до заданного времени (PITR, point-in-time recovery).
Что Litestream не делает — и это важно, чтобы не строить ложных ожиданий:
- не является репликацией для чтения с нескольких нод: запись всё равно одна, локальная;
- не защищает от ошибки в самом приложении: если код удалил таблицу, это уедет в S3 через секунду, спасает только PITR и то, пока жива история;
- не заменяет контроль целостности: восстановленную базу надо проверять;
- не работает без WAL — если режим журнала не включён, репликация просто не стартует.
Шаг 1. Подготовка базы: WAL и параметры
Начните с режима журнала и трёх прагм. Выполнять на сервере, где лежит база:
sudo -u myapp sqlite3 /var/lib/myapp/app.db "PRAGMA journal_mode=WAL;"
sudo -u myapp sqlite3 /var/lib/myapp/app.db "PRAGMA synchronous=NORMAL;"
sudo -u myapp sqlite3 /var/lib/myapp/app.db "PRAGMA busy_timeout=5000;"
Что здесь важно:
journal_mode=WAL— обязательное условие. Проверить, что применилось:PRAGMA journal_mode;должно вернутьwal.synchronous=NORMAL— компромисс: при падении процесса данные не теряются, при падении сервера можно потерять последние транзакции, но база останется целой. Для денег и бухгалтерии ставьтеFULL.busy_timeout=5000— вместо мгновенной ошибки блокировки SQLite подождёт до пяти секунд. Это спасает отdatabase is lockedпри коротких всплесках.
Проверьте, что рядом с базой появились файлы app.db-wal и app.db-shm, а права на каталог позволяют писать и приложению, и Litestream. Если приложение работает под пользователем myapp, а Litestream запущен от root — работать будет, но безопаснее завести отдельного пользователя и общую группу.
Отдельно про существующую базу: перед первым включением репликации сделайте разовую копию (.backup, не cp живого файла) — на случай, если что-то пойдёт не так:
sqlite3 /var/lib/myapp/app.db ".backup '/var/backups/app-$(date +%F).db'"
Шаг 2. Установка и настройка через systemd
Самый предсказуемый вариант для одного сервера — systemd-юнит.
Установка бинарника. Скачайте релиз под свою архитектуру и положите в /usr/local/bin:
cd /tmp
curl -sSL -o litestream.tar.gz \
https://github.com/benbjohnson/litestream/releases/download/v0.5.17/litestream-v0.5.17-linux-amd64.tar.gz
tar -xzf litestream.tar.gz
install -m 0755 litestream /usr/local/bin/litestream
litestream version
Конфигурация. Файл /etc/litestream.yml. Обратите внимание: в ветке 0.5 настройки снапшотов и уплотнения вынесены в глобальные блоки snapshot и levels, а не лежат внутри реплики, как в старых примерах.
dbs:
- path: /var/lib/myapp/app.db
replica:
url: s3://my-bucket/myapp/app.db
region: ru-1
endpoint: https://s3.example.ru
snapshot:
interval: 1h
retention: 168h
levels:
- interval: 30s
- interval: 5m
- interval: 1h
Для AWS S3 блок endpoint и region подставьте свои; для совместимых хранилищ (Selectel, Yandex Object Storage, MinIO, Backblaze) endpoint обязателен, а region часто принимает любое значение. Если провайдер требует path-style обращения, добавьте force-path-style: true.
Ключи — отдельно от конфига. Файл /etc/litestream.env, права 0600, владелец — пользователь сервиса:
sudo install -m 0600 -o litestream -g litestream /dev/null /etc/litestream.env
sudo tee /etc/litestream.env >/dev/null <<'EOF'
LITESTREAM_ACCESS_KEY_ID=REPLACE_ME
LITESTREAM_SECRET_ACCESS_KEY=REPLACE_ME
EOF
В самом YAML тогда не нужны поля с ключами: Litestream читает LITESTREAM_ACCESS_KEY_ID и LITESTREAM_SECRET_ACCESS_KEY из окружения. Это лучше, чем держать секреты в конфиге, который легко случайно закоммитить.
Юнит systemd. /etc/systemd/system/litestream.service:
[Unit]
Description=Litestream — репликация SQLite в S3
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=litestream
Group=litestream
EnvironmentFile=/etc/litestream.env
ExecStart=/usr/local/bin/litestream replicate -config /etc/litestream.yml
Restart=always
RestartSec=2s
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Запуск и проверка:
sudo systemctl daemon-reload
sudo systemctl enable --now litestream.service
sudo systemctl status litestream.service --no-pager
sudo journalctl -u litestream.service -n 30 --no-pager
В логах должно быть replicating to s3://... и первые compaction complete level=1. Через минуту проверьте бакет: там появятся каталоги с LTX-сегментами, а после первого снапшота — файл снапшота.
Права — самая частая причина «не стартует». Пользователь litestream должен читать app.db, app.db-wal, app.db-shm и каталог базы. Практично: usermod -aG myapp litestream плюс chmod 750 на каталог, либо общая группа с правом записи.
Шаг 3. Вариант в Docker: sidecar с общим томом
Если приложение уже в контейнере, Litestream удобно держать рядом — отдельным сервисом с тем же томом. Ключевая тонкость: путь к базе внутри контейнера Litestream должен совпадать с путём приложения, иначе он просто не найдёт файл.
services:
app:
image: myorg/myapp:latest
volumes:
- db:/var/lib/myapp
environment:
- DATABASE_URL=sqlite:////var/lib/myapp/app.db
restart: always
litestream:
image: litestream/litestream:0.5.17
command: replicate -config /etc/litestream.yml
volumes:
- db:/var/lib/myapp
- ./litestream.yml:/etc/litestream.yml:ro
environment:
- LITESTREAM_ACCESS_KEY_ID=${LITESTREAM_ACCESS_KEY_ID}
- LITESTREAM_SECRET_ACCESS_KEY=${LITESTREAM_SECRET_ACCESS_KEY}
restart: always
volumes:
db:
Два практических замечания. Во-первых, тег latest для бэкапного контейнера — плохая идея: зафиксируйте версию, иначе обновление образа может поменять поведение в самый неподходящий момент. Во-вторых, если том named, база живёт внутри Docker — при пересоздании контейнера она не теряется, но найти её руками сложнее; для отладки помогает docker volume inspect.
Есть и третий вариант — litestream replicate -exec "ваше приложение": Litestream запускает приложение как дочерний процесс и живёт вместе с ним. Меньше юнитов, но сложнее наблюдать два процесса по отдельности, поэтому на проде чаще берут два независимых сервиса.
Шаг 4. Частота снапшотов, ретенция и деньги на S3
Параметры по умолчанию разумны, но их стоит понимать, потому что они прямо влияют на время восстановления и на счёт за хранилище.
sync-interval(по умолчанию 1 секунда) — как часто отправляются изменения. Это ваш фактический RPO: упал сервер — потеряли примерно столько последних секунд.snapshot.interval(по умолчанию 24 часа) — как часто делается полный снапшот, с которого начинается восстановление.snapshot.retention(по умолчанию 24 часа) — сколько живут снапшоты и связанные с ними сегменты.levels— как быстро уплотняются LTX-файлы. Чем реже уровни, тем больше мелких файлов лежит в бакете и тем медленнее листинг, но тем точнее доступные точки восстановления.retention.enabled(появилось в 0.5.8) — можно выключить удаление файлов из бакета и отдать очистку lifecycle-политике провайдера. Полезно и с точки зрения безопасности: ключу Litestream тогда не нужны права на удаление объектов.
Рабочая отправная точка для небольшого сервиса: sync-interval: 1s, snapshot.interval: 1h, snapshot.retention: 168h (неделя). Это значит, что восстановление начинается с часового снапшота плюс до часа WAL — обычно минуты работы.
Про деньги. Объём в бакете примерно равен размеру базы плюс запас на сегменты между снапшотами. База 2 ГБ с интенсивной записью при часовых снапшотах и недельной ретенции легко занимает 10–20 ГБ: снапшоты полные, а не инкрементальные. Перед настройкой прикиньте: размер базы × число снапшотов в ретенции + суточный объём WAL. Если получается дорого, есть три ручки — реже снапшоты, короче ретенция, lifecycle-политика провайдера на старые версии.
Что не надо делать: ставить snapshot.interval: 5m «чтобы понадёжнее». Это умножит расход в разы, а RPO у вас всё равно определяется sync-interval, а не снапшотами.
Шаг 5. Восстановление: обычный restore
Порядок, который стоит один раз пройти руками до того, как он понадобится в аварии.
# 1. Остановите приложение: база не должна быть занята
sudo systemctl stop myapp
# 2. Восстановите базу из реплики в отдельный файл — не поверх рабочей
litestream restore -config /etc/litestream.yml \
-o /var/lib/myapp/app-restored.db \
s3://my-bucket/myapp/app.db
# 3. Проверьте целостность
sqlite3 /var/lib/myapp/app-restored.db "PRAGMA integrity_check;"
# ожидаемо: ok
# 4. Подмените файл и запустите приложение
sudo mv /var/lib/myapp/app.db /var/lib/myapp/app.db.broken
sudo mv /var/lib/myapp/app-restored.db /var/lib/myapp/app.db
sudo chown myapp:myapp /var/lib/myapp/app.db
sudo systemctl start myapp
Полезные флаги, которые экономят нервы:
-integrity-check full— Litestream сам прогонитPRAGMA integrity_checkпосле восстановления и вернёт ненулевой код, если база битая (с версии 0.5.10).-if-db-not-existsвместе с-if-replica-exists— идемпотентный restore: если файл уже есть, команда завершится успешно и ничего не перезапишет. Это удобно для автозапуска на новом сервере.-parallelism 8— сколько файлов качать одновременно; на большом канале заметно ускоряет.-dry-run— показать план восстановления без записи. Хорошо для проверки, что цепочка снапшот → сегменты целая.restore-if-db-not-exists: trueв конфиге (с 0.5.6) — Litestream сам восстановит базу при стартеreplicate, если локального файла нет. Это и есть «подняться на новом сервере одной командой».
Отдельно про подмену файла: не восстанавливайте поверх живой базы и не запускайте два Litestream над одним файлом. При подмене убедитесь, что старые app.db-wal и app.db-shm убраны — иначе SQLite попробует накатить чужой WAL на новую базу.
Шаг 6. PITR: откат на конкретный момент
Point-in-time recovery — это сценарий «данные испортили не сбоем диска, а действием»: миграция удалила колонку, скрипт прошёлся DELETE без WHERE, интеграция записала мусор. Ночной бэкап тут не помогает, а PITR — да.
# Состояние на 09:15 UTC — до того, как всё сломалось
litestream restore -config /etc/litestream.yml \
-timestamp "2026-11-03T09:15:00Z" \
-integrity-check full \
-o /var/lib/myapp/app-pitr.db \
s3://my-bucket/myapp/app.db
Дальше два пути. Первый: поднять приложение на восстановленном файле, если готовы откатить систему целиком на этот момент. Второй, более аккуратный: подключиться к app-pitr.db и вытащить нужные строки, перенеся их в рабочую базу точечно — так вы не теряете данные, появившиеся после инцидента.
Проверить, какие точки вообще доступны, можно без восстановления: litestream ltx -level all /var/lib/myapp/app.db покажет список файлов в реплике, а -txid позволит восстановиться на конкретную транзакцию вместо времени. Перед важным восстановлением всегда делайте -dry-run: он покажет, какой снапшот и какие сегменты будут использованы.
Граница возможностей: PITR работает только в пределах сохранённой истории. Если snapshot.retention равен суткам, то «откатиться на позавчера» не выйдет — данных уже нет. Поэтому срок ретенции выбирается не по размеру диска, а по вопросу «на сколько назад мне может понадобиться откат».
Что мониторить, чтобы не узнать о проблеме от клиента
Litestream тихо отваливается в трёх случаях: кончились права, протухли ключи, испортилась сеть. Во всех трёх приложение продолжает работать, а бэкап перестаёт обновляться. Поэтому наблюдать надо не «работает ли сервис», а «свежая ли копия».
Минимум:
- Возраст последней репликации. Простейший внешний чекер: раз в 15 минут смотреть на время последнего объекта в бакете по префиксу базы. Старше 10 минут — алерт. Это самая полезная проверка из всех.
- Статус юнита и логи.
systemctl is-active litestream, плюс grep поjournalctl -u litestreamнаerror,failed,permission denied. - Отставание по WAL. Если
app.db-walрастёт и не чекпоинтится, репликация не успевает; на диске это выглядит как необъяснимо тающее место. - Свободное место на диске. WAL может занимать сотни мегабайт при интенсивной записи и редких чекпоинтах.
- Размер бакета. Резкий рост — либо всплеск записи, либо ретенция не работает.
Практика, которая окупается: алерт на «бэкап не обновлялся N минут» должен идти в мессенджер или почту, а не только в панель сервера. Если сервер лежит, панель на нём же вам ничего не расскажет.
Безопасность ключей и бакета
- Отдельный ключ только для этого бакета и префикса. Не берите ключ с полными правами на аккаунт: утечка такого ключа — это не «потеряли бэкап», а «потеряли всё хранилище».
- Права по минимуму. Для записи достаточно
PutObjectиListBucketпо префиксу. Права наDeleteObjectнужны только если Litestream сам чистит старые файлы; приretention.enabled: falseих можно не выдавать. - Секреты — в
EnvironmentFileс правами 0600, владелец — служебный пользователь. Не в YAML, не в git, не вdocker-compose.yml. - Шифрование на стороне хранилища (SSE) включите, если провайдер это умеет; Litestream данные не шифрует.
- Время на сервере. Подпись запросов к S3 привязана к времени: при рассинхроне больше нескольких минут вы получите
RequestTimeTooSkewed. Держитеchrony/ntpdживым. - Проверьте, что бакет не публичный. Один раз включённый публичный доступ к бакету с базой клиентов — это утечка, которую нельзя откатить.
Типичные ошибки и что они значат
database is locked в приложении. Либо WAL не включён, либо busy_timeout мал, либо в базу пишет второй процесс (например, забытая копия воркера). Проверьте PRAGMA journal_mode; и список процессов.
Litestream не стартует: permission denied. Служебный пользователь не читает каталог базы. Лечится группой и правами, а не запуском от root.
no such file or directory при restore. Путь в -o и путь в реплике не совпадают, либо в бакете другой префикс. Сверьте url в конфиге и аргумент команды.
Репликация идёт, но restore падает на середине. Повреждена цепочка: удалён снапшот, на который опирались сегменты (например, вручную почистили бакет). Проверяйте -dry-run до аварии, а не во время.
RequestTimeTooSkewed или SignatureDoesNotMatch. Рассинхрон часов или неверный регион/endpoint. Для совместимых хранилищ чаще всего виноват endpoint.
Диск заполняется, хотя база маленькая. Не чекпоинтится WAL. Причина обычно — долгие читающие транзакции, которые держат старый снапшот WAL. Проверьте PRAGMA wal_checkpoint(TRUNCATE); и долгие запросы.
После восстановления приложение пишет ошибки. Забыли убрать старые -wal и -shm файлы от прежней базы, либо восстановили базу под другим владельцем. Проверьте права и наличие лишних файлов.
Два Litestream над одной базой. Классика при переезде: старый сервис не остановили, новый уже запустили. Будут дубли и гонки. Всегда останавливайте старый перед запуском нового.
Учения: как убедиться, что бэкап живой
Бэкап существует ровно с того момента, когда вы успешно из него восстановились. До этого это надежда.
Рабочий ритуал для небольшого сервиса:
- Раз в месяц — восстановить базу из бакета на отдельный VPS (или в контейнер на другом хосте), прогнать
PRAGMA integrity_check, выполнить три-четыре запроса, которые показывает приложение, и записать время восстановления. - Раз в квартал — сделать PITR на произвольный момент недельной давности и убедиться, что история за этот период цела.
- Каждый раз после смены ретенции или снапшот-интервала — повторить короткий restore: изменение настроек ломает цепочки чаще, чем кажется.
- Фиксировать результат в одном месте: дата, что восстанавливали, сколько заняло, что сломалось. Через полгода это единственный способ понять, деградировал ли процесс.
Если restore ни разу не делали — считайте, что бэкапа нет, как бы красиво ни выглядел бакет.
Чек-лист настройки с нуля
- Включить WAL,
synchronous,busy_timeout; сделать разовую.backup-копию. - Создать бакет и отдельный ключ с минимальными правами на префикс.
- Установить Litestream, положить конфиг и
EnvironmentFileс правами 0600. - Запустить юнит, дождаться первого снапшота и сегментов в бакете.
- Выполнить restore в отдельный файл, проверить
integrity_check, сравнить пару записей. - Настроить внешний чекер «возраст последней репликации» с алертом.
- Записать в документацию: где бакет, какой ключ, срок ретенции, как восстанавливать.
Какой VPS нужен под SQLite и Litestream
Требования скромные, но два из них не стоит недооценивать.
- Диск NVMe. Litestream читает WAL постоянно, SQLite делает чекпоинты. На медленном SATA-диске это превращается в заметный iowait при активной записи.
- Память. Самой базе хватает 1–2 ГБ, но page cache для SQLite — это ваш главный ускоритель чтения. Для сервиса с базой 1–3 ГБ практично 4 ГБ RAM.
- Сеть. Репликация упирается не в канал, а в стабильность: чем ровнее соединение до Object Storage, тем меньше отставание.
- Локальное место. Держите запас минимум в 3–4 размера базы: WAL, чекпоинты, VACUUM и временные файлы при восстановлении.
Отдельный совет про переезд: не пытайтесь «перенести базу вместе с сервером» через scp живого файла. Правильный путь — развернуть на новом VPS Litestream, сделать restore из бакета и только потом переключить трафик. Это и есть проверка бэкапа, только по делу и без учебного стенда.
Если выбираете сервер под такой сценарий — посмотрите наши тарифы (на момент 03.11.2026 конфигурации начинаются от 299 ₽ в месяц): для SQLite с Litestream важнее диск NVMe и стабильная сеть, чем количество ядер. Смежные материалы по теме: как делают бэкапы на VPS, первый вход по SSH, почему VPS тормозит, базовая безопасность сервера, как выбрать тариф и что делать, если VPS не работает. Если нужна другая база — у нас есть разбор кластера Redis на VPS и Sentry на своём сервере.
Частые вопросы
Litestream заменяет обычные бэкапы?
В большинстве случаев да, если речь о резервном копировании одной базы SQLite. Litestream даёт непрерывную репликацию и PITR, чего ночной sqlite3 .backup не умеет. Но проверку восстановления он за вас не делает — учения остаются на вас.
Нужен ли отдельный сервер под S3?
Нет, подойдёт любое S3-совместимое хранилище, включая Object Storage у вашего хостера. Важно, чтобы оно было в другом домене отказа, чем сервер с базой: бакет в том же дата-центре защищает от падения диска, но не от потери всей площадки.
Сколько данных теряется при падении сервера?
Столько, сколько прошло с последнего sync-interval. При значении по умолчанию 1 секунда это одна-две последние секунды записи. Если поставить 30 секунд, окно потери станет шире, а нагрузка на S3 — меньше.
Можно ли так бэкапить несколько баз на одном VPS?
Да, в конфиге перечисляются несколько dbs, у каждой своя реплика. Следите только за суммарным расходом хранилища и за тем, чтобы у каждой базы был свой префикс в бакете.
Работает ли Litestream, если приложение пишет через ORM?
Да, ему всё равно, кто пишет: он читает WAL на уровне файла. Ограничение одно — база должна быть в режиме WAL и писатель должен быть один.
Что будет, если интернет пропал на час?
Litestream продолжит работать локально, WAL будет расти, а в бакет ничего не уйдёт. Когда сеть вернётся, он догонит репликацию. Опасность в другом: если за это время заполнится диск, приложение встанет.
Нужно ли останавливать приложение для restore?
Для восстановления в отдельный файл — нет, база читается из бакета. Останавливать нужно в момент подмены рабочего файла: иначе приложение продолжит писать в старую базу и вы потеряете изменения.
Litestream подходит для продакшена с деньгами и платежами?
Для одного экземпляра с умеренной нагрузкой — да, при условии synchronous=FULL, короткого sync-interval и регулярных учений по восстановлению. Если платежей много и требуется отказоустойчивость без простоя, это уже история про PostgreSQL с репликацией, а не про SQLite.
Итог
SQLite + Litestream — это способ оставить простоту одного файла и при этом получить нормальный контур резервного копирования: непрерывная репликация в S3, восстановление на момент времени, отдельный ключ с минимальными правами и понятная процедура подъёма на новом сервере. Настройка занимает около получаса, а всё, что действительно требует внимания, — это три вещи: режим WAL, свежесть репликации (её надо проверять снаружи) и учения по восстановлению, которые проводятся до аварии, а не во время неё.