Skip to content

Backup & Restore — Ops-Leitfaden

Was das Framework zu Backup/Restore beitraegt, was der Operator tun muss, wie Recovery konkret ablaeuft. Keine Framework-Infrastruktur — dies ist ein Dokument plus das Core-Feature core-tenant-export.

Vier Ebenen

EbeneVerantwortungTools
DR (Disaster Recovery)OpsDB-native Backups (wal-g, mariabackup, SQLite snapshot)
Rollback (Migration/Incident)OpsPITR (Point-In-Time Recovery)
Tenant-Rollback (Kunde hat versemmelt)Ops + FrameworkPITR in Second-Instance + core-tenant-export
Data Portability (DSGVO)Frameworkcore-tenant-export

Framework-Constraints die Backup/Restore nicht brechen duerfen

Diese Regeln sind bereits im Framework umgesetzt und duerfen nicht aufgeweicht werden:

  1. Kein In-Memory-State der DB voraus ist. Alle State-Aenderungen gehen erst in die DB, dann ggf. in Caches/Events. Damit ist DB-PITR ausreichend — wiederhergestellter Stand ist konsistent. (Outbox-Transport ist genau dafuer da.)
  2. Deterministische, idempotente Migrations. Gleiche Migration zweimal anwenden muss dieselbe Tabelle ergeben. Erlaubt Rebuild aus Backup ohne Datensalat.
  3. Keine Schema-Divergenz pro Tenant. Single-Schema mit tenantId-Filter — Backup ist eine Tabelle pro Entity, nicht N pro Tenant.
  4. Alle Secrets mit kekVersion. Backup traegt Envelope inkl. Version. Restore-Ziel muss denselben KEK kennen (oder gezielt anderen — siehe unten).
  5. Keine “hidden” Seiteneffekte ausserhalb der DB ausser bewusst-designten (Meilisearch-Index, File-Storage — beide haben eigenen Backup-Pfad und sind re-buildbar aus DB).

Setup pro Dialect

PostgreSQL (empfohlen fuer Prod)

Empfehlung: Managed-PG mit PITR (Neon, Supabase, AWS RDS, Crunchy). Die machen wal-g/pg_basebackup + WAL-Archiving korrekt und haben Recovery-UX.

Self-hosted:

Terminal window
# Kontinuierliches WAL-Archiving nach S3
wal-g backup-push /var/lib/postgresql/data
wal-g wal-push /var/lib/postgresql/wal/...
# Recovery auf PITR-Zeitpunkt
wal-g backup-fetch /var/lib/postgresql/data LATEST
# postgres.conf: recovery_target_time = '2026-04-14 12:34:56'

Retention: 30 Tage Full + WAL laufend. Config via wal-g ENV.

MariaDB / MySQL

Tool: mariabackup (bei MariaDB) oder xtrabackup (MySQL).

Terminal window
# Full Backup
mariabackup --backup --target-dir=/backup/full-$(date +%F)
# Incremental
mariabackup --backup --incremental --target-dir=/backup/inc-$(date +%F)
# Binlog fuer PITR aktiviert haben:
# my.cnf: log_bin = mysql-bin, expire_logs_days = 14, binlog_format = ROW
# Recovery:
mariabackup --prepare --target-dir=/backup/full-...
mysqlbinlog --start-datetime="..." --stop-datetime="..." mysql-bin.* | mysql

SQLite

Tool: VACUUM INTO oder file-copy bei gestopptem Writer.

Terminal window
sqlite3 app.db "VACUUM INTO '/backup/app-$(date +%F).db'"
# Oder Litestream fuer kontinuierliches Replication zu S3

PITR ist mit Litestream moeglich. Ohne Litestream: tägliche Snapshots als Minimum.

File-Storage-Backup

Files (via core-files) werden separat gesichert:

S3-kompatibel (empfohlen): S3 Versioning aktivieren + Lifecycle-Policy (NoncurrentVersionExpiration: 30 days). Kontinuierliche Versionierung ohne Extra-Job.

Alternative: Daily aws s3 sync auf Backup-Bucket.

Search-Index (Meilisearch)

Nicht backup-pflichtig — Re-Indexable aus DB in wenigen Minuten (abhaengig von Datenmenge). Recovery-Runbook enthaelt Re-Index-Schritt.

Recovery-Runbook

Szenario A: Total-Verlust (DR)

  1. Neue DB-Instanz provisionieren (gleiche Version, selber Dialect)
  2. Letztes Full-Backup einspielen (pg_basebackup / mariabackup / VACUUM INTO)
  3. WAL/Binlog bis Ziel-Zeitpunkt abspielen (oder bis zum aktuellen Zeitpunkt)
  4. App-Server mit neuer DB verbinden
  5. Master-KEK im Env setzen (gleicher Wert wie vorher — sonst keine Secrets/PII lesbar)
  6. Search-Index reindexen (yarn kumiko reindex-all)
  7. File-Storage-Bucket verfuegbar machen (Pointer auf S3-Backup ggf. swappen)
  8. Outbox-Replay: Poller anwerfen, Stau abarbeiten
  9. Smoke-Tests

Szenario B: Migration kaputt / Rollback binnen 1h

  1. PITR auf T-1h (vor Migration)
  2. Wie A, Schritt 3-9
  3. Fehlerhafte Migration fixen, neu deployen

Szenario C: Einzel-Tenant hat versemmelt (Tenant-Rollback)

Ohne per-tenant PITR (haben wir nicht) laeuft das so:

  1. PITR der gesamten DB in zweite Instanz (Staging-Hardware)
  2. Dort yarn kumiko backup export <tenantId> ausfuehren → Bundle mit T-xh-Zustand
  3. Auf Prod: nur den Ziel-Tenant rollbacken mittels tenantImport.replace(bundle, tenantId) mit dryRun → Diff ueberpruefen, dann confirm
  4. Search-Reindex nur fuer diesen Tenant
  5. Audit-Eintrag “catastrophic tenant replacement” ist automatisch geschrieben

Kosten: ~1h Second-Instance-Zeit, manuelle Sorgfalt. Vorteil: andere Tenants unberuehrt.

Szenario D: DSGVO-Portability

Tenant-Admin klickt “Daten exportieren” → Framework macht tenantExport.create() → verschluesseltes Bundle wird per Download oder Email zugestellt. Entschluesselungs-Key gesondert (siehe core-tenant-export).

Test-Strategie

Ein Backup das nie getestet wurde ist kein Backup.

  • Monthly Drill: vollen Restore auf Staging durchspielen, Smoke-Tests
  • yarn kumiko backup verify <file> als Pflicht-Schritt nach jedem Prod-Backup — validiert Bundle-Integritaet, Schema, Counts, Decrypt-Round-trip

Was das Framework NICHT macht

  • Eigenes DB-Backup-Tool bauen. PG/MariaDB/SQLite haben jahrzehntelang erprobte Tools.
  • Automatisierte PITR-Verwaltung. Operator-Verantwortung.
  • Cross-Dialect-Restore (PG-Backup → MariaDB-Restore). Weder portabel noch noetig.
  • Tenant-level CDC oder Log-Tailing. Zu komplex fuer v1; Second-Instance-Trick funktioniert.