docs(ops): backup/restore + email deliverability runbooks

Two new runbooks under docs/runbooks/ plus the automation scripts the
backup runbook references. Both are written so an operator who has only
the off-site backup credentials and the runbook can recover the system
unaided.

Backup/restore (Phase 4a):
- docs/runbooks/backup-and-restore.md — covers what gets backed up
  (Postgres / MinIO / .env+ENCRYPTION_KEY), schedule (hourly DB +
  hourly MinIO mirror, 7-day hourly + 30-day daily retention),
  cold-restore procedure with row-count verification, weekly drill
- scripts/backup/pg-backup.sh — pg_dump → gzip → optional GPG → mc
  upload, fails loud
- scripts/backup/minio-mirror.sh — incremental mc mirror, no --remove
  flag so accidental deletes on the live bucket can't cascade
- scripts/backup/restore.sh — interactive prod restore + --drill mode
  that runs against a sandbox DB and diffs row counts

Email deliverability (Phase 4b):
- docs/runbooks/email-deliverability.md — what the CRM sends, DNS
  records (SPF/DKIM/DMARC/MX), per-port override implications,
  diagnosis flow ("didn't arrive" → 4-step checklist starting with
  EMAIL_REDIRECT_TO), provider migration plan, realapi suite as the
  end-to-end probe

Tests still 778/778 vitest, tsc/lint clean — these phases are docs +
shell scripts, no code changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-28 20:10:30 +02:00
parent a3305a94f3
commit 6eb0d3dc92
5 changed files with 620 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Hourly MinIO mirror for Port Nimara CRM.
#
# Mirrors the live `MINIO_BUCKET` to the backup destination. `mc mirror`
# is incremental — only changed objects transfer — so this is cheap.
#
# Versioning on the destination bucket is what protects against object
# deletes / overwrites; we don't try to roll our own.
set -euo pipefail
: "${MINIO_ENDPOINT:?MINIO_ENDPOINT not set}"
: "${MINIO_ACCESS_KEY:?MINIO_ACCESS_KEY not set}"
: "${MINIO_SECRET_KEY:?MINIO_SECRET_KEY not set}"
: "${MINIO_BUCKET:?MINIO_BUCKET not set}"
: "${BACKUP_S3_BUCKET:?BACKUP_S3_BUCKET not set}"
: "${BACKUP_S3_ENDPOINT:?BACKUP_S3_ENDPOINT not set}"
: "${BACKUP_S3_ACCESS_KEY:?BACKUP_S3_ACCESS_KEY not set}"
: "${BACKUP_S3_SECRET_KEY:?BACKUP_S3_SECRET_KEY not set}"
# Default scheme: live MinIO is plain HTTP unless MINIO_USE_SSL=true.
LIVE_URL="${MINIO_ENDPOINT}"
if [[ "${MINIO_USE_SSL:-false}" == "true" ]]; then
LIVE_URL="https://${MINIO_ENDPOINT}:${MINIO_PORT:-443}"
else
LIVE_URL="http://${MINIO_ENDPOINT}:${MINIO_PORT:-9000}"
fi
LIVE_ALIAS="live-$$"
BACKUP_ALIAS="bk-$$"
trap 'mc alias remove "$LIVE_ALIAS" 2>/dev/null || true; mc alias remove "$BACKUP_ALIAS" 2>/dev/null || true' EXIT
mc alias set "$LIVE_ALIAS" "$LIVE_URL" \
"$MINIO_ACCESS_KEY" "$MINIO_SECRET_KEY" --api S3v4 >/dev/null
mc alias set "$BACKUP_ALIAS" "$BACKUP_S3_ENDPOINT" \
"$BACKUP_S3_ACCESS_KEY" "$BACKUP_S3_SECRET_KEY" --api S3v4 >/dev/null
SOURCE="${LIVE_ALIAS}/${MINIO_BUCKET}/"
DEST="${BACKUP_ALIAS}/${BACKUP_S3_BUCKET}/minio/"
echo "[$(date -u +%FT%TZ)] Mirroring $SOURCE$DEST"
# `--remove` would delete objects from the destination that no longer
# exist in source — we DON'T pass it, because that would let an
# accidental delete on the live bucket cascade into permanent loss on
# the backup side. Versioning + lifecycle handle stale-object cleanup.
mc mirror --quiet --overwrite "$SOURCE" "$DEST"
# Print byte / count diff for the operator.
echo "[$(date -u +%FT%TZ)] Done. Destination summary:"
mc du "$DEST"