1. Гайды
  2. Сервер с нуля

Бэкапы, которые восстанавливаются: pg_dump, restic и systemd

Дамп базы панели, шифрованный репозиторий в S3, расписание и, главное, проверка восстановления. Бэкап, который ни разу не восстанавливали, это надежда, а не бэкап.

Актуально для restic 0.17+, PostgreSQL 16–18 в Docker, systemdОбновлено 23.09.2026 6 мин чтения

Бэкапы делают все. Восстанавливают их обычно в три часа ночи, после того как хостер «потерял диск», и именно тогда выясняется, что дамп пустой, пароль от репозитория был только на умершем сервере, а 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 | head

pg_dump не сохраняет роли и табличные пространства (для них есть pg_dumpall). Для панели в Docker это обычно не проблема: роль создаёт сам образ из POSTGRES_USER.

restic: репозиторий в S3

restic шифрует и дедуплицирует, поэтому ежедневный дамп на сотни мегабайт не превращается в терабайты. Подойдёт любое S3-совместимое хранилище. Формат адреса для не-Amazon S3: s3:https://server:port/bucket_name.

Секреты в отдельный файл, доступный только root:

/etc/restic/envini
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-1
install -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/local/sbin/backup-panel.shbash
#!/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

/etc/systemd/system/backup-panel.serviceini
[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
/etc/systemd/system/backup-panel.timerini
[Unit]
Description=Daily panel backup

[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=15min
Persistent=true

[Install]
WantedBy=timers.target
  • Persistent=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: база.

Источники