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
| Ebene | Verantwortung | Tools |
|---|---|---|
| DR (Disaster Recovery) | Ops | DB-native Backups (wal-g, mariabackup, SQLite snapshot) |
| Rollback (Migration/Incident) | Ops | PITR (Point-In-Time Recovery) |
| Tenant-Rollback (Kunde hat versemmelt) | Ops + Framework | PITR in Second-Instance + core-tenant-export |
| Data Portability (DSGVO) | Framework | core-tenant-export |
Framework-Constraints die Backup/Restore nicht brechen duerfen
Diese Regeln sind bereits im Framework umgesetzt und duerfen nicht aufgeweicht werden:
- 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.)
- Deterministische, idempotente Migrations. Gleiche Migration zweimal anwenden muss dieselbe Tabelle ergeben. Erlaubt Rebuild aus Backup ohne Datensalat.
- Keine Schema-Divergenz pro Tenant. Single-Schema mit
tenantId-Filter — Backup ist eine Tabelle pro Entity, nicht N pro Tenant. - Alle Secrets mit
kekVersion. Backup traegt Envelope inkl. Version. Restore-Ziel muss denselben KEK kennen (oder gezielt anderen — siehe unten). - 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:
# Kontinuierliches WAL-Archiving nach S3wal-g backup-push /var/lib/postgresql/datawal-g wal-push /var/lib/postgresql/wal/...
# Recovery auf PITR-Zeitpunktwal-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).
# Full Backupmariabackup --backup --target-dir=/backup/full-$(date +%F)
# Incrementalmariabackup --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.* | mysqlSQLite
Tool: VACUUM INTO oder file-copy bei gestopptem Writer.
sqlite3 app.db "VACUUM INTO '/backup/app-$(date +%F).db'"# Oder Litestream fuer kontinuierliches Replication zu S3PITR 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)
- Neue DB-Instanz provisionieren (gleiche Version, selber Dialect)
- Letztes Full-Backup einspielen (pg_basebackup / mariabackup / VACUUM INTO)
- WAL/Binlog bis Ziel-Zeitpunkt abspielen (oder bis zum aktuellen Zeitpunkt)
- App-Server mit neuer DB verbinden
- Master-KEK im Env setzen (gleicher Wert wie vorher — sonst keine Secrets/PII lesbar)
- Search-Index reindexen (
yarn kumiko reindex-all) - File-Storage-Bucket verfuegbar machen (Pointer auf S3-Backup ggf. swappen)
- Outbox-Replay: Poller anwerfen, Stau abarbeiten
- Smoke-Tests
Szenario B: Migration kaputt / Rollback binnen 1h
- PITR auf T-1h (vor Migration)
- Wie A, Schritt 3-9
- Fehlerhafte Migration fixen, neu deployen
Szenario C: Einzel-Tenant hat versemmelt (Tenant-Rollback)
Ohne per-tenant PITR (haben wir nicht) laeuft das so:
- PITR der gesamten DB in zweite Instanz (Staging-Hardware)
- Dort
yarn kumiko backup export <tenantId>ausfuehren → Bundle mit T-xh-Zustand - Auf Prod: nur den Ziel-Tenant rollbacken mittels
tenantImport.replace(bundle, tenantId)mitdryRun→ Diff ueberpruefen, dann confirm - Search-Reindex nur fuer diesen Tenant
- 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.