#!/bin/bash # backup.sh — pg_dump | gzip | gpg encrypt → /backups/ # Run inside the backend container: # docker compose exec backend bash /app/scripts/backup.sh # Or from host: # docker compose exec -e BACKUP_PASSPHRASE="$BACKUP_PASSPHRASE" backend bash scripts/backup.sh set -euo pipefail TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_DIR="${BACKUP_DIR:-/app/backups}" BACKUP_FILE="${BACKUP_DIR}/${TIMESTAMP}.sql.gz.gpg" RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}" # Require passphrase if [ -z "${BACKUP_PASSPHRASE:-}" ]; then echo "[backup] ERROR: BACKUP_PASSPHRASE is not set" >&2 exit 1 fi mkdir -p "${BACKUP_DIR}" echo "[backup] Starting at ${TIMESTAMP}" # pg_dump using the DATABASE_URL but swap asyncpg driver for psycopg2-compatible URL PG_URL="${DATABASE_URL/postgresql+asyncpg/postgresql}" pg_dump "${PG_URL}" \ | gzip \ | gpg --batch --yes --symmetric --cipher-algo AES256 \ --passphrase "${BACKUP_PASSPHRASE}" \ --output "${BACKUP_FILE}" SIZE=$(du -sh "${BACKUP_FILE}" | cut -f1) echo "[backup] Written ${SIZE} → ${BACKUP_FILE}" # List current backups COUNT=$(find "${BACKUP_DIR}" -name "*.sql.gz.gpg" | wc -l) echo "[backup] ${COUNT} backup(s) on disk" # Prune old backups PRUNED=$(find "${BACKUP_DIR}" -name "*.sql.gz.gpg" -mtime "+${RETENTION_DAYS}" -print -delete | wc -l) if [ "${PRUNED}" -gt 0 ]; then echo "[backup] Pruned ${PRUNED} backup(s) older than ${RETENTION_DAYS} days" fi echo "[backup] Done"