gabriel / musehub public
backup.sh bash
74 lines 3.1 KB
Raw
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor ⚠ breaking 1 day ago
1 #!/usr/bin/env bash
2 # Postgres daily backup script — run via cron on the EC2 instance.
3 #
4 # Two-tier backup strategy:
5 # 1. Local dump → /opt/backups/musehub/ (fast restore, 14-day rotation)
6 # 2. R2 upload → r2://BACKUP_R2_BUCKET/musehub-db/ (off-disk, long retention)
7 #
8 # R2 upload requires:
9 # - rclone installed: sudo apt-get install rclone
10 # - rclone configured with an R2 remote named "r2":
11 # rclone config (add remote → S3-compatible → Cloudflare R2)
12 # - BACKUP_R2_BUCKET set in /opt/musehub/.env, e.g.:
13 # BACKUP_R2_BUCKET=musehub-backups
14 # If BACKUP_R2_BUCKET is unset or rclone is not installed, the script
15 # continues with local-only backup and emits a warning.
16 #
17 # Install (on EC2):
18 # sudo mkdir -p /opt/backups/musehub
19 # sudo chown ubuntu:ubuntu /opt/backups/musehub
20 # chmod +x /opt/musehub/deploy/backup.sh
21 # crontab -e
22 # # Add this line (runs daily at 3 AM):
23 # 0 3 * * * /opt/musehub/deploy/backup.sh >> /var/log/musehub-backup.log 2>&1
24
25 set -euo pipefail
26
27 APP_DIR="/opt/musehub"
28 BACKUP_DIR="/opt/backups/musehub"
29 TIMESTAMP=$(date +%Y%m%d_%H%M%S)
30 BACKUP_FILE="$BACKUP_DIR/musehub_${TIMESTAMP}.sql.gz"
31 RETAIN_DAYS=14
32
33 mkdir -p "$BACKUP_DIR"
34
35 echo "[$(date)] Starting backup..."
36
37 # Load DB_PASSWORD and optional BACKUP_R2_BUCKET from the app's .env
38 DB_PASSWORD=$(grep '^DB_PASSWORD=' "$APP_DIR/.env" | cut -d'=' -f2-)
39 BACKUP_R2_BUCKET=$(grep '^BACKUP_R2_BUCKET=' "$APP_DIR/.env" 2>/dev/null | cut -d'=' -f2- || true)
40
41 sudo docker compose -f "$APP_DIR/docker-compose.yml" exec -T postgres \
42 env PGPASSWORD="$DB_PASSWORD" \
43 pg_dump -U musehub musehub \
44 | gzip > "$BACKUP_FILE"
45
46 echo "[$(date)] Backup written: $BACKUP_FILE ($(du -sh "$BACKUP_FILE" | cut -f1))"
47
48 # ── Off-disk: sync to Cloudflare R2 ──────────────────────────────────────────
49 # Keeps backups on a separate storage medium — survives disk failure on the
50 # EC2 instance. rclone copy is idempotent (skips already-uploaded files).
51 if [[ -n "${BACKUP_R2_BUCKET:-}" ]] && command -v rclone &>/dev/null; then
52 echo "[$(date)] Syncing backup to R2 bucket: ${BACKUP_R2_BUCKET}..."
53 rclone copy "$BACKUP_FILE" "r2:${BACKUP_R2_BUCKET}/musehub-db/" \
54 --s3-chunk-size=128M \
55 --s3-upload-concurrency=4 \
56 --stats=30s
57 echo "[$(date)] R2 upload complete."
58
59 # Remove R2 copies older than 90 days (long-term retention).
60 rclone delete "r2:${BACKUP_R2_BUCKET}/musehub-db/" \
61 --min-age=90d \
62 --include "musehub_*.sql.gz" || true
63 echo "[$(date)] R2 old-backup rotation complete."
64 else
65 echo "[$(date)] WARNING: BACKUP_R2_BUCKET not set or rclone not installed — local-only backup."
66 echo "[$(date)] To enable off-disk backups: install rclone, configure an R2 remote,"
67 echo "[$(date)] and set BACKUP_R2_BUCKET=<your-bucket-name> in $APP_DIR/.env"
68 fi
69
70 echo "[$(date)] Removing local backups older than $RETAIN_DAYS days..."
71 find "$BACKUP_DIR" -name "musehub_*.sql.gz" -mtime "+$RETAIN_DAYS" -delete
72
73 echo "[$(date)] Backup complete. Local files kept:"
74 ls -lh "$BACKUP_DIR"
File History 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago