briven-backup.sh 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. #!/usr/bin/env bash
  2. # Daily backup of briven control plane (+ optional off-site mirror).
  3. # Runs on the France host via systemd (briven-backup.{service,timer}).
  4. #
  5. # Topology (2026-07-21): Briven is Doltgres end-to-end.
  6. # - Control + data plane: DoltGres (briven_control + proj_* databases).
  7. # - dolt-backup sidecar runs native dolt_backup for ALL databases including
  8. # briven_control. This host script is a transitional dump helper for
  9. # stock Postgres rollback windows only — prefer dolt_backup + off-site
  10. # mirror of the doltgres_backups volume for real DR.
  11. #
  12. # Env (optional, /etc/briven/backup.env):
  13. # BRIVEN_BACKUP_PG_CONTAINER default: briven-brivenfrance-uilsk6-postgres-1
  14. # BRIVEN_BACKUP_PG_USER default: postgres
  15. # BRIVEN_BACKUP_DBS space-separated; default: briven_control
  16. # BRIVEN_BACKUP_S3_ENDPOINT if set with bucket+keys → off-site upload
  17. # BRIVEN_BACKUP_S3_BUCKET
  18. # BRIVEN_BACKUP_S3_ACCESS_KEY
  19. # BRIVEN_BACKUP_S3_SECRET_KEY
  20. #
  21. # Local layout:
  22. # /var/backups/briven/<db-name>/<YYYY-MM-DD>/<hh-mm-ss>.dump.gz
  23. #
  24. # Exit 1 if any off-site upload fails (local dump still kept) so
  25. # OnFailure=briven-backup-alert.service can fire.
  26. set -euo pipefail
  27. BACKUP_ENV_FILE="/etc/briven/backup.env"
  28. if [ -f "$BACKUP_ENV_FILE" ]; then
  29. # shellcheck disable=SC1090
  30. source "$BACKUP_ENV_FILE"
  31. fi
  32. PG_CONTAINER="${BRIVEN_BACKUP_PG_CONTAINER:-briven-brivenfrance-uilsk6-postgres-1}"
  33. PG_USER="${BRIVEN_BACKUP_PG_USER:-postgres}"
  34. # shellcheck disable=SC2206
  35. DBS=(${BRIVEN_BACKUP_DBS:-briven_control})
  36. LOCAL_BACKUP_ROOT="/var/backups/briven"
  37. LOCAL_RETENTION_DAYS="${BRIVEN_BACKUP_LOCAL_RETENTION_DAYS:-30}"
  38. STAMP="$(date -u +'%Y-%m-%d/%H-%M-%S')"
  39. UPLOAD_FAILURES=0
  40. UPLOAD_FAILURE_DBS=""
  41. log() {
  42. printf '[%s] %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*"
  43. }
  44. die() {
  45. log "ERROR: $*"
  46. exit 1
  47. }
  48. dump_one() {
  49. local db_name="$1"
  50. local local_dir="${LOCAL_BACKUP_ROOT}/${db_name}/${STAMP%/*}"
  51. local local_file="${local_dir}/${STAMP##*/}.dump.gz"
  52. mkdir -p "$local_dir"
  53. if ! docker inspect "$PG_CONTAINER" >/dev/null 2>&1; then
  54. die "postgres container not found: ${PG_CONTAINER}"
  55. fi
  56. log "dumping ${db_name} from ${PG_CONTAINER}"
  57. if ! docker exec "$PG_CONTAINER" pg_dump \
  58. --username="$PG_USER" \
  59. --format=custom \
  60. --compress=0 \
  61. --no-owner --no-privileges \
  62. "$db_name" \
  63. | gzip -9 > "$local_file"; then
  64. rm -f "$local_file"
  65. die "pg_dump failed for ${db_name}"
  66. fi
  67. local size
  68. size="$(stat -c%s "$local_file")"
  69. log "ok ${db_name}: ${size} bytes → ${local_file}"
  70. # Optional off-site (B2/R2 S3-compatible via minio/mc).
  71. if [ -n "${BRIVEN_BACKUP_S3_ENDPOINT:-}" ] \
  72. && [ -n "${BRIVEN_BACKUP_S3_BUCKET:-}" ] \
  73. && [ -n "${BRIVEN_BACKUP_S3_ACCESS_KEY:-}" ] \
  74. && [ -n "${BRIVEN_BACKUP_S3_SECRET_KEY:-}" ]; then
  75. local object="s3://${BRIVEN_BACKUP_S3_BUCKET}/${db_name}/${STAMP}.dump.gz"
  76. log "uploading ${object}"
  77. local endpoint_no_scheme="${BRIVEN_BACKUP_S3_ENDPOINT#https://}"
  78. endpoint_no_scheme="${endpoint_no_scheme#http://}"
  79. if ! docker run --rm \
  80. -v "${local_file}:/backup.dump.gz:ro" \
  81. -e "MC_HOST_off=https://${BRIVEN_BACKUP_S3_ACCESS_KEY}:${BRIVEN_BACKUP_S3_SECRET_KEY}@${endpoint_no_scheme}" \
  82. --entrypoint sh \
  83. minio/mc:latest \
  84. -c "mc cp /backup.dump.gz off/${BRIVEN_BACKUP_S3_BUCKET}/${db_name}/${STAMP}.dump.gz"; then
  85. log "WARN: off-site upload failed for ${db_name} — local copy still safe"
  86. UPLOAD_FAILURES=$((UPLOAD_FAILURES + 1))
  87. UPLOAD_FAILURE_DBS="${UPLOAD_FAILURE_DBS:+${UPLOAD_FAILURE_DBS} }${db_name}"
  88. else
  89. log "off-site upload ok"
  90. fi
  91. else
  92. log "off-site upload skipped (BRIVEN_BACKUP_S3_* unset) — Phase 0.1 still Not done"
  93. fi
  94. }
  95. prune_local() {
  96. log "pruning local dumps older than ${LOCAL_RETENTION_DAYS}d"
  97. if [ -d "$LOCAL_BACKUP_ROOT" ]; then
  98. find "$LOCAL_BACKUP_ROOT" -type f -name '*.dump.gz' -mtime +"$LOCAL_RETENTION_DAYS" -delete || true
  99. find "$LOCAL_BACKUP_ROOT" -type d -empty -delete || true
  100. fi
  101. }
  102. log "briven backup run starting (container=${PG_CONTAINER} dbs=${DBS[*]})"
  103. for db in "${DBS[@]}"; do
  104. dump_one "$db"
  105. done
  106. prune_local
  107. if [ "$UPLOAD_FAILURES" -gt 0 ]; then
  108. log "ERROR: ${UPLOAD_FAILURES} off-site upload(s) failed (dbs=${UPLOAD_FAILURE_DBS})"
  109. echo "upload_failures=${UPLOAD_FAILURES}" > /run/briven-backup-status
  110. echo "upload_failure_dbs=${UPLOAD_FAILURE_DBS}" >> /run/briven-backup-status
  111. exit 1
  112. fi
  113. # Clear stale status on success
  114. rm -f /run/briven-backup-status
  115. log "briven backup run complete"