pg_dump -> gzip -> gpg AES-256 symmetric encryption, plaintext deleted immediately, optional off-host rsync-over-SSH copy (Hetzner Storage Box or any SSH target), retention pruning, Prometheus textfile-collector metrics for backup success/age/size. Genuinely tested end-to-end, not just written: real production migrations (17 total, correct cross-service order) applied to a disposable Postgres, sample data inserted, backed up, restored into a second disposable Postgres, verified table-for-table and byte-for-byte. Exit 0.
39 lines
1.8 KiB
Bash
Executable File
39 lines
1.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Restores an encrypted backup produced by backup-postgres.sh into a target database.
|
|
# Deliberately requires an EXPLICIT target — never silently restores over the live production
|
|
# database ("what would you do if this ran the wrong way" is a real disaster-recovery failure
|
|
# mode, so making that impossible to trigger by accident matters more than convenience here).
|
|
#
|
|
# Usage: ./restore-postgres.sh <encrypted-backup-file> <target-db-url>
|
|
#
|
|
# For a REAL production restore, target-db-url should point at a FRESH database created for
|
|
# this purpose (docs/RUNBOOK_POSTGRES_RESTORE.md walks through creating it) — restoring into a
|
|
# database that already has data merges/conflicts rather than cleanly replacing it.
|
|
set -euo pipefail
|
|
|
|
ENC_FILE="${1:?usage: restore-postgres.sh <encrypted-backup-file> <target-db-url>}"
|
|
TARGET_DB_URL="${2:?usage: restore-postgres.sh <encrypted-backup-file> <target-db-url>}"
|
|
: "${BACKUP_ENCRYPTION_PASSPHRASE:?BACKUP_ENCRYPTION_PASSPHRASE is required to decrypt}"
|
|
|
|
[ -f "$ENC_FILE" ] || { echo "No such file: $ENC_FILE" >&2; exit 1; }
|
|
|
|
TMPDIR="$(mktemp -d)"
|
|
trap 'rm -rf "$TMPDIR"' EXIT
|
|
DUMP_FILE="$TMPDIR/restore.sql.gz"
|
|
|
|
echo "==> Decrypting $ENC_FILE"
|
|
gpg --batch --yes --passphrase "$BACKUP_ENCRYPTION_PASSPHRASE" --decrypt --output "$DUMP_FILE" "$ENC_FILE"
|
|
|
|
echo "==> Confirming target is reachable"
|
|
psql "$TARGET_DB_URL" -c "SELECT 1" > /dev/null
|
|
|
|
echo "==> Target database currently has these tables (should be empty/unimportant before you continue):"
|
|
psql "$TARGET_DB_URL" -c "\dt" || true
|
|
read -r -p "Type RESTORE to proceed against $TARGET_DB_URL: " CONFIRM
|
|
[ "$CONFIRM" = "RESTORE" ] || { echo "Aborted."; exit 1; }
|
|
|
|
echo "==> Restoring"
|
|
gunzip -c "$DUMP_FILE" | psql "$TARGET_DB_URL"
|
|
|
|
echo "==> Restore complete. Verify with infra/backup/verify-restore.sh $TARGET_DB_URL"
|