#!/usr/bin/env bash # # ПРОД food-market: резервная копия БД (pg_dump -Fc) с тирами и офсайт-копией. # # Тиры (FM_BACKUP_TIER): # hourly — каждый час, хранение 48 ч → $FM_BACKUP_DIR/hourly # daily — ежедневно, хранение 30 дн → $FM_BACKUP_DIR/daily (+ офсайт-копия) # # Офсайт: daily-дамп копируется на другой хост/диск (FM_OFFSITE_DEST, scp-назначение # вида user@host:/path ИЛИ локальный путь на другом диске). Копия ротационно # хранится FM_OFFSITE_RETENTION_DAYS (по умолчанию 30). # # Контейнер БД — food-market-prod-postgres (НЕ shared-postgres с чужими базами!). set -euo pipefail CONTAINER="${FM_PG_CONTAINER:-food-market-prod-postgres}" DB="${FM_PG_DB:-food_market}" DB_USER="${FM_PG_USER:-food_market}" BASE_DIR="${FM_BACKUP_DIR:-/opt/food-market-data/backups}" UPLOADS_DIR="${FM_UPLOADS_DIR:-/opt/food-market-data/uploads}" TIER="${FM_BACKUP_TIER:-daily}" OFFSITE_DEST="${FM_OFFSITE_DEST:-}" # напр. nns@192.168.1.193:/home/nns/fm-prod-backups OFFSITE_RETENTION="${FM_OFFSITE_RETENTION_DAYS:-30}" case "$TIER" in hourly) DIR="$BASE_DIR/hourly"; RET_MIN=2880 ;; # 48 ч = 2880 мин daily) DIR="$BASE_DIR/daily"; RET_MIN=43200 ;; # 30 дн = 43200 мин *) echo "FM_BACKUP_TIER должен быть hourly|daily (дано: $TIER)" >&2; exit 2 ;; esac TS="$(date +%Y%m%d-%H%M%S)" DB_FILE="$DIR/db-$TS.dump" log() { echo "[$(date -Is)] [$TIER] $*"; } mkdir -p "$DIR" if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then log "ОШИБКА: контейнер '$CONTAINER' не запущен — бэкап невозможен." >&2 exit 1 fi log "Дамп '$DB' → $DB_FILE" 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 # uploads архивируем только в daily (меняются реже, чем БД). if [ "$TIER" = "daily" ] && [ -d "$UPLOADS_DIR" ]; then UP="$DIR/uploads-$TS.tgz" tar czf "$UP.tmp" -C "$(dirname "$UPLOADS_DIR")" "$(basename "$UPLOADS_DIR")" && mv "$UP.tmp" "$UP" log "uploads → $UP ($(du -h "$UP" | cut -f1))" fi # Офсайт-копия (только daily). if [ "$TIER" = "daily" ] && [ -n "$OFFSITE_DEST" ]; then if [[ "$OFFSITE_DEST" == *:* ]]; then # удалённый scp-назначение host="${OFFSITE_DEST%%:*}"; path="${OFFSITE_DEST#*:}" ssh -o StrictHostKeyChecking=no "$host" "mkdir -p '$path'" 2>/dev/null || true if scp -o StrictHostKeyChecking=no "$DB_FILE" "$OFFSITE_DEST/" 2>/dev/null; then log "офсайт-копия → $OFFSITE_DEST" ssh -o StrictHostKeyChecking=no "$host" "find '$path' -maxdepth 1 -name 'db-*.dump' -mtime +$OFFSITE_RETENTION -delete" 2>/dev/null || true else log "ВНИМАНИЕ: офсайт-копия не удалась (scp $OFFSITE_DEST)" >&2 fi else mkdir -p "$OFFSITE_DEST" cp "$DB_FILE" "$OFFSITE_DEST/" && log "офсайт-копия → $OFFSITE_DEST" find "$OFFSITE_DEST" -maxdepth 1 -name 'db-*.dump' -mtime +"$OFFSITE_RETENTION" -delete 2>/dev/null || true fi fi log "Ротация $DIR: удаляю старше $RET_MIN мин." find "$DIR" -maxdepth 1 -type f -name 'db-*.dump' -mmin +"$RET_MIN" -print -delete find "$DIR" -maxdepth 1 -type f -name 'uploads-*.tgz' -mmin +"$RET_MIN" -print -delete log "Бэкап завершён."