backup: nightly pg_dump service on the postgres:18 image
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>
This commit is contained in:
1 parent
5b95a9712c
commit
caee1d5655
4 files changed
+107
No files matched your search
@@ -0,0 +1,30 @@
|
||||
#!/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)"
|
||||
@@ -194,3 +194,42 @@ services:
|
||||
fi
|
||||
sleep 20
|
||||
done
|
||||
|
||||
db-backup:
|
||||
# Nightly pg_dump at 20:10 ET Mon-Fri, on the postgres:18 image so the
|
||||
# client always matches the server major (the app image has NO pg_dump).
|
||||
# This host is pull-only (no repo checkout), so the backup logic is INLINE
|
||||
# — keep it in sync with deploy/docker/backup_db.sh. Restore:
|
||||
# gunzip -c momentum-<date>.sql.gz | psql "$DATABASE_URL"
|
||||
image: postgres:18
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DATABASE_URL: 'postgresql://momentum:<FILL-ME-PG-PASSWORD>@postgres:5432/momentum'
|
||||
BACKUP_DIR: '/backups'
|
||||
BACKUP_KEEP_DAYS: '14'
|
||||
TZ: 'America/New_York' # postgres image defaults to UTC; the 20:10 gate is ET
|
||||
volumes:
|
||||
- /mnt/volume/Apps/momentum/data/backups:/backups
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
set -o pipefail
|
||||
echo "[backup] up; fires 20:10 ET Mon-Fri"
|
||||
while true; do
|
||||
if [ "$$(date +%H:%M)" = "20:10" ] && [ "$$(date +%u)" -le 5 ]; then
|
||||
out="$$BACKUP_DIR/momentum-$$(date +%F).sql.gz"
|
||||
mkdir -p "$$BACKUP_DIR" \
|
||||
&& 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 \
|
||||
&& date > "$$BACKUP_DIR/last_backup_ok" \
|
||||
&& echo "[backup] OK $$out" \
|
||||
|| echo "[backup] FAILED"
|
||||
sleep 120
|
||||
fi
|
||||
sleep 20
|
||||
done
|
||||
Reference in new issue
Block a user