Блог работает, посты публикуются, но бэкапов нет. Классическая история. Пока не потеряешь данные, резервное копирование кажется лишним, а потом уже поздно что-то настраивать. Решил не ждать аварии.

Что бэкапить

PostgreSQL база данных — посты, теги, пользователи. Крутится в Docker-контейнере blog_db на PostgreSQL 16. Код проекта — весь каталог /mnt/apps/blog на TrueNAS-сервере: backend, frontend, Docker-конфиги, .env.prod.

Бэкапы складываются на отдельный ZFS-пул /mnt/backup/MAIN_BACKUPS_sync/blog_backup/. Три уровня: daily, weekly, monthly. Скрипт сам определяет тип — 1-е число месяца значит monthly, воскресенье — weekly, всё остальное — daily.

Как устроен бэкап

Один bash-скрипт backup-blog.sh, 416 строк. Два режима: full (дамп БД + архив кода) и db (только база).

Дамп через pg_dump с --format=custom --compress=9. Выбрал custom format, а не plain SQL — та же база в plain SQL весит 216 KB, а в custom всего 70 KB, втрое меньше. Плюс custom позволяет восстанавливать отдельные таблицы. Таблицу django_session исключил — сессии бэкапить бессмысленно.

Код пакуется tar -czf целиком.

Верификация

Создать файл мало — надо убедиться, что он не битый. Дамп БД проходит четыре проверки: файл не пустой, размер больше 10 KB, команда file подтверждает формат "PostgreSQL custom database dump", создаётся SHA256 контрольная сумма.

Архив кода проверяется через file (gzip?) и tar -tzf (целостность). Не прошёл верификацию — файл удаляется, бэкап считается неудачным.

Ротация

Daily: 28 дампов БД и 14 архивов кода — покрытие на две недели. Weekly: по 8 штук, примерно два месяца. Monthly: по 6, полгода назад. Старые файлы удаляются автоматически.

Симлинки в latest/ указывают на свежие бэкапы — не нужно помнить дату последнего файла для восстановления.

Расписание

Два задания в cron. Полный бэкап ежедневно в 02:00 UTC, когда нагрузка минимальная. Второй дамп только БД — в 14:00 UTC, страховка на середину дня.

Результат

Первый полный бэкап: дамп базы (10 постов, 3 тега) — 70 KB, одна секунда. Архив кода — 54 MB, восемь секунд. Весь цикл от старта до завершения ротации — 11 секунд.

Уведомления приходят через indicatme — push-нотификация с режимом, размерами и временем. Восстановление проверил сразу: pipe дампа в контейнер через cat | docker exec -i, затем pg_restore --clean --if-exists — данные на месте.