#!/bin/sh # Praxis v0.4 — Nightly Postgres backup (D-055, G-008). # # Host-side cron script (decoupled from praxis service uptime — # RESEARCH-v0.4 §1.5). Runs pg_dump inside the postgres container and # writes a compressed custom-format dump to the pgbackups volume. # # The %u date format = day-of-week 1..7 (Monday=1, Sunday=7) → rolling # 7-file retention with zero cleanup logic (D-055). Re-running overwrites # the same day-of-week file. # # Cron entry (host, 03:30 CT nightly): # 30 3 * * * /opt/praxis/scripts/backup-pg.sh # # Restore drill (G-008 — run at least once in staging to prove the backup # is valid; NEVER restore into a live DB without stopping praxis first): # docker compose stop praxis # docker compose exec postgres pg_restore -U praxis -d praxis \ # --clean --if-exists /backups/praxis-3.dump # # verify: \d operators; SELECT count(*) FROM operators; (etc. for all 5 tables) # docker compose start praxis # # POSIX-sh compatible (no bashisms). Exit 0 on success, 1 on failure. # Args: none. Env: COMPOSE_PROJECT_DIR (default: current dir). set -eu PROJECT_DIR="${COMPOSE_PROJECT_DIR:-$(pwd)}" cd "$PROJECT_DIR" DOW="$(date +%u)" DUMP_FILE="/backups/praxis-${DOW}.dump" echo "backup-pg: dumping praxis DB → ${DUMP_FILE} (day-of-week ${DOW})" # -Fc = custom compressed format (works with pg_restore --clean --if-exists). # -T stops the container from streaming while dumping? No — pg_dump is # consistent within a transaction; the praxis service can stay up. docker compose exec -T postgres pg_dump -U praxis -Fc praxis -f "$DUMP_FILE" # Verify the dump is non-empty (sanity — a 0-byte dump means failure). SIZE=$(docker compose exec -T postgres stat -c '%s' "$DUMP_FILE" 2>/dev/null || echo 0) if [ "$SIZE" -le 0 ]; then echo "backup-pg: ERROR — dump file is empty (${DUMP_FILE})" >&2 exit 1 fi echo "backup-pg: OK — ${DUMP_FILE} is ${SIZE} bytes" echo "backup-pg: restore drill (G-008): docker compose exec postgres pg_restore -U praxis -d praxis --clean --if-exists ${DUMP_FILE}" exit 0