briven-backup.sh 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. #!/usr/bin/env bash
  2. # Daily backup of Briven — Doltgres-native (PRIMARY DR) + stock-Postgres pg_dump
  3. # (secondary rollback). Runs on the France host via systemd
  4. # (briven-backup.{service,timer}).
  5. #
  6. # 2026-08-01 REWORK — why this changed:
  7. # The old `dolt-backup` SIDECAR looped `dolt_backup('sync-url', …)` over EVERY
  8. # database with no throttle, against a buggy DoltGres 0.56.6. That contributed
  9. # to a full-platform outage (engine locked under load). Fix: engine upgraded to
  10. # 0.57.2 (lock-subsystem fixes) with auto-GC disabled, and backups are now a
  11. # GENTLE, one-database-at-a-time, throttled pass run from this host job. The
  12. # sidecar is removed from compose. Proven safe under monitoring (2026-08-01).
  13. #
  14. # Backup layers:
  15. # 1. PRIMARY — `dolt_backup('sync-url', file:///backups/<db>)` for ALL live
  16. # DoltGres DBs (control + engine + every project) → doltgres_backups
  17. # volume (/backups). Restorable via `dolt backup restore`.
  18. # 2. auth.db — snapshot the engine users/grants file (nothing else backs it up).
  19. # 3. SECONDARY— pg_dump of stock-Postgres briven_control (rollback window helper).
  20. # 4. OFF-SITE — mirror the doltgres_backups volume to external S3 (Backblaze/R2/…)
  21. # when BRIVEN_BACKUP_S3_* is configured (see BACKUP-OFFSITE.md).
  22. #
  23. # Env (/etc/briven/backup.env, optional):
  24. # BRIVEN_DOLTGRES_CONTAINER default: briven-brivenfrance-uilsk6-doltgres-1
  25. # BRIVEN_DOLTGRES_BACKUPS_VOLUME default: briven-brivenfrance-uilsk6_doltgres_backups
  26. # BRIVEN_DOLTGRES_PASSWORD default: read from the doltgres container env
  27. # BRIVEN_BACKUP_THROTTLE_SECS default: 8 (pause between DBs — gentleness)
  28. # BRIVEN_BACKUP_PG_CONTAINER default: briven-brivenfrance-uilsk6-postgres-1
  29. # BRIVEN_BACKUP_PG_USER default: postgres
  30. # BRIVEN_BACKUP_PG_DBS default: briven_control (secondary pg_dump)
  31. # BRIVEN_BACKUP_S3_ENDPOINT/BUCKET/ACCESS_KEY/SECRET_KEY → off-site mirror
  32. # BRIVEN_BACKUP_LOCAL_RETENTION_DAYS default: 30
  33. #
  34. # Exit 1 if the dolt phase fails for any DB OR an off-site mirror fails, so
  35. # OnFailure=briven-backup-alert.service fires.
  36. set -euo pipefail
  37. BACKUP_ENV_FILE="/etc/briven/backup.env"
  38. if [ -f "$BACKUP_ENV_FILE" ]; then
  39. # shellcheck disable=SC1090
  40. source "$BACKUP_ENV_FILE"
  41. fi
  42. DC="${BRIVEN_DOLTGRES_CONTAINER:-briven-brivenfrance-uilsk6-doltgres-1}"
  43. BACKUPS_VOLUME="${BRIVEN_DOLTGRES_BACKUPS_VOLUME:-briven-brivenfrance-uilsk6_doltgres_backups}"
  44. THROTTLE="${BRIVEN_BACKUP_THROTTLE_SECS:-8}"
  45. PG_CONTAINER="${BRIVEN_BACKUP_PG_CONTAINER:-briven-brivenfrance-uilsk6-postgres-1}"
  46. PG_USER="${BRIVEN_BACKUP_PG_USER:-postgres}"
  47. # shellcheck disable=SC2206
  48. PG_DBS=(${BRIVEN_BACKUP_PG_DBS:-briven_control})
  49. LOCAL_BACKUP_ROOT="/var/backups/briven"
  50. LOCAL_RETENTION_DAYS="${BRIVEN_BACKUP_LOCAL_RETENTION_DAYS:-30}"
  51. STAMP="$(date -u +'%Y-%m-%d/%H-%M-%S')"
  52. STAMP_FLAT="$(date -u +'%Y-%m-%dT%H-%M-%SZ')"
  53. FAILURES=0
  54. FAILURE_DETAIL=""
  55. log() { printf '[%s] %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*"; }
  56. die() { log "ERROR: $*"; exit 1; }
  57. fail() { log "WARN: $*"; FAILURES=$((FAILURES + 1)); FAILURE_DETAIL="${FAILURE_DETAIL:+${FAILURE_DETAIL}; }$*"; }
  58. # --- resolve doltgres password without printing it ---
  59. dolt_password() {
  60. if [ -n "${BRIVEN_DOLTGRES_PASSWORD:-}" ]; then
  61. printf '%s' "$BRIVEN_DOLTGRES_PASSWORD"; return 0
  62. fi
  63. docker inspect "$DC" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null \
  64. | sed -n 's/^DOLTGRES_PASSWORD=//p' | head -1
  65. }
  66. # ===== PHASE 1: PRIMARY — gentle DoltGres-native backup of every live DB =====
  67. dolt_backup_all() {
  68. docker inspect "$DC" >/dev/null 2>&1 || die "doltgres container not found: ${DC}"
  69. local pw; pw="$(dolt_password)"
  70. [ -n "$pw" ] || die "could not resolve DOLTGRES_PASSWORD"
  71. local base="postgres://postgres:${pw}@127.0.0.1:5432"
  72. log "dolt phase: enumerating live databases"
  73. local dbs ok=0
  74. dbs="$(docker exec "$DC" sh -lc \
  75. "psql \"${base}/postgres?sslmode=disable\" -tAc \"select datname from pg_database where datname not in ('template0','template1','postgres')\"" \
  76. 2>/dev/null || true)"
  77. [ -n "$dbs" ] || die "no databases enumerated (is doltgres serving?)"
  78. for db in $dbs; do
  79. # sync-url writes a restorable dolt archive to the server's /backups/<db>.
  80. if docker exec "$DC" sh -lc \
  81. "psql \"${base}/${db}?sslmode=disable\" -tAc \"select dolt_backup('sync-url','file:///backups/${db}')\"" \
  82. >/dev/null 2>&1; then
  83. log " ok dolt backup: ${db}"
  84. ok=$((ok + 1))
  85. else
  86. fail "dolt backup failed: ${db}"
  87. fi
  88. sleep "$THROTTLE" # gentleness — never hammer the engine
  89. done
  90. log "dolt phase: ${ok} database(s) backed up (throttle=${THROTTLE}s)"
  91. # auth.db snapshot — engine users/grants; keep newest 14.
  92. if docker exec "$DC" test -f /var/lib/doltgres/auth.db 2>/dev/null; then
  93. if docker exec "$DC" sh -lc \
  94. "mkdir -p /backups/auth-db && cp /var/lib/doltgres/auth.db /backups/auth-db/auth.db.${STAMP_FLAT} && ls -1t /backups/auth-db | tail -n +15 | while read -r f; do rm -f \"/backups/auth-db/\$f\"; done" \
  95. >/dev/null 2>&1; then
  96. log " ok auth.db snapshot"
  97. else
  98. fail "auth.db snapshot failed"
  99. fi
  100. fi
  101. }
  102. # ===== PHASE 2: SECONDARY — pg_dump of stock-Postgres (rollback window) =====
  103. pg_dump_secondary() {
  104. docker inspect "$PG_CONTAINER" >/dev/null 2>&1 || { log "pg secondary: container ${PG_CONTAINER} absent — skipping"; return 0; }
  105. for db in "${PG_DBS[@]}"; do
  106. local dir="${LOCAL_BACKUP_ROOT}/${db}/${STAMP%/*}"
  107. local file="${dir}/${STAMP##*/}.dump.gz"
  108. mkdir -p "$dir"
  109. if docker exec "$PG_CONTAINER" pg_dump --username="$PG_USER" --format=custom --compress=0 \
  110. --no-owner --no-privileges "$db" 2>/dev/null | gzip -9 > "$file"; then
  111. log " ok pg_dump ${db}: $(stat -c%s "$file") bytes"
  112. else
  113. rm -f "$file"; fail "pg_dump failed: ${db}"
  114. fi
  115. done
  116. }
  117. # ===== PHASE 3: OFF-SITE — mirror the dolt backups volume to external S3 =====
  118. offsite_mirror() {
  119. if [ -z "${BRIVEN_BACKUP_S3_ENDPOINT:-}" ] || [ -z "${BRIVEN_BACKUP_S3_BUCKET:-}" ] \
  120. || [ -z "${BRIVEN_BACKUP_S3_ACCESS_KEY:-}" ] || [ -z "${BRIVEN_BACKUP_S3_SECRET_KEY:-}" ]; then
  121. log "off-site mirror skipped (BRIVEN_BACKUP_S3_* unset) — see BACKUP-OFFSITE.md"
  122. return 0
  123. fi
  124. local ep="${BRIVEN_BACKUP_S3_ENDPOINT#https://}"; ep="${ep#http://}"
  125. local vol="/var/lib/docker/volumes/${BACKUPS_VOLUME}/_data"
  126. [ -d "$vol" ] || { fail "off-site: backups volume path missing: ${vol}"; return 0; }
  127. log "off-site mirror → s3://${BRIVEN_BACKUP_S3_BUCKET}/doltgres-backups/"
  128. if docker run --rm -v "${vol}:/backups:ro" \
  129. -e "MC_HOST_off=https://${BRIVEN_BACKUP_S3_ACCESS_KEY}:${BRIVEN_BACKUP_S3_SECRET_KEY}@${ep}" \
  130. --entrypoint sh minio/mc:latest \
  131. -c "mc mirror --overwrite --remove /backups off/${BRIVEN_BACKUP_S3_BUCKET}/doltgres-backups/" >/dev/null 2>&1; then
  132. log "off-site mirror ok"
  133. else
  134. fail "off-site mirror failed"
  135. fi
  136. if [ -d "$LOCAL_BACKUP_ROOT" ]; then
  137. docker run --rm -v "${LOCAL_BACKUP_ROOT}:/pgd:ro" \
  138. -e "MC_HOST_off=https://${BRIVEN_BACKUP_S3_ACCESS_KEY}:${BRIVEN_BACKUP_S3_SECRET_KEY}@${ep}" \
  139. --entrypoint sh minio/mc:latest \
  140. -c "mc mirror --overwrite /pgd off/${BRIVEN_BACKUP_S3_BUCKET}/pg-dumps/" >/dev/null 2>&1 \
  141. || fail "off-site mirror (pg dumps) failed"
  142. fi
  143. }
  144. prune_local() {
  145. [ -d "$LOCAL_BACKUP_ROOT" ] || return 0
  146. log "pruning local pg dumps older than ${LOCAL_RETENTION_DAYS}d"
  147. find "$LOCAL_BACKUP_ROOT" -type f -name '*.dump.gz' -mtime +"$LOCAL_RETENTION_DAYS" -delete || true
  148. find "$LOCAL_BACKUP_ROOT" -type d -empty -delete || true
  149. }
  150. log "briven backup run starting (doltgres=${DC}, throttle=${THROTTLE}s)"
  151. dolt_backup_all # primary DR
  152. pg_dump_secondary # secondary rollback
  153. offsite_mirror # off-site (if configured)
  154. prune_local
  155. if [ "$FAILURES" -gt 0 ]; then
  156. log "ERROR: ${FAILURES} failure(s): ${FAILURE_DETAIL}"
  157. { echo "failures=${FAILURES}"; echo "detail=${FAILURE_DETAIL}"; } > /run/briven-backup-status
  158. exit 1
  159. fi
  160. rm -f /run/briven-backup-status
  161. log "briven backup run complete — all layers ok"