бекапирование sqlite в minio
services:
sqlite_backup:
image: git.realmanual.ru/pub/sqlite-backup-s3
container_name: sqlite_backup
restart: always
volumes:
- ./var/lib/data/:/data
environment:
- DB_FILE
- CRONTAB
- DELETE_AFTER
- KEEP_LAST
- TZ
- SQLITE_TIMEOUT
- BACKUP_RETRIES
- BACKUP_RETRY_DELAY
- MINIO_PATH
- MINIO_ACCOUNT_ID
- MINIO_APPLICATION_KEY
- MINIO_ENDPOINT
- MINIO_LOCATION
- HEALTHCHECK_URL
- HEALTHCHECK_UUID
пример .env
DB_FILE=/data/db.sqlite
CRONTAB=00 06 * * *
DELETE_AFTER=10
KEEP_LAST=10
TZ=Asia/Novosibirsk
SQLITE_TIMEOUT=60000
BACKUP_RETRIES=5
BACKUP_RETRY_DELAY=30
MINIO_PATH=myminio://sqlite/master/
MINIO_ACCOUNT_ID=account
MINIO_APPLICATION_KEY=key
MINIO_ENDPOINT=https://s3.domain.ru
MINIO_LOCATION=ru-nsk
# необязательно: пинг в healthchecks.io (в т.ч. self-hosted)
HEALTHCHECK_URL=https://hc.domain.ru/ping
HEALTHCHECK_UUID=00000000-0000-0000-0000-000000000000
- myminio - системно! не менять
- проверьте настройки локации, не должно быть пусто
- DELETE_AFTER - в днях
- KEEP_LAST - минимальное число копий, которые сохраняются всегда
Ротация
Удаление старых бекапов работает по двум условиям одновременно:
- бекап старше
DELETE_AFTERдней - и при этом не входит в
KEEP_LASTсамых свежих копий
То есть последние KEEP_LAST бекапов не удаляются никогда, даже если они старше DELETE_AFTER дней. Это защищает от потери всех копий, если контейнер долго стоял и новые бекапы не создавались.
KEEP_LAST по умолчанию 10, если переменная не задана.
Заблокированная база
Если базу пишет живой процесс (grafana, или не добитый старый под), sqlite3 .backup падает с Error: database is locked.
Бекап это переживает: соединение открывается с .timeout SQLITE_TIMEOUT (мс) — sqlite ждёт освобождения блокировки вместо мгновенной ошибки. Если за это время писатель не отпустил базу, попытка повторяется BACKUP_RETRIES раз с паузой BACKUP_RETRY_DELAY секунд.
| Переменная | Default | Что делает |
|---|---|---|
SQLITE_TIMEOUT |
60000 |
сколько мс ждать блокировку в рамках одной попытки |
BACKUP_RETRIES |
5 |
сколько попыток снять дамп |
BACKUP_RETRY_DELAY |
30 |
пауза между попытками, сек |
Максимальное время ожидания с дефолтами: 60s * 5 + 30s * 4 = 7 минут. После этого — exit 1 и пинг /fail.
Если база залочена постоянно — это не про бекап, а про то, что старый под не умер. Ретраи только прикрывают короткие окна.
Healthchecks
Если HEALTHCHECK_URL пуст — пинги не отправляются, бекап работает как обычно.
Итоговый адрес: ${HEALTHCHECK_URL}/${HEALTHCHECK_UUID}
| Момент | Запрос |
|---|---|
| старт бекапа | .../${HEALTHCHECK_UUID}/start |
дамп снят, проверен PRAGMA integrity_check, запакован, залит в s3, размер в s3 сверен с локальным, старое удалено |
.../${HEALTHCHECK_UUID} |
| любая ошибка на любом шаге | .../${HEALTHCHECK_UUID}/fail |
Для self-hosted инстанса HEALTHCHECK_URL — это адрес его ping-эндпоинта, например https://hc.domain.ru/ping.
Планировщик
crond из busybox не используется: под non-root uid (обычная ситуация в
kubernetes с securityContext.runAsUser) он не загружает ни одного crontab и
не сообщает об этом — в логе бесконечно идёт wakeup dt=60, а бекапов нет.
Вместо него entrypoint.sh содержит свой планировщик на sh.
Что это даёт:
- работает под любым uid;
- окружение наследуется напрямую, без выгрузки env в файл;
- бекапы никогда не накладываются друг на друга — следующий запуск возможен только после завершения предыдущего;
- некорректный
CRONTABроняет контейнер на старте с внятным сообщением ([scheduler] FATAL: minute: '60' is outside 0-59), а не приводит к тихому простою.
Синтаксис CRONTAB — стандартные пять полей: *, 5, 1,15, 2-6, */15,
8-18/2, 30/5. В месяцах и днях недели понимаются имена (jan, mon).
Воскресенье — и 0, и 7. Если ограничены одновременно день месяца и день
недели, запуск происходит при совпадении любого из них — как в crontab(5).
При старте контейнера бекап делается сразу, эта минута считается отработанной.
Расписание видно в логе при запуске:
[scheduler] schedule: 30 07 * * * (TZ=Asia/Novosibirsk, now 2026-09-02 16:45:21 +0700)
[scheduler] expanded: min=[30] hour=[7] dom=[1 2 3 ...] mon=[1 2 ...] dow=[0 1 ...] day-match=and
Ручной запуск: docker exec ... /scripts/backup.sh — использует то окружение,
с которым его вызвали.
Период опроса часов — 20 секунд, меняется через POLL_INTERVAL (секунды).