managarten/docs/BACKUP_STRATEGY.md
Till JS 670036d56d
Some checks are pending
CD Mac Mini / Detect Changes (push) Waiting to run
CD Mac Mini / Deploy (push) Blocked by required conditions
CI / Detect Changes (push) Waiting to run
CI / Validate (push) Waiting to run
CI / Build mana-search (push) Blocked by required conditions
CI / Build mana-sync (push) Blocked by required conditions
CI / Build mana-api-gateway (push) Blocked by required conditions
CI / Build mana-crawler (push) Blocked by required conditions
Mirror to Forgejo / Push to Forgejo (push) Waiting to run
docs: BACKUP_STRATEGY.md — current local-only setup + off-site plan
Dokumentiert:
- Was läuft (com.mana.backup-databases LaunchD, täglich 3 AM, ~45 GB)
- Was gebackupt wird (6 postgres-Container, 12 DBs)
- Was NICHT gebackupt wird (MinIO, Volumes-as-files, cloudflared-creds, ~/secrets/)
- Off-Site-Plan (R2/Hetzner/restic-Optionen, rclone-Skizze)
- Recovery-Drill (Quartals-Test mit pg_restore gegen Throwaway-Container)
- Pre-Live-Gate-Checkliste für Endurance-User-Daten

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:55:47 +02:00

4.7 KiB

BACKUP_STRATEGY — Postgres + Volumes

Stand 2026-05-13. Wiederhergestellt und erweitert, nachdem der LaunchD-Job 3 Monate stillgestanden hat (alter Pfad zeigte auf mana-monorepo/, das nicht mehr existiert).

Heute: lokales Backup, kein Off-Site

Was läuft

  • com.mana.backup-databases LaunchD-Job auf mana-server
  • Skript: ~/projects/managarten/scripts/mac-mini/backup-databases.sh
  • Schedule: täglich 03:00 (StartCalendarInterval)
  • Ziel: /Volumes/ManaData/backups/postgres/{daily,weekly}/
  • Retention: daily 7 Tage, weekly (Sonntag) 4 Wochen

Was wird gebackupt

Alle Postgres-Container, die *postgres* matchen, ausgenommen *exporter* und mana-infra-postgres-backup. Stand 2026-05-13:

Container User DB(s)
mana-infra-postgres postgres mana_platform, mana_sync, mana_admin, memoro (956 MB), forgejo, glitchtip, umami
cards-postgres cards cards
manaspur-postgres manaspur manaspur
nutriphi-postgres nutriphi nutriphi
zitare-postgres zitare zitare
chorportal-prod-postgres chorportal chorportal

Dump-Pattern: ${container}_${db}_${date}.sql.gz.

Total nach erstem Run 2026-05-13: ~45 GB in /Volumes/ManaData/backups/postgres.

Was NICHT gebackupt wird (heute)

  • MinIO-Objekte (Cards-Media, mana-media, …) — getrennte Volumes
  • /Volumes/ManaData/{cards,manaspur,…}/postgres auf File-Level — pg_dump reicht für DBs, aber Disk-Bitrot würde damit nicht erfasst
  • Cloudflared-Tunnel-Credentials unter ~/.cloudflared/ (kritisch!)
  • ~/secrets/ (App-Service-Keys, Master-Keys-Klartexte)

Off-Site — heute noch nicht aktiv

Problem: Mac Mini ist Single-Point-of-Failure. Defekte Disk, Diebstahl, Brand → alle Backups weg, weil sie auf der gleichen Disk liegen.

Aufgaben für die Off-Site-Strategie:

  1. Endpoint wählen:

    • Cloudflare R2 (S3-kompatibel, Vereins-tauglich Preis, mana hat schon Cloudflare-Account) ← Empfehlung
    • Hetzner Storage Box (günstig, EU-Hoster, Verein-Datenschutz- konform)
    • Wasabi / Backblaze B2 (günstig, US-Anbieter, DSGVO-fragwürdig)
    • eigener mana-server-2 (privater Off-Site-Mini, max Kontrolle)
  2. Tool wählen:

    • rclone (Multi-Provider, läuft auf macOS) ← Empfehlung
    • restic (Encryption + Dedup eingebaut, S3-fähig)
    • borg (klassisch, Repo-orientiert)
  3. Encryption: Daten verlassen den Mac Mini — ohne Encryption-at- transit-AND-at-rest verletzen wir die Vereins-Werte (Memoro/Cards/ Manaspur enthalten User-Inhalte). Empfehlung: rclone crypt als Wrapper, Key in ~/secrets/ und Off-Site-Recovery-Code im secret_offsite_backup_key-Memory.

Vorgeschlagener Aufbau (zu implementieren)

# rclone-Config mit verschlüsseltem Remote:
rclone config create r2-raw s3 provider Cloudflare \
    access_key_id <KEY> secret_access_key <SECRET> \
    endpoint <ACCOUNT>.r2.cloudflarestorage.com

rclone config create r2-encrypted crypt remote r2-raw:mana-backups \
    password <RANDOM> password2 <RANDOM2>

LaunchD-Job nach backup-databases.sh:

# scripts/mac-mini/backup-sync-offsite.sh
rclone sync /Volumes/ManaData/backups/postgres r2-encrypted:postgres \
    --transfers 4 --checkers 8 --log-file /tmp/mana-backup-offsite.log

Cron alle 6h, separate plist com.mana.backup-offsite.plist.

Pre-Live-Gate für mana-Plattform

Bevor manaspur-Endurance-User-Daten landen (siehe manaspur-native/docs/ENDURANCE_TEST.md):

  • Local-Backup-Job wieder aktiv (2026-05-13)
  • Off-Site-Endpoint provisioniert (R2-Bucket o. ä.)
  • rclone + Encryption-Setup
  • LaunchD-Job für Off-Site-Sync alle 6h
  • Recovery-Probe: zufällige Daily-Backup-Datei herunterladen + entschlüsseln + pg_restore-Trockenlauf gegen Test-DB

Recovery-Drill (Test, dass Backups wiederherstellbar sind)

Pro Quartal:

# Beispiel: cards-postgres aus Backup wiederherstellen
ssh mana-server
docker run --rm -d --name cards-postgres-restore-test \
    -e POSTGRES_PASSWORD=test -e POSTGRES_USER=cards -e POSTGRES_DB=cards \
    postgres:16-alpine

gunzip -c /Volumes/ManaData/backups/postgres/daily/cards-postgres_cards_2026-05-13.sql.gz \
    | docker exec -i cards-postgres-restore-test psql -U cards -d cards

# Probe-Query
docker exec cards-postgres-restore-test psql -U cards -d cards \
    -c "SELECT count(*) FROM cards.decks;"

docker stop cards-postgres-restore-test

Cross-Refs

  • secret_offsite_backup_key.md (Memory, kommt mit Off-Site-Setup)
  • mana/docs/PLAN.md § Backup-Strategy
  • scripts/mac-mini/backup-databases.sh — der eigentliche Code
  • ~/Library/LaunchAgents/com.mana.backup-databases.plist — LaunchD-Job