#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" ENV_FILE="${1:-$ROOT_DIR/.env.production}" BACKUP_ROOT="${2:-$ROOT_DIR/backups}" COMPOSE_FILE="$ROOT_DIR/infra/docker-compose.prod.yml" if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then cat <<'USAGE' Usage: scripts/ops/backup-prod.sh [env-file] [backup-root] Examples: scripts/ops/backup-prod.sh scripts/ops/backup-prod.sh .env.production /var/backups/productier Behavior: - Validates production env configuration. - Creates a timestamped backup directory. - Dumps PostgreSQL to postgres.sql.gz. - Syncs S3-compatible object data to s3/ via rustfs-init container. - Writes SHA256 checksums and metadata.json. USAGE exit 0 fi if [[ ! -f "$ENV_FILE" ]]; then echo "[error] env file not found: $ENV_FILE" >&2 exit 1 fi if ! command -v docker >/dev/null 2>&1; then echo "[error] docker CLI is required" >&2 exit 1 fi node "$ROOT_DIR/scripts/check-production-env.mjs" "$ENV_FILE" timestamp="$(date -u +%Y%m%dT%H%M%SZ)" backup_dir="$BACKUP_ROOT/$timestamp" tmp_backup_dir="$BACKUP_ROOT/.tmp-$timestamp-$$" mkdir -p "$tmp_backup_dir/s3" echo "[info] writing backup to $backup_dir" echo "[step] backing up postgres..." docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" exec -T postgres \ pg_dump -U productier -d productier --format=plain --no-owner --no-privileges \ | gzip -9 > "$tmp_backup_dir/postgres.sql.gz" echo "[step] backing up object storage..." docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" run --rm --no-deps \ -v "$tmp_backup_dir/s3:/backup" \ --entrypoint /bin/sh \ rustfs-init \ -lc 'set -euo pipefail; endpoint="http://rustfs:9000"; aws --endpoint-url "$endpoint" s3 sync "s3://${S3_BUCKET:-productier}" /backup --no-progress' echo "[step] writing checksums..." ( cd "$tmp_backup_dir" sha256sum postgres.sql.gz > checksums.sha256 if find s3 -type f -print -quit | grep -q .; then find s3 -type f -print0 | sort -z | xargs -0 sha256sum >> checksums.sha256 fi ) cat > "$tmp_backup_dir/metadata.json" <