feat(deploy): авто-бэкап БД+uploads — systemd timer/service + скрипт (P0-6)
food-market-backup.sh: pg_dump -Fc контейнера + tar uploads, ротация 30 дней,
атомарная запись через .tmp+mv. food-market-backup.{service,timer} — ежедневно
03:00 с догоном пропущенных. docs/backup-restore.md — установка таймера, ручной
бэкап, восстановление БД (drop+create / --clean) и uploads, проверка дампа.
Скрипт проверен против food-market-postgres: дамп PGDMP custom-format,
248 TOC, pg_restore --list читает. Установку на prod-vm не делаем — только артефакты.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
744847661d
commit
7c34bb1abd
15
deploy/food-market-backup.service
Normal file
15
deploy/food-market-backup.service
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[Unit]
|
||||||
|
Description=food-market: бэкап БД и загруженных файлов
|
||||||
|
Documentation=https://github.com/nurdotnet/food-market/blob/main/docs/backup-restore.md
|
||||||
|
Wants=docker.service
|
||||||
|
After=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
# Опциональные переопределения FM_* (см. шапку скрипта). Знак "-" — файл не
|
||||||
|
# обязателен. Путь скорректировать под фактический каталог деплоя.
|
||||||
|
EnvironmentFile=-/opt/food-market/deploy/.env
|
||||||
|
ExecStart=/opt/food-market/deploy/food-market-backup.sh
|
||||||
|
# Бэкап не должен мешать основной нагрузке.
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=idle
|
||||||
67
deploy/food-market-backup.sh
Executable file
67
deploy/food-market-backup.sh
Executable file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# food-market: ежедневный бэкап БД + загруженных файлов с ротацией.
|
||||||
|
#
|
||||||
|
# Дампит Postgres (контейнер food-market-postgres) в custom-формат pg_dump
|
||||||
|
# (-Fc, пригоден для pg_restore с параллелизмом/выборочным восстановлением) и
|
||||||
|
# архивирует каталог uploads. Удаляет бэкапы старше RETENTION_DAYS дней.
|
||||||
|
#
|
||||||
|
# Запускается из systemd-таймера food-market-backup.timer (ежедневно), либо
|
||||||
|
# вручную. Конфигурируется переменными окружения (значения по умолчанию
|
||||||
|
# совпадают с deploy/docker-compose.yml):
|
||||||
|
#
|
||||||
|
# FM_PG_CONTAINER имя контейнера Postgres (food-market-postgres)
|
||||||
|
# FM_PG_DB имя БД (food_market)
|
||||||
|
# FM_PG_USER пользователь БД (food_market)
|
||||||
|
# FM_BACKUP_DIR куда складывать бэкапы (/opt/food-market-data/backups)
|
||||||
|
# FM_UPLOADS_DIR каталог изображений (/opt/food-market-data/uploads)
|
||||||
|
# FM_BACKUP_RETENTION_DAYS срок хранения, дней (30)
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONTAINER="${FM_PG_CONTAINER:-food-market-postgres}"
|
||||||
|
DB="${FM_PG_DB:-food_market}"
|
||||||
|
DB_USER="${FM_PG_USER:-food_market}"
|
||||||
|
BACKUP_DIR="${FM_BACKUP_DIR:-/opt/food-market-data/backups}"
|
||||||
|
UPLOADS_DIR="${FM_UPLOADS_DIR:-/opt/food-market-data/uploads}"
|
||||||
|
RETENTION_DAYS="${FM_BACKUP_RETENTION_DAYS:-30}"
|
||||||
|
|
||||||
|
TS="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
DB_FILE="$BACKUP_DIR/db-$TS.dump"
|
||||||
|
UPLOADS_FILE="$BACKUP_DIR/uploads-$TS.tgz"
|
||||||
|
|
||||||
|
log() { echo "[$(date -Is)] $*"; }
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
|
||||||
|
log "ОШИБКА: контейнер '$CONTAINER' не запущен — бэкап невозможен." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Дамп БД '$DB' → $DB_FILE"
|
||||||
|
# Дамп пишем во временный файл и переименовываем по успеху — частичный/битый
|
||||||
|
# дамп при падении pg_dump не попадёт в ротацию как валидный.
|
||||||
|
if docker exec "$CONTAINER" pg_dump -U "$DB_USER" -d "$DB" -Fc > "$DB_FILE.tmp"; then
|
||||||
|
mv "$DB_FILE.tmp" "$DB_FILE"
|
||||||
|
log "Готово: $(du -h "$DB_FILE" | cut -f1)"
|
||||||
|
else
|
||||||
|
rm -f "$DB_FILE.tmp"
|
||||||
|
log "ОШИБКА: pg_dump завершился с ошибкой." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$UPLOADS_DIR" ]; then
|
||||||
|
log "Архив uploads '$UPLOADS_DIR' → $UPLOADS_FILE"
|
||||||
|
tar czf "$UPLOADS_FILE.tmp" -C "$(dirname "$UPLOADS_DIR")" "$(basename "$UPLOADS_DIR")" \
|
||||||
|
&& mv "$UPLOADS_FILE.tmp" "$UPLOADS_FILE"
|
||||||
|
log "Готово: $(du -h "$UPLOADS_FILE" | cut -f1)"
|
||||||
|
else
|
||||||
|
log "Каталог uploads '$UPLOADS_DIR' отсутствует — пропуск."
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Ротация: удаляю бэкапы старше $RETENTION_DAYS дн."
|
||||||
|
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'db-*.dump' -mtime +"$RETENTION_DAYS" -print -delete
|
||||||
|
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'uploads-*.tgz' -mtime +"$RETENTION_DAYS" -print -delete
|
||||||
|
|
||||||
|
log "Бэкап завершён."
|
||||||
14
deploy/food-market-backup.timer
Normal file
14
deploy/food-market-backup.timer
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[Unit]
|
||||||
|
Description=food-market: ежедневный бэкап (03:00)
|
||||||
|
Documentation=https://github.com/nurdotnet/food-market/blob/main/docs/backup-restore.md
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
# Каждый день в 03:00 локального времени сервера.
|
||||||
|
OnCalendar=*-*-* 03:00:00
|
||||||
|
# Догнать пропущенный запуск, если сервер был выключен в момент срабатывания.
|
||||||
|
Persistent=true
|
||||||
|
# Небольшой разброс — на случай нескольких таймеров одновременно.
|
||||||
|
RandomizedDelaySec=300
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
101
docs/backup-restore.md
Normal file
101
docs/backup-restore.md
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# Бэкап и восстановление
|
||||||
|
|
||||||
|
Артефакты в репозитории (`deploy/`):
|
||||||
|
|
||||||
|
- `food-market-backup.sh` — скрипт бэкапа БД + uploads с ротацией.
|
||||||
|
- `food-market-backup.service` — systemd oneshot-юнит, запускающий скрипт.
|
||||||
|
- `food-market-backup.timer` — ежедневный таймер (03:00, с догоном пропущенных).
|
||||||
|
|
||||||
|
> Установку на prod-vm выполняет отдельный деплой-шаг (см. ниже) — здесь только
|
||||||
|
> подготовленные артефакты.
|
||||||
|
|
||||||
|
## Что бэкапится
|
||||||
|
|
||||||
|
| Что | Как | Файл |
|
||||||
|
|---|---|---|
|
||||||
|
| База данных | `pg_dump -Fc` из контейнера `food-market-postgres` | `db-<TS>.dump` (custom-format) |
|
||||||
|
| Загруженные файлы (картинки товаров) | `tar czf` каталога uploads | `uploads-<TS>.tgz` |
|
||||||
|
|
||||||
|
Папка назначения по умолчанию — `/opt/food-market-data/backups`. Хранение —
|
||||||
|
30 дней (`FM_BACKUP_RETENTION_DAYS`), старые удаляются ротацией. Конфиг —
|
||||||
|
переменными `FM_*` (см. шапку `food-market-backup.sh`).
|
||||||
|
|
||||||
|
## Установка таймера на сервере (деплой-шаг)
|
||||||
|
|
||||||
|
Предполагается, что репозиторий выложен в `/opt/food-market` (иначе скорректировать
|
||||||
|
`ExecStart`/`EnvironmentFile` в `.service` и пути ниже).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo install -m 0755 /opt/food-market/deploy/food-market-backup.sh /opt/food-market/deploy/food-market-backup.sh
|
||||||
|
sudo cp /opt/food-market/deploy/food-market-backup.service /etc/systemd/system/
|
||||||
|
sudo cp /opt/food-market/deploy/food-market-backup.timer /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now food-market-backup.timer
|
||||||
|
|
||||||
|
# Проверить расписание и последний запуск
|
||||||
|
systemctl list-timers food-market-backup.timer
|
||||||
|
# Прогнать бэкап немедленно (разово)
|
||||||
|
sudo systemctl start food-market-backup.service
|
||||||
|
journalctl -u food-market-backup.service --no-pager | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ручной бэкап
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo /opt/food-market/deploy/food-market-backup.sh
|
||||||
|
# или с переопределением каталога:
|
||||||
|
FM_BACKUP_DIR=/mnt/backups sudo -E /opt/food-market/deploy/food-market-backup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Восстановление БД
|
||||||
|
|
||||||
|
> ⚠️ Восстановление перезаписывает данные. Сначала остановить API, чтобы не было
|
||||||
|
> записи во время восстановления.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/food-market/deploy
|
||||||
|
docker compose stop api web
|
||||||
|
|
||||||
|
DUMP=/opt/food-market-data/backups/db-YYYYMMDD-HHMMSS.dump
|
||||||
|
|
||||||
|
# Скопировать дамп внутрь контейнера БД
|
||||||
|
docker cp "$DUMP" food-market-postgres:/tmp/restore.dump
|
||||||
|
|
||||||
|
# Вариант A — восстановить в чистую БД (рекомендуется):
|
||||||
|
docker exec food-market-postgres psql -U food_market -d postgres -c \
|
||||||
|
"DROP DATABASE IF EXISTS food_market WITH (FORCE); CREATE DATABASE food_market OWNER food_market;"
|
||||||
|
docker exec food-market-postgres pg_restore -U food_market -d food_market --no-owner /tmp/restore.dump
|
||||||
|
|
||||||
|
# Вариант B — в существующую БД, заменив объекты (без пересоздания БД):
|
||||||
|
# docker exec food-market-postgres pg_restore -U food_market -d food_market --clean --if-exists --no-owner /tmp/restore.dump
|
||||||
|
|
||||||
|
docker exec food-market-postgres rm -f /tmp/restore.dump
|
||||||
|
docker compose start api web
|
||||||
|
```
|
||||||
|
|
||||||
|
После старта API применит миграции (`Migrate()` идемпотентен) и поднимется. Проверить:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS http://localhost:8080/health/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
## Восстановление uploads
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TGZ=/opt/food-market-data/backups/uploads-YYYYMMDD-HHMMSS.tgz
|
||||||
|
# tar содержит каталог uploads/ — распаковать в родителя смонтированного пути
|
||||||
|
sudo tar xzf "$TGZ" -C /opt/food-market-data/
|
||||||
|
```
|
||||||
|
|
||||||
|
(Каталог `/opt/food-market-data/uploads` смонтирован в контейнер api как `/app/uploads`.)
|
||||||
|
|
||||||
|
## Проверка дампа без восстановления
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker cp <dump> food-market-postgres:/tmp/v.dump
|
||||||
|
docker exec food-market-postgres pg_restore --list /tmp/v.dump | head # TOC валидного архива
|
||||||
|
docker exec food-market-postgres rm -f /tmp/v.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт и формат проверены локально (2026-05-27): дамп `PGDMP`, custom-format,
|
||||||
|
248 TOC-записей, `pg_restore --list` читает.
|
||||||
Loading…
Reference in a new issue