Production Postgres had NO automated backup (pg_dump appeared only in the one-time migration script). New db-backup service in both compose files fires 20:10 ET Mon-Fri: pg_dump | gzip with pipefail + gunzip -t + tmp-then-rename (a mid-stream death can't masquerade as a backup), prunes past BACKUP_KEEP_DAYS, writes last_backup_ok on success. postgres:18 image = version-matched client (app image has none). TrueNAS host is pull-only, so its copy inlines the script. Restore: gunzip -c dump | psql $DATABASE_URL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
31 lines
1.2 KiB
Bash
31 lines
1.2 KiB
Bash
#!/usr/bin/env bash
|
|
# Nightly logical backup of the momentum Postgres. Runs inside the db-backup
|
|
# compose service — the postgres:18 image, so pg_dump always matches the
|
|
# server major (the app image has NO pg_dump, and a Debian-stock client is an
|
|
# older major that refuses to dump a newer server).
|
|
#
|
|
# pg_dump | gzip -> $BACKUP_DIR/momentum-YYYY-MM-DD.sql.gz
|
|
# prune anything older than BACKUP_KEEP_DAYS (default 14)
|
|
# write $BACKUP_DIR/last_backup_ok on success (freshness marker)
|
|
#
|
|
# Restore: gunzip -c momentum-<date>.sql.gz | psql "$DATABASE_URL"
|
|
#
|
|
# Run with bash (not sh): pipefail is what catches a mid-stream pg_dump death
|
|
# that would otherwise leave a truncated-but-valid gzip behind.
|
|
set -euo pipefail
|
|
|
|
BACKUP_DIR="${BACKUP_DIR:-/backups}"
|
|
BACKUP_KEEP_DAYS="${BACKUP_KEEP_DAYS:-14}"
|
|
: "${DATABASE_URL:?DATABASE_URL is required}"
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
out="$BACKUP_DIR/momentum-$(date +%F).sql.gz"
|
|
|
|
echo "[backup] $(date) -> $out"
|
|
pg_dump "$DATABASE_URL" | gzip > "$out.tmp"
|
|
gunzip -t "$out.tmp"
|
|
mv "$out.tmp" "$out"
|
|
find "$BACKUP_DIR" -name 'momentum-*.sql.gz*' -mtime +"$BACKUP_KEEP_DAYS" -delete # *: also sweep stale .tmp from failed runs
|
|
date > "$BACKUP_DIR/last_backup_ok"
|
|
echo "[backup] OK $(du -h "$out" | cut -f1)"
|