68edc524e3
- traefik-architect: Traefik v3 reverse proxy patterns - docker-compose-architect: compose.yaml conventions and templates - gitea-actions-cd: workflow_dispatch CD pattern, Linux+Windows targets - web-security-hardening: OWASP Top 10, CSP, CrowdSec, sops+age - backup-restore: restic + WAL-G, GFS retention, tested restore - observability: Prometheus + Loki + Grafana + Alertmanager README: regenerated skill table and added 'Web stack skills bundle' section showing recommended composition order.
13 KiB
13 KiB
name, version, description, command
| name | version | description | command |
|---|---|---|---|
| backup-restore | 0.1.0 | Backup & restore strategy for self-hosted Docker stacks. restic to S3-compatible storage, Postgres logical+WAL, named-volume snapshots, retention policies (GFS), encryption at rest, tested restoration playbooks, automated verification, Telegram alerts on failure. | /backup |
Backup & Restore
Ты — инженер по сохранности данных. У пользователя — самостоятельная инфраструктура, единственная страховка — собственный backup. Правило: бэкап, который не восстанавливали, не существует.
Жёсткие инварианты
- 3-2-1: 3 копии, 2 разных носителя, 1 off-site.
- Encrypted at rest: AES-256 для всех бэкапов, ключ НЕ на том же хосте, что и данные.
- Tested restore: ежемесячно — автоматический test restore в стейдж-окружение. Без теста бэкап = плацебо.
- Retention (GFS): 7 daily, 4 weekly, 12 monthly, 5 yearly.
- БД: логические дампы + WAL/binlog. Никогда только snapshot volume для прод-БД.
- Не сохраняем секреты в бэкапе вместе с шифрующим ключом — иначе компрометация одного = доступа ко всему.
- Notifications: success — silent (в дашборд), failure — немедленно в Telegram.
- Документация: для каждого бэкапа — playbook восстановления в
creator/obsidian-vault.
Backup-стек: restic
# /opt/restic-backup/compose.yaml
services:
restic-backup:
image: restic/restic:0.17.3
container_name: restic-backup
restart: "no" # запускается по cron
network_mode: host
environment:
- RESTIC_REPOSITORY=${RESTIC_REPOSITORY} # s3:https://s3.endpoint/bucket
- RESTIC_PASSWORD_FILE=/run/secrets/restic_password
- AWS_ACCESS_KEY_ID_FILE=/run/secrets/s3_access
- AWS_SECRET_ACCESS_KEY_FILE=/run/secrets/s3_secret
- TZ=Europe/Moscow
volumes:
- /opt:/source/opt:ro # все стеки
- /var/lib/docker/volumes:/source/volumes:ro
- ./logs:/logs
- ./scripts:/scripts:ro
secrets:
- restic_password
- s3_access
- s3_secret
entrypoint: ["/scripts/run.sh"]
secrets:
restic_password:
file: ./secrets/restic_password.txt
s3_access:
file: ./secrets/s3_access.txt
s3_secret:
file: ./secrets/s3_secret.txt
scripts/run.sh:
#!/bin/sh
set -eu
export RESTIC_PASSWORD=$(cat "$RESTIC_PASSWORD_FILE")
export AWS_ACCESS_KEY_ID=$(cat "$AWS_ACCESS_KEY_ID_FILE")
export AWS_SECRET_ACCESS_KEY=$(cat "$AWS_SECRET_ACCESS_KEY_FILE")
# init repo if missing
restic snapshots >/dev/null 2>&1 || restic init
# backup
restic backup /source \
--tag scheduled \
--exclude='**/node_modules' \
--exclude='**/.git/objects/pack' \
--exclude='**/__pycache__' \
--exclude='/source/volumes/*/restic-*' \
--verbose
# retention
restic forget \
--tag scheduled \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 12 \
--keep-yearly 5 \
--prune
# integrity check (раз в неделю — полный, иначе fast)
if [ "$(date +%u)" = "7" ]; then
restic check --read-data-subset=5%
else
restic check
fi
Cron на хосте:
# /etc/cron.d/restic-backup
0 3 * * * root cd /opt/restic-backup && docker compose run --rm restic-backup >> /opt/restic-backup/logs/cron.log 2>&1 || /opt/restic-backup/notify-failure.sh
notify-failure.sh:
#!/bin/bash
curl -fsS -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TG_CHAT_ID}" \
-d "parse_mode=Markdown" \
-d "text=❌ *Restic backup failed* on \`$(hostname)\` at \`$(date -Iseconds)\`. Check logs."
Postgres backup — pg_dump + WAL-G
pg_dump достаточен для приложений с RPO ≥ 24h. Для критичных — WAL-G с PITR.
Логический дамп через sidecar
# В compose.yaml сервиса с Postgres:
services:
postgres:
image: postgres:16.5-alpine
# ...
pg-backup:
image: postgres:16.5-alpine
container_name: pg-backup
restart: "no"
depends_on:
postgres:
condition: service_healthy
environment:
- PGHOST=postgres
- PGUSER=postgres
- PGPASSWORD_FILE=/run/secrets/db_password
- BACKUP_DIR=/backups
volumes:
- ./backups/pg:/backups
- ./scripts/pg-dump.sh:/pg-dump.sh:ro
secrets:
- db_password
networks:
- internal
entrypoint: ["/pg-dump.sh"]
scripts/pg-dump.sh:
#!/bin/sh
set -eu
export PGPASSWORD=$(cat "$PGPASSWORD_FILE")
TS=$(date +%Y%m%d_%H%M%S)
DBS=$(psql -h "$PGHOST" -U "$PGUSER" -d postgres -tAc \
"SELECT datname FROM pg_database WHERE datistemplate=false AND datname NOT IN ('postgres');")
for db in $DBS; do
pg_dump -h "$PGHOST" -U "$PGUSER" -d "$db" -Fc -Z 9 \
-f "$BACKUP_DIR/${db}_${TS}.dump"
done
# retention локально — 7 дней (restic подберёт)
find "$BACKUP_DIR" -name '*.dump' -mtime +7 -delete
WAL-G для PITR (для критичной БД)
services:
postgres:
image: postgres:16.5-alpine
command:
- postgres
- -c
- wal_level=replica
- -c
- archive_mode=on
- -c
- archive_command=/usr/local/bin/wal-g wal-push %p
- -c
- archive_timeout=300
environment:
- WALG_S3_PREFIX=s3://backups/pg-wal
- AWS_ACCESS_KEY_ID_FILE=/run/secrets/s3_access
# ...
volumes:
- ./scripts/wal-g:/usr/local/bin/wal-g:ro
- postgres_data:/var/lib/postgresql/data
Ежедневный base backup:
docker exec postgres wal-g backup-push /var/lib/postgresql/data
Restore playbook: Docker stack из restic
# 1. На целевом хосте поднять restic
export RESTIC_REPOSITORY=s3:https://s3.endpoint/bucket
export RESTIC_PASSWORD=$(cat /secure/restic_password.txt)
# 2. Найти нужный snapshot
restic snapshots --tag scheduled
# восстановить последний:
SNAP=$(restic snapshots --json --tag scheduled | jq -r '.[-1].id')
# 3. Восстановить определённый стек
restic restore "$SNAP" --target /tmp/restore --include /source/opt/myapp
# 4. Перенести на место
sudo systemctl stop docker # если на тот же хост
sudo rsync -aHAX /tmp/restore/source/opt/myapp/ /opt/myapp/
sudo systemctl start docker
# 5. Поднять стек
cd /opt/myapp && docker compose up -d
# 6. Verify
docker compose ps
curl -fsS https://myapp.abelentsev.pro/health
Restore playbook: Postgres
# Из логического дампа (полное восстановление БД)
docker compose exec postgres dropdb -U postgres mydb
docker compose exec postgres createdb -U postgres mydb
docker compose exec -T postgres pg_restore -U postgres -d mydb < /opt/myapp/backups/pg/mydb_20260513_030000.dump
# PITR через WAL-G (к моменту t)
docker compose down
docker volume rm myapp_postgres_data
docker volume create myapp_postgres_data
docker run --rm -v myapp_postgres_data:/var/lib/postgresql/data \
-e WALG_S3_PREFIX=s3://backups/pg-wal \
-e AWS_ACCESS_KEY_ID=... \
postgres-with-walg \
wal-g backup-fetch /var/lib/postgresql/data LATEST
# recovery.conf указать recovery_target_time='2026-05-13 02:55:00 MSK'
docker compose up -d
Verification — автоматический test restore
#!/bin/bash
# /opt/restic-backup/verify-restore.sh — раз в месяц cron
set -euo pipefail
WORK=/tmp/verify-$(date +%s)
mkdir -p "$WORK"
# Восстановить последний снапшот целиком
restic restore latest --target "$WORK"
# Проверить, что критичные файлы есть и непустые
test -s "$WORK/source/opt/postgres-app/backups/pg/mydb_"*.dump
test -s "$WORK/source/opt/traefik/acme/acme.json"
# Попробовать pg_restore в тест-БД
docker run --rm -d --name verify-pg -e POSTGRES_PASSWORD=t postgres:16.5-alpine
sleep 10
LATEST_DUMP=$(ls -t "$WORK/source/opt/postgres-app/backups/pg/"*.dump | head -1)
docker cp "$LATEST_DUMP" verify-pg:/tmp/restore.dump
docker exec verify-pg pg_restore -U postgres -d postgres /tmp/restore.dump
docker stop verify-pg
rm -rf "$WORK"
echo "Verify OK $(date -Iseconds)"
Cron:
0 4 1 * * root /opt/restic-backup/verify-restore.sh >> /opt/restic-backup/logs/verify.log 2>&1
S3-compatible storage опции
Для off-site копии — любой S3-совместимый bucket с серверной стороны шифрованием:
- Backblaze B2 — $6/TB/мес, безлимитные API-операции
- Wasabi — $7/TB/мес, лимит на egress
- Storj DCS — децентрализованный, дешёвый
- MinIO свой второй хост — для on-prem second copy перед off-site
Дублирование: первичный — MinIO на втором локальном хосте (192.168.7.179), вторичный — Backblaze B2 (restic copy).
Антипаттерны
- Бэкап на тот же диск/хост, где данные. Disk failure уносит обоих.
- Только snapshot Docker volume для prod БД — высокая вероятность corrupt на restore.
- Backup без шифрования в публичном S3.
- Ключ шифрования рядом с бэкапом (например, в
creator/obsidian-vault, который сам бэкапится). - Никакой retention → bucket растёт бесконечно → счёт растёт.
- Никогда не тестировал восстановление — Schrödinger's backup.
--excludeне учитывает динамические каталоги (/tmp,node_modules) → раздутый бэкап.- Хранить дампы БД на named volume рядом с самой БД — оба унесёт.
- Бэкап Traefik
acme.jsonбез бэкапа — после restore новые серты на 7 дней rate-limit Let's Encrypt.
Чек-лист для нового стека
- Volume(ы) с данными зафиксированы в backup-source list
- Если есть БД —
pg_dump/mysqldumpsidecar добавлен - Если БД критичная — WAL-G / binlog настроены
- Secrets (acme.json, age-keys) бэкапятся отдельным защищённым путём
- Restore playbook написан в
creator/obsidian-vault/claude/memory/backup/restore-<service>.md - Restore хотя бы один раз вручную выполнен и задокументирован
- Cron-задача добавлена
- Telegram-нотификация настроена
Команды
# Список снапшотов
restic snapshots
# Размер репозитория
restic stats --mode raw-data
# Что внутри снапшота
restic ls latest /source/opt/myapp
# Восстановить один файл
restic restore latest --target /tmp --include /source/opt/myapp/config.yaml
# Diff между двумя снапшотами
restic diff snap_id_a snap_id_b
# Сменить пароль (новый писать сразу в Vault!)
restic key add # добавить новый ключ
restic key remove ID # удалить старый
# Скопировать в second repo
RESTIC_REPOSITORY=s3:primary RESTIC_REPOSITORY2=s3:backblaze restic copy
# Очистить и пересжать (раз в полгода для эффективности)
restic prune
Интеграция с инфрой пользователя
- Primary repo: MinIO на
192.168.7.179(тот же хост, что SonarQube). - Off-site: Backblaze B2 bucket, отдельный аккаунт.
- Шифрующий ключ: длинная фраза в 1Password vault
Infrastructure. Дубликат в bank safe deposit box. - Logs:
/opt/restic-backup/logs/→ Promtail → Loki → Grafana dashboardBackups. - Alerts: Telegram через
homework/TGServerServiceбот. - Восстановление документации: каждый сервис должен иметь playbook в
creator/obsidian-vault/claude/memory/backup/. - n8n flow: ежемесячная Telegram-сводка «Last successful backups by stack» с
restic snapshots --json→ форматирование.