Бэкапы, которые восстанавливаются: pg_dump, restic и systemd
Дамп базы панели, шифрованный репозиторий в S3, расписание и, главное, проверка восстановления. Бэкап, который ни разу не восстанавливали, это надежда, а не бэкап.
Бэкапы делают все. Восстанавливают их обычно в три часа ночи, после того как хостер «потерял диск», и именно тогда выясняется, что дамп пустой, пароль от репозитория был только на умершем сервере, а Redis после восстановления надо сносить. Этот гайд про то, как узнать всё это заранее.
Что бэкапить
Для панели и бота на PostgreSQL минимальный набор:
- Логический дамп базы (
pg_dump). Не копия каталогаpostgres_data. .envиdocker-compose.yml: без секретов из.envдамп не поднимется в новой инсталляции.- Конфиги реверс-прокси и сертификаты, если выпускаешь их не через Caddy.
Сообщество обжигалось именно на мелочах: не та роль в pg_dump, забытый .env при переезде, готовый скрипт восстановления не справился с Postgres 18. Подборка случаев в базе:
Бэкапы и restore и
Установка и обновление.
Заметка
Почему дамп, а не копия тома. Документация PostgreSQL прямо говорит: для файловой копии сервер должен быть остановлен, «полумеры» вроде запрета подключений не работают. Плюс формат файлов данных может меняться между мажорными версиями, а pg_dump как раз штатный способ переноса на новую версию. В образах Postgres 18+ ещё и сменился путь PGDATA. Дамп от всего этого не зависит.
Дамп из контейнера
pg_dump снимает согласованный снимок на работающей базе и не блокирует ни читателей, ни писателей. Формат -Fc (custom) сжат по умолчанию и читается pg_restore, умеющим восстанавливать выборочно и параллельно.
Имя пользователя не угадываем, а берём из окружения контейнера. Официальный образ postgres создаёт суперпользователя из POSTGRES_USER, а если POSTGRES_DB не задан, база называется так же, как пользователь:
docker exec remnawave-db sh -c \
'pg_dump -Fc -U "$POSTGRES_USER" -d "${POSTGRES_DB:-$POSTGRES_USER}"' \
> /var/backups/panel/panel.dumpОдинарные кавычки важны: переменные должны раскрыться внутри контейнера, а не на хосте. Это заодно лечит частую ошибку role "postgres" does not exist, когда у бота роль называется иначе.
Лайфхак
Имя контейнера смотри в docker ps. У панели это обычно remnawave-db, у бота Bedolaga remnawave_bot_db. У бота к тому же есть встроенные автобэкапы (BACKUP_AUTO_ENABLED и соседние переменные в .env), см.
базу. Встроенный бэкап хорошо, но он лежит на том же сервере.
Быстро убедиться, что дамп читается, не восстанавливая его. pg_restore -l выводит оглавление архива, а без имени файла читает стандартный ввод:
docker exec -i remnawave-db pg_restore -l < /var/backups/panel/panel.dump | headpg_dump не сохраняет роли и табличные пространства (для них есть pg_dumpall). Для панели в Docker это обычно не проблема: роль создаёт сам образ из POSTGRES_USER.
restic: репозиторий в S3
restic шифрует и дедуплицирует, поэтому ежедневный дамп на сотни мегабайт не превращается в терабайты. Подойдёт любое S3-совместимое хранилище. Формат адреса для не-Amazon S3: s3:https://server:port/bucket_name.
Секреты в отдельный файл, доступный только root:
RESTIC_REPOSITORY=s3:https://s3.example-provider.com/my-backups
RESTIC_PASSWORD_FILE=/etc/restic/password
AWS_ACCESS_KEY_ID=ключ
AWS_SECRET_ACCESS_KEY=секрет
# регион по умолчанию us-east-1; если провайдер требует свой:
# AWS_DEFAULT_REGION=ru-1install -d -m 700 /etc/restic
openssl rand -base64 32 > /etc/restic/password
chmod 600 /etc/restic/env /etc/restic/password
set -a; . /etc/restic/env; set +a
restic initНе делай так
Документация restic: «Losing your password means that your data is irrecoverably lost». Пароль от репозитория сохрани вне сервера: в менеджер паролей, вместе с ключами от S3. Если он жил только в /etc/restic/password на сервере, который умер, бэкапов у тебя нет.
restic ожидает path-style адреса (хост/бакет), а не бакет.хост. Если провайдер умеет только virtual-hosted стиль, есть опция -o s3.bucket-lookup=dns.
Скрипт бэкапа
#!/usr/bin/env bash
set -euo pipefail
DUMP_DIR=/var/backups/panel
install -d -m 700 "$DUMP_DIR"
# 1. Дамп во временный файл, чтобы упавший pg_dump не затёр прошлый
docker exec remnawave-db sh -c \
'pg_dump -Fc -U "$POSTGRES_USER" -d "${POSTGRES_DB:-$POSTGRES_USER}"' \
> "$DUMP_DIR/panel.dump.tmp"
mv "$DUMP_DIR/panel.dump.tmp" "$DUMP_DIR/panel.dump"
# 2. Дамп + конфиги в репозиторий
restic backup --tag panel "$DUMP_DIR" /opt/remnawave
# 3. Политика хранения и удаление лишних данных
restic forget --tag panel --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
# 4. Проверка структуры репозитория и 5% данных
restic check --read-data-subset=5%chmod 700 /usr/local/sbin/backup-panel.shЧто здесь происходит:
forgetудаляет снимки по политике,pruneудаляет данные, на которые больше никто не ссылается.--pruneделает оба шага за раз. Политика применяется отдельно к каждой группе снимков, по умолчанию группировка по хосту и путям, так что снимки другого сервера в том же репозитории не пострадают.- Во время
pruneрепозиторий заблокирован и параллельныйbackupне пройдёт. Если в один репозиторий пишут несколько серверов, разнеси их по времени. checkбез флагов проверяет только структуру.--read-data-subset=5%дополнительно скачивает и проверяет случайные 5% данных. Полный--read-dataчитает всё и стоит трафика.restic backupвозвращает код 3, если часть файлов не удалось прочитать. Сset -eскрипт упадёт, и таймер покажет ошибку: так и задумано.
Лайфхак
Начиная с restic 0.17 можно не держать дамп на диске: restic backup --stdin-filename panel.dump --stdin-from-command -- docker exec remnawave-db pg_dump .... restic проверяет код возврата команды и отменяет бэкап при ошибке, в отличие от pg_dump | restic backup --stdin, который провал не заметит. Но локальная копия дампа тоже полезна: это вторая копия из правила 3-2-1.
Если /opt/remnawave у тебя содержит bind-mount с файлами базы, исключи его через --exclude: живые файлы Postgres в бэкапе бесполезны.
Расписание: systemd timer
[Unit]
Description=Backup panel DB and configs to restic
Wants=network-online.target
After=network-online.target docker.service
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
ExecStart=/usr/local/sbin/backup-panel.sh[Unit]
Description=Daily panel backup
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=15min
Persistent=true
[Install]
WantedBy=timers.targetPersistent=trueзапоминает время последнего запуска на диске. Если сервер был выключен в момент срабатывания, сервис запустится сразу после включения таймера. Работает только сOnCalendar=.RandomizedDelaySec=добавляет случайную задержку, чтобы десяток серверов не ломился в S3 одновременно.- Таймер по умолчанию запускает сервис с тем же именем, поэтому
Unit=не нужен.
systemctl daemon-reload
systemctl enable --now backup-panel.timer
systemctl start backup-panel.service # первый прогон руками
journalctl -u backup-panel.service -n 50
systemctl list-timers backup-panel.timerКак вообще устроены сервисы и таймеры, будет в гайде systemd-сервисы.
Правило 3-2-1
Три копии данных, на двух разных носителях, одна из них в другом месте. На практике для панели: рабочая база, дамп на локальном диске, репозиторий restic у другого провайдера. S3 в том же дата-центре, что и сервер, это не «другое место».
Проверка восстановления
Раз в месяц, а также после каждого обновления панели или Postgres. Сначала список снимков и восстановление в отдельную папку:
set -a; . /etc/restic/env; set +a
restic snapshots --tag panel
restic restore latest --tag panel --target /tmp/restore-test
ls -la /tmp/restore-test/var/backups/panel/ /tmp/restore-test/opt/remnawave/Потом дамп в тестовую базу рядом с рабочей. Пустую базу создаём из template0, как советует документация pg_restore:
docker exec remnawave-db sh -c 'createdb -U "$POSTGRES_USER" -T template0 restore_test'
docker exec -i remnawave-db sh -c \
'pg_restore -U "$POSTGRES_USER" -d restore_test --no-owner --exit-on-error' \
< /tmp/restore-test/var/backups/panel/panel.dump
docker exec remnawave-db sh -c 'psql -U "$POSTGRES_USER" -d restore_test -c "\dt"'Таблицы на месте, количество строк в главных таблицах похоже на правду: бэкап живой. Убираем за собой:
docker exec remnawave-db sh -c 'dropdb -U "$POSTGRES_USER" restore_test'
rm -rf /tmp/restore-testГрабли
Боевое восстановление поверх рабочей базы делается с --clean --if-exists (--if-exists без --clean не работает) и при остановленном приложении. pg_restore -j для ускорения работает только с файлом, не со стандартным вводом, и несовместим с --single-transaction.
Важно
pg_dump новее сервера умеет снимать дампы со старых версий, а вот загрузка дампа в более старую мажорную версию Postgres не гарантируется. Восстанавливай на ту же или более новую версию. Если панель после restore не стартует, в сообществе помогало снести Redis:
база.
Источники
- PostgreSQL: pg_dump
- PostgreSQL: pg_restore
- PostgreSQL: File System Level Backup
- PostgreSQL: Upgrading a PostgreSQL Cluster
- Docker Official Image: postgres
- restic: Preparing a new repository
- restic: Backing up
- restic: Working with repositories
- restic: Restoring from backup
- restic: Removing backup snapshots
- restic: Scripting
- restic CHANGELOG
- systemd.timer
- systemd.service, systemd.exec