Litestream, cron-скрипт или dbsend: как бэкапить SQLite

Если приложение живёт на SQLite, рано или поздно встаёт вопрос: чем бэкапить файл базы. Обычно рассматривают три варианта: Litestream с непрерывной репликацией в S3, простой cron-скрипт с .backup и сервис версионированных бэкапов вроде dbsend. Ниже — как устроен каждый, что он реально даёт, где его пределы и как выбрать подход под свою задачу.

Два ключевых вопроса: RPO и что вы восстанавливаете

Прежде чем сравнивать инструменты, ответьте на два вопроса.

Сколько данных вы готовы потерять (RPO, recovery point objective). Если сервер умер в 14:37, а последний бэкап был в 03:00, вы теряете всё, что записано за день. Для интернет-магазина это заказы, для внутреннего инструмента — возможно, ничего страшного.

Что именно вы хотите уметь восстановить. Одно дело — поднять базу после смерти диска. Другое — понять, что вчерашняя миграция удалила половину строк в таблице, и найти версию «до». Первое — задача репликации, второе — задача истории версий. Инструменты закрывают эти задачи по-разному.

Litestream: непрерывная репликация WAL в S3

Litestream — open-source утилита, которая работает рядом с приложением отдельным процессом (sidecar) и непрерывно копирует изменения SQLite в S3-совместимое хранилище. Она читает WAL-файл базы и отправляет новые страницы в хранилище с задержкой порядка секунд, а периодически дополнительно сохраняет полный снимок базы (snapshot).

Что это даёт:

  • RPO в секунды. Если сервер пропал, теряются только изменения за последние несколько секунд.
  • Восстановление на момент времени. Из снимка и последующих WAL-сегментов Litestream собирает базу на нужный момент в пределах окна хранения (retention, задаётся в конфиге).
  • Не нужно останавливать приложение и не нужен отдельный сервер базы.

Конфигурация

Конфиг по умолчанию — /etc/litestream.yml. Пример для Yandex Object Storage:

dbs:
  - path: /var/lib/myapp/app.db
    replicas:
      - url: s3://my-backups/myapp
        endpoint: https://storage.yandexcloud.net
        region: ru-central1

Для Selectel указывайте endpoint: https://s3.ru-1.storage.selcloud.ru и region: ru-1; для других провайдеров эндпоинт смотрите в панели провайдера. Ключи доступа удобнее передавать через переменные окружения AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY, а не хранить в YAML.

Запуск:

# реплицировать все базы из /etc/litestream.yml
litestream replicate

# или запустить приложение как дочерний процесс Litestream
litestream replicate -exec "/usr/local/bin/myapp"

На сервере Litestream обычно запускают как systemd-сервис (при установке из deb-пакета юнит litestream уже есть):

sudo systemctl enable --now litestream
journalctl -u litestream -f

Восстановление

# восстановить базу из реплики, указанной в конфиге, в новый файл
litestream restore -o /tmp/app-restored.db /var/lib/myapp/app.db

# или прямо по URL реплики
litestream restore -o /tmp/app-restored.db s3://my-backups/myapp

Файл назначения не должен существовать — Litestream не перезаписывает рабочую базу. Дальше вы останавливаете приложение, проверяете восстановленный файл (PRAGMA integrity_check;) и подменяете им рабочий.

Ограничения Litestream

У Litestream честная и узкая задача — непрерывная копия, и из неё вытекают ограничения:

  • Нужен режим WAL. Приложение должно открывать базу с PRAGMA journal_mode=WAL; и разумным busy_timeout.
  • Checkpoint контролирует Litestream. Приложение не должно само агрессивно делать checkpoint (например, регулярный PRAGMA wal_checkpoint(TRUNCATE)) и тем более удалять -wal-файл: изменения, которые Litestream не успел прочитать, пропадут из реплики.
  • Отдельный процесс, который тоже надо мониторить. Если сервис Litestream упал или потерял доступ к S3, приложение продолжит работать, а реплика тихо отстанет. Следить за этим — ваша задача.
  • Результат восстановления — собранный файл базы, а не каталог именованных версий. Вы выбираете момент времени, но не видите «v41 — перед миграцией» и не можете сравнить содержимое двух точек без того, чтобы восстановить обе и сравнивать вручную.
  • Ошибки реплицируются тоже. Если DELETE без WHERE выполнился в 14:00, реплика честно получит его в 14:00. Спасает только восстановление на момент до этого, и то в пределах окна хранения.

У Litestream есть активная разработка и крупные изменения между версиями, поэтому синтаксис флагов и конфига сверяйте с документацией той версии, которую ставите.

Cron-скрипт: .backup или VACUUM INTO по расписанию

Самый простой и прозрачный вариант. Раз в N часов скрипт делает консистентную копию живой базы, сжимает её и отправляет в S3. Подробно про сами команды и почему нельзя просто cp при WAL — в статье как сделать бэкап SQLite без повреждения.

#!/usr/bin/env bash
# /usr/local/bin/backup-sqlite.sh
set -euo pipefail

DB=/var/lib/myapp/app.db
DIR=/var/backups/myapp
TS=$(date +%Y%m%d-%H%M)
OUT="$DIR/app-$TS.db"

mkdir -p "$DIR"
sqlite3 "$DB" ".backup '$OUT'"
sqlite3 "$OUT" "PRAGMA quick_check;" | grep -qx ok
gzip "$OUT"

aws s3 cp "$OUT.gz" "s3://my-backups/myapp/app-$TS.db.gz" \
  --endpoint-url https://storage.yandexcloud.net

# локальная ротация: держим 14 дней
find "$DIR" -name 'app-*.db.gz' -mtime +14 -delete

Вместо .backup можно использовать VACUUM INTO '/path/out.db' (SQLite 3.27+, файл назначения не должен существовать) — он заодно дефрагментирует копию.

Расписание через systemd timer:

# /etc/systemd/system/backup-sqlite.service
[Unit]
Description=SQLite backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-sqlite.sh
# /etc/systemd/system/backup-sqlite.timer
[Unit]
Description=SQLite backup every 6 hours

[Timer]
OnCalendar=*-*-* 00/6:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup-sqlite.timer

Или одной строкой в crontab: 0 */6 * * * /usr/local/bin/backup-sqlite.sh.

Ротацию в бакете удобно отдать правилу жизненного цикла (lifecycle) хранилища: удалять объекты с префиксом myapp/ старше N дней. Его настраивают в панели провайдера или через aws s3api put-bucket-lifecycle-configuration.

Что нужно понимать про cron-подход:

  • RPO равен интервалу. Бэкап раз в 6 часов — до 6 часов потерянных данных.
  • Всё остальное вы строите сами: ротацию, проверку целостности, уведомления о том, что скрипт перестал работать. Самая частая авария — скрипт месяц назад сломался из-за сменившегося пароля к S3, и никто не заметил.
  • История — это просто список файлов с датами в именах. Чтобы понять, что изменилось между двумя бэкапами, их надо скачать и сравнить вручную.

Зато такой скрипт понятен целиком, не требует отдельного демона и не зависит от режима журнала.

dbsend: версии с историей, сравнением и алертами

dbsend — сервис версионированных бэкапов, который мы делаем. Он не реплицирует WAL непрерывно и не заменяет Litestream: это периодические бэкапы, как у cron-скрипта, но с инфраструктурой вокруг них.

  • Каждая загрузка становится версией vN базы. Версии можно подписывать (-l) и закреплять — закреплённые не удаляются ротацией.
  • Для SQLite dbsend извлекает схему, таблицы и число строк. Можно сравнить любые две версии: какие таблицы и колонки появились или исчезли, как изменилось число строк и размер. Сравнение доступно на платных тарифах.
  • При загрузке файл проверяется PRAGMA quick_check, у каждой версии есть SHA-256.
  • Если бэкап не пришёл по расписанию, приходит алерт в Telegram, на email или в webhook (платные тарифы).
  • Хранение в S3 в России, шифрование AES-256-GCM.
  • Есть MCP-сервер: AI-агент в Claude Code или Cursor может посмотреть историю и сравнить версии — подробнее в статье бэкапы через MCP и в документации MCP.

Чего dbsend не делает: не даёт RPO в секунды, не стримит WAL и не восстанавливает базу «на 14:36:12». Точность восстановления равна частоте ваших загрузок.

Сравнение

| | Litestream | Cron-скрипт | dbsend | |---|---|---|---| | Модель | Непрерывная репликация WAL | Периодическая копия | Периодические версии | | RPO | Секунды | Интервал расписания | Интервал расписания | | Что запущено на сервере | Постоянный процесс | Скрипт по таймеру | CLI по таймеру | | Требования к базе | Режим WAL, без ручных checkpoint | Нет | Нет | | Восстановление | Сборка файла на момент времени | Скачать нужный файл | dbsend restore с проверкой sha256 и integrity_check | | История | Окно retention, без имён | Файлы с датами | Версии с метками и закреплением | | Сравнение двух точек | Вручную | Вручную | Схема и число строк (платные тарифы) | | Уведомление, если бэкап не идёт | Настраиваете сами | Настраиваете сами | Встроено (платные тарифы) | | Хранилище | Ваш S3 | Ваш S3 или диск | S3 dbsend в России | | Стоимость | Бесплатно + оплата S3 | Бесплатно + оплата S3 | Тарифы на /pricing |

Когда что выбрать

Litestream, если потеря даже часа записей недопустима: заказы, платежи, пользовательский контент, который нельзя попросить ввести заново. Вы готовы держать отдельный процесс, следить за ним и соблюдать правила WAL.

Cron-скрипт, если данные меняются редко или их потеря за несколько часов терпима, а вы хотите минимум движущихся частей. Хорошо подходит для внутренних инструментов, ботов, небольших сайтов. Обязательно добавьте хотя бы простой мониторинг и регулярно проверяйте, что бэкап рабочий.

dbsend, если вам нужна понятная история: «что было в базе до релиза», «когда пропали строки из orders», закреплённая версия перед миграцией, уведомление, если бэкапы перестали приходить. И если данные должны храниться в России.

Можно совмещать. Это частый и разумный вариант: Litestream закрывает RPO в секунды на случай отказа сервера, а раз в сутки (и перед каждой миграцией) вы загружаете версию в dbsend. Для катастрофы восстанавливаетесь из Litestream, для «кто и когда сломал данные» — смотрите diff между версиями и при необходимости берёте файл оттуда. Litestream при этом не мешает: dbsend backup читает базу как обычный клиент.

Как это сделать в dbsend

npm install --global @dbsend/sdk
dbsend login --key sqv_pk_…

# CLI сам делает консистентную копию через VACUUM INTO и загружает её
dbsend backup /var/lib/myapp/app.db -d "$DBSEND_DATABASE_ID" -e sqlite -l "daily"

# перед миграцией — подписанная версия, которую стоит закрепить
dbsend backup /var/lib/myapp/app.db -d "$DBSEND_DATABASE_ID" -e sqlite -l "before-migration-42"
dbsend log
dbsend pin <snapshotId>

# что изменилось между двумя версиями
dbsend diff <fromSnapshotId> <toSnapshotId>

# восстановление: остановите приложение, затем
dbsend restore <snapshotId> /var/lib/myapp/app.db --yes

Если файл не изменился с прошлой загрузки, новая версия не создаётся. dbsend restore скачивает файл во временный, проверяет sha256 и PRAGMA integrity_check, убирает устаревшие -wal/-shm рядом с целевым файлом и атомарно подменяет его. Все команды — в документации CLI, обзор возможностей для SQLite — на странице бэкапов SQLite. На бесплатном тарифе доступна одна база, 3 версии и хранение 7 дней. Общий порядок восстановления разных баз описан в статье как восстановить базу из бэкапа.

Если вы используете PocketBase или пишете Telegram-бота на SQLite, посмотрите отдельные инструкции: бэкап PocketBase и бэкап базы Telegram-бота.

Частые вопросы

Litestream — это бэкап или репликация?

Репликация в объектное хранилище с возможностью восстановления на момент времени. Она защищает от потери сервера и диска, но ошибочное удаление данных тоже реплицируется. Поэтому для защиты от логических ошибок нужны либо достаточно длинное окно retention, либо отдельные периодические копии.

Можно ли запустить Litestream и cron-бэкап одновременно?

Да. .backup, VACUUM INTO и dbsend backup читают базу как обычный клиент и не трогают WAL-файл. Не делайте только ручной wal_checkpoint(TRUNCATE) в своём скрипте — checkpoint оставьте Litestream.

Подходит ли Litestream для нескольких серверов, пишущих в одну базу?

Нет. Litestream копирует одну базу с одного сервера, у неё один писатель. Это не кластер и не автоматический failover: при аварии вы вручную восстанавливаете базу на новом сервере.

Нужен ли режим WAL для cron-скрипта или dbsend?

Нет. .backup и VACUUM INTO работают в любом режиме журнала. WAL полезен для конкурентного чтения и записи, но для периодических бэкапов не обязателен.

Как часто делать периодический бэкап?

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