Журнал Tenku

SQLite + Litestream на VPS: бэкап в S3 и PITR

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 над одной базой. Классика при переезде: старый сервис не остановили, новый уже запустили. Будут дубли и гонки. Всегда останавливайте старый перед запуском нового.

Учения: как убедиться, что бэкап живой

Бэкап существует ровно с того момента, когда вы успешно из него восстановились. До этого это надежда.

Рабочий ритуал для небольшого сервиса:

  1. Раз в месяц — восстановить базу из бакета на отдельный VPS (или в контейнер на другом хосте), прогнать PRAGMA integrity_check, выполнить три-четыре запроса, которые показывает приложение, и записать время восстановления.
  2. Раз в квартал — сделать PITR на произвольный момент недельной давности и убедиться, что история за этот период цела.
  3. Каждый раз после смены ретенции или снапшот-интервала — повторить короткий restore: изменение настроек ломает цепочки чаще, чем кажется.
  4. Фиксировать результат в одном месте: дата, что восстанавливали, сколько заняло, что сломалось. Через полгода это единственный способ понять, деградировал ли процесс.

Если restore ни разу не делали — считайте, что бэкапа нет, как бы красиво ни выглядел бакет.

Чек-лист настройки с нуля

  1. Включить WAL, synchronous, busy_timeout; сделать разовую .backup-копию.
  2. Создать бакет и отдельный ключ с минимальными правами на префикс.
  3. Установить Litestream, положить конфиг и EnvironmentFile с правами 0600.
  4. Запустить юнит, дождаться первого снапшота и сегментов в бакете.
  5. Выполнить restore в отдельный файл, проверить integrity_check, сравнить пару записей.
  6. Настроить внешний чекер «возраст последней репликации» с алертом.
  7. Записать в документацию: где бакет, какой ключ, срок ретенции, как восстанавливать.

Какой 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, свежесть репликации (её надо проверять снаружи) и учения по восстановлению, которые проводятся до аварии, а не во время неё.

Нужен сервер под задачу?

Выберите тариф и локацию — VPS появится в кабинете.

Смотреть тарифы