Two complementary safety nets for the business-critical DB:
1. Host-side nightly backup
- scripts/backup-db.sh drives pg_dump through docker compose exec,
gzips to /srv/backups/dow-prod-tracker/, auto-prunes >30 days.
Env-overridable (BACKUP_DIR / RETAIN_DAYS / COMPOSE_DIR / PGUSER
/ PGDATABASE) for anyone running a different layout.
- Runs from host cron at midnight; crontab snippet + restore
procedure + optional off-site (S3 / rsync) pattern documented
in DEPLOY.md.
2. On-demand admin XLSX export
- New GET /api/projects/export?format=xlsx — ADMIN-only; builds a
two-sheet workbook via new buildFullExportWorkbook():
- "Job Tracker": one row per project, header strings chosen to
round-trip through the Dow bulk-import endpoint so a dump can
be re-ingested in a worst case. Owner / Risk / OMG Number /
Team / etc., mirroring the importer's fuzzy HEADER_MATCHERS.
- "Deliverables": one row per deliverable with project OMG #,
status, priority, dates, CMF/SKU, current stage, assignees,
notes — enough to reconstitute pipeline state.
Respects visibility scoping (ADMIN sees everything).
- Dashboard shows an "Export Full XLSX" button in the header for
admins; streams the workbook with a date-stamped filename using
the standard blob-download pattern from ExportButton.
Both are additive — no schema, no migration, no deploy breakage.
56 lines
2.2 KiB
Bash
Executable file
56 lines
2.2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# scripts/backup-db.sh — nightly Postgres dump for dow-prod-tracker
|
|
#
|
|
# Runs on the HOST (not inside the app container), driving pg_dump
|
|
# through the db service's compose project. Writes a gzipped SQL
|
|
# file to $BACKUP_DIR and prunes anything older than $RETAIN_DAYS.
|
|
#
|
|
# Intended to run from cron at midnight — see DEPLOY.md for the
|
|
# crontab entry. Safe to run manually at any time.
|
|
#
|
|
# Env vars (all optional, sensible defaults for optical-dev):
|
|
# BACKUP_DIR where to write dumps (default /srv/backups/dow-prod-tracker)
|
|
# RETAIN_DAYS days to keep dumps (default 30)
|
|
# COMPOSE_PROJECT compose project name (default dow-prod-tracker)
|
|
# COMPOSE_DIR dir containing docker-compose.yml (default /opt/dow-prod-tracker)
|
|
# PGDATABASE database name (default dow_prod_tracker)
|
|
# PGUSER postgres user (default postgres)
|
|
#
|
|
# Exit codes: 0 ok, non-zero = failure (cron will mail root on non-zero).
|
|
|
|
set -euo pipefail
|
|
|
|
BACKUP_DIR="${BACKUP_DIR:-/srv/backups/dow-prod-tracker}"
|
|
RETAIN_DAYS="${RETAIN_DAYS:-30}"
|
|
COMPOSE_PROJECT="${COMPOSE_PROJECT:-dow-prod-tracker}"
|
|
COMPOSE_DIR="${COMPOSE_DIR:-/opt/dow-prod-tracker}"
|
|
PGDATABASE="${PGDATABASE:-dow_prod_tracker}"
|
|
PGUSER="${PGUSER:-postgres}"
|
|
|
|
TIMESTAMP=$(date +%Y-%m-%d_%H%M%S)
|
|
OUT_FILE="${BACKUP_DIR}/dow-prod-tracker_${TIMESTAMP}.sql.gz"
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
|
|
echo "[backup] $(date -Iseconds) starting dump → $OUT_FILE"
|
|
|
|
# Stream pg_dump output straight through gzip to disk without buffering
|
|
# the whole dump in memory. `exec -T` keeps the TTY disabled so this
|
|
# works from cron. We `cd` into the compose dir because compose reads
|
|
# its project definition from the working dir.
|
|
(
|
|
cd "$COMPOSE_DIR"
|
|
docker compose -p "$COMPOSE_PROJECT" exec -T db \
|
|
pg_dump -U "$PGUSER" --format=plain --clean --if-exists "$PGDATABASE"
|
|
) | gzip > "$OUT_FILE"
|
|
|
|
SIZE=$(du -h "$OUT_FILE" | cut -f1)
|
|
echo "[backup] wrote $OUT_FILE ($SIZE)"
|
|
|
|
# Prune anything older than RETAIN_DAYS. `-mtime +N` is "modified more
|
|
# than N days ago" — older dumps get deleted. Fails gracefully if the
|
|
# dir is empty.
|
|
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'dow-prod-tracker_*.sql.gz' \
|
|
-mtime +"$RETAIN_DAYS" -print -delete || true
|
|
|
|
echo "[backup] $(date -Iseconds) done"
|