Disaster Recovery für selbst betriebene Dienste: Unsere Backup-Strategie
Self-Hosting gibt Ihnen Kontrolle, die verwaltete Dienste nicht bieten: Sie wählen die Hardware, das Netzwerk, den geografischen Speicher- und Verarbeitungsort der Daten und die Kostenstruktur. Diese Kontrolle bringt jedoch eine Verantwortung mit sich, die verwaltete Dienste im Hintergrund übernehmen: Wenn etwas schiefgeht, liegt die Lösung vollständig bei Ihnen. Bei einer Installation auf einem einzelnen Server, auf dem mehrere Dienste im Produktivbetrieb laufen – Supabase, ein CRM, ein DAM und ein Git-Server –, ist die Backup- und Wiederherstellungsstrategie keine Nebensache. Sie entscheidet darüber, ob sich ein Vorfall beheben lässt oder Daten dauerhaft verloren gehen.
Dieser Beitrag dokumentiert die Backup-Strategie, die wir für eine selbst betriebene Produktivumgebung einsetzen. Die beteiligten Dienste sind ein Supabase-Stack mit PostgreSQL als primärem Datenspeicher, ein selbst betriebenes CRM (Twenty, das ebenfalls PostgreSQL nutzt), ein Gitea-Git-Server und ein System für Digital Asset Management. Alle laufen als Docker-Container auf einem einzigen Hetzner-VPS. Die Strategie muss alle Dienste abdecken, ohne unvertretbare Lastspitzen oder komplexe Abhängigkeiten bei der Orchestrierung zu erzeugen.
Warum ein einzelner Server das Risiko erhöht
Eine Installation auf einem einzelnen Server hat keine eingebaute Redundanz. Es gibt weder eine Standby-Replik noch ein Failover über mehrere Verfügbarkeitszonen oder eine automatische Datenbankwiederherstellung, die durch einen Health Check ausgelöst wird. Wenn der Server ausfällt – durch einen Hardwaredefekt, ein misslungenes Upgrade, ein Speicherproblem oder einen Sicherheitsvorfall –, bleibt nur die Wiederherstellung aus einem Backup. Ist das Backup unvollständig, veraltet oder ungetestet, sind die Wiederherstellungsmöglichkeiten entsprechend begrenzt.
Dieses Bedrohungsmodell unterscheidet sich von dem eines verwalteten Cloud-Dienstes, bei dem Hardwareausfälle weitgehend unbemerkt bleiben, weil der Infrastrukturanbieter das Failover übernimmt. Bei einem einzelnen VPS planen Sie ausdrücklich für die Ausfallszenarien, die verwaltete Dienste vor Ihnen verbergen. Diese Planung ist nicht besonders komplex, muss aber vor dem Vorfall erfolgen, statt währenddessen.
Was gesichert werden muss
Jeder Dienst hat ein eigenes Datenprofil. Um eine Backup-Strategie zu entwickeln, müssen Sie zunächst wissen, welche Daten unersetzlich sind und wo sie liegen.
Supabase speichert seine Daten in einer PostgreSQL-Instanz, die vom Supabase-Stack verwaltet wird. Die Datenbank enthält alle Anwendungsdaten, Benutzerdatensätze sowie die Authentifizierungs- und Speichermetadaten von Supabase selbst. Die Storage Buckets mit den hochgeladenen Dateien liegen auf der Festplatte und müssen separat vom Datenbank-Backup gesichert werden.
Twenty CRM nutzt eine eigene PostgreSQL-Instanz, getrennt von der Supabase-Instanz. Sie enthält Kontaktdatensätze, Daten zu Verkaufschancen und den Zustand der Workflows. Diese Datenbank ist kleiner, enthält aber besonders sensible Daten: Der Verlust von CRM-Daten beeinträchtigt die operativen Abläufe stärker als der Verlust von Anwendungsdaten, die sich erneut erzeugen lassen.
Gitea speichert Repositorys als Bare-Git-Objekte auf der Festplatte. Die Repository-Daten lassen sich grundsätzlich von den Arbeitsrechnern der Entwickler rekonstruieren, da ein vollständiger, nicht flacher Klon die von ihm abgerufene Historie enthält, jedoch keine Git-LFS-Objekte oder Referenzen, die er nie abgerufen hat. Die Daten des Issue-Trackers, Kommentare zu Pull Requests und die Teamkonfiguration liegen jedoch ausschließlich in der Gitea-Datenbank und sind in keinem Klon enthalten. Sowohl die Git-Objekte als auch die Gitea-Datenbank müssen gesichert werden.
DAM (Digital Asset Management) speichert Originaldateien, daraus erzeugte Varianten und Metadaten. Die Originaldateien sind unersetzlich; abgeleitete Varianten lassen sich neu erzeugen. Die Metadaten liegen in einer Datenbank, die Beziehungen zwischen Dateien, Tags und Nutzungsrechte erfasst.
pg_dump-Orchestrierung
pg_dump von PostgreSQL ist das passende Werkzeug für logische Datenbank-Backups. Es exportiert eine SQL-Darstellung der Datenbank, die in jeder kompatiblen PostgreSQL-Version wiederhergestellt werden kann. Damit ist es portabler als physische Backups, die dieselbe PostgreSQL-Version und dasselbe Binärformat erfordern.
Das Backup-Skript folgt für jede PostgreSQL-Instanz demselben Muster:
#!/usr/bin/env bash
set -euo pipefail
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DB_NAME="$1"
CONTAINER_NAME="$2"
BACKUP_DIR="/opt/backups/postgres"
OUTPUT_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"
mkdir -p "${BACKUP_DIR}"
docker exec "${CONTAINER_NAME}" pg_dump -U postgres -d "${DB_NAME}" | gzip -9 > "${OUTPUT_FILE}"
echo "Backup complete: ${OUTPUT_FILE} ($(du -sh "${OUTPUT_FILE}" | cut -f1))"
set -euo pipefail am Anfang ist wichtig: Es sorgt dafür, dass das Skript sofort beendet wird, wenn ein Befehl fehlschlägt, auch innerhalb einer Pipeline. Ohne pipefail würde ein fehlgeschlagenes pg_dump mit anschließend erfolgreichem gzip eine leere oder abgeschnittene komprimierte Datei hinterlassen (die Fehlermeldung wird auf stderr ausgegeben, nicht in die Datei geschrieben), während das Skript weiterhin Erfolg meldet. Das Problem würde erst bei der Wiederherstellung auffallen.
Die Ausgabe wird direkt mit gzip -9 komprimiert, statt erst in einem nachgelagerten Schritt. Das hält den Speicherbedarf gering und vermeidet es, zunächst einen unkomprimierten Dump auf die Festplatte zu schreiben. Das ist auf Servern wichtig, deren freier Speicherplatz gezielt verwaltet werden muss.
Gestaffelte Zeitplanung
Wenn alle Backup-Jobs gleichzeitig liefen, würden sie zeitweise um die I/O-Ressourcen konkurrieren und könnten die zu sichernden Dienste beeinträchtigen. Eine Supabase-Instanz unter Last profitiert nicht davon, mit einem pg_dump um Festplattenzugriffe zu konkurrieren. Die Lösung ist eine gestaffelte Zeitplanung: Jeder Backup-Job startet zu einem anderen Zeitpunkt, mit ausreichend Abstand, damit der vorherige Job abgeschlossen ist, bevor der nächste beginnt.
Unser Zeitplan als Cron-Einträge:
# Supabase PostgreSQL backup — 02:00 daily
0 2 * * * /opt/scripts/pg-backup.sh supabase supabase-db
# Twenty CRM PostgreSQL backup — 02:30 daily
30 2 * * * /opt/scripts/pg-backup.sh twenty twenty-db
# Gitea database backup — 03:00 daily
0 3 * * * /opt/scripts/pg-backup.sh gitea gitea-db
# DAM database backup — 03:30 daily
30 3 * * * /opt/scripts/pg-backup.sh dam dam-db
# Gitea repository objects — 04:00 daily
0 4 * * * /opt/scripts/git-objects-backup.sh
# Storage files (Supabase + DAM originals) — 04:30 daily
30 4 * * * /opt/scripts/files-backup.sh
Die Abstände von 30 Minuten zwischen den Jobs sind großzügig bemessen: Die meisten Datenbank-Backups sind bei Datenbanken dieser Größe in wenigen Minuten abgeschlossen. Die Abstände berücksichtigen aber auch den Upload nach jedem Backup. Die komprimierte Datei wird in den Objektspeicher hochgeladen, bevor der nächste Job beginnt, damit sich auf der lokalen Festplatte nicht gleichzeitig Backups mehrerer Tage ansammeln.
Upload in den Objektspeicher und Aufbewahrung
Backups, die nur auf demselben Server liegen wie die Dienste, die sie schützen sollen, sind keine Backups. Es sind Momentaufnahmen, die bei demselben Vorfall verloren gehen wie die geschützten Daten. Jedes Backup muss an einen separaten Speicherort hochgeladen werden, bevor die lokale Sicherung als abgeschlossen gelten kann.
Wir nutzen einen Anbieter für S3-kompatiblen Objektspeicher, der unabhängig vom VPS-Anbieter ist. Für den Upload verwenden wir rclone, das Wiederholungsversuche und die Prüfsummenprüfung nach der Übertragung automatisch übernimmt:
rclone copy "${OUTPUT_FILE}" "backup-remote:tva-backups/postgres/${DB_NAME}/" --checksum --transfers 1 --log-level INFO
Die Option --checksum lässt rclone Dateien anhand von Hashwert und Größe (MD5 bei S3-kompatiblen Zielen) statt anhand von Änderungszeit und Größe vergleichen. Zusammen mit der standardmäßigen Hashprüfung von rclone nach der Übertragung erkennt dies Übertragungsfehler, sofern das Ziel einen nutzbaren Hashwert speichert.
Ein separater Bereinigungsjob, der wöchentlich läuft, setzt die Aufbewahrungsregeln um: tägliche Backups werden sieben Tage aufbewahrt, wöchentliche Backups (das tägliche Backup vom Sonntag, das ein wöchentlicher Job umbenennt) vier Wochen und monatliche Backups (vom ersten Sonntag jedes Monats) sechs Monate. Das schafft ein angemessenes Zeitfenster, um Datenverluste zu erkennen, die nicht sofort auffallen. Eine vor drei Wochen beschädigte Zeile lässt sich so noch aus einem wöchentlichen Backup wiederherstellen.
Die Bereinigung verwendet rclone delete mit einem Filter nach Änderungszeit (--min-age), statt sich allein auf Dateinamen zu verlassen. Das ist zuverlässiger, wenn die Dateibenennung nicht vollständig einheitlich ist:
rclone delete "backup-remote:tva-backups/postgres/" --min-age 7d --include "*_daily_*" --dry-run
Beim Testen des Bereinigungsjobs wird die Option --dry-run verwendet. Entfernen Sie sie erst, wenn Sie bestätigt haben, dass die Filtermuster genau die zu löschenden Dateien erfassen. Das Muster für --include setzt voraus, dass tägliche Backups die Kennzeichnung _daily_ im Namen tragen. Das obige Backup-Skript benennt die Dateien jedoch nach dem Schema DB_TIMESTAMP.sql.gz. Passen Sie daher zuerst das Muster oder die Dateinamen an.
Wiederherstellungstests
Ein Backup, das noch nie getestet wurde, ist eine Hypothese. Ob es sich wiederherstellen lässt, wissen Sie nur, wenn Sie es tatsächlich wiederherstellen. Wir führen für jede Datenbank monatlich einen Wiederherstellungstest in einem temporären Docker-Container durch, der vom Produktiv-Stack isoliert ist:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_FILE="$1"
ROLES_FILE="${2:-}"
TEST_CONTAINER="restore-test-$(date +%s)"
# Start a temporary PostgreSQL container
docker run -d --name "${TEST_CONTAINER}" -e POSTGRES_PASSWORD=testpass -e POSTGRES_DB=testdb postgres:15-alpine
# Remove the container on exit, also when a step fails
trap 'docker rm -f "${TEST_CONTAINER}" > /dev/null' EXIT
# Wait for PostgreSQL to be ready
for _ in $(seq 1 30); do
docker exec "${TEST_CONTAINER}" pg_isready -h 127.0.0.1 -U postgres -q && break
sleep 1
done
# pg_dump omits roles: restore them first from a pg_dumpall --roles-only file (optional 2nd argument)
if [ -n "${ROLES_FILE}" ]; then
docker exec -i "${TEST_CONTAINER}" psql -U postgres -d testdb -q < "${ROLES_FILE}"
fi
# Restore the backup
zcat "${BACKUP_FILE}" | docker exec -i "${TEST_CONTAINER}" psql -U postgres -d testdb -v ON_ERROR_STOP=1
# Verify row counts match expectations
docker exec "${TEST_CONTAINER}" psql -U postgres -d testdb -c "SELECT schemaname, relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 10;"
Die Prüfung der Zeilenanzahl ist nicht umfassend: Sie kontrolliert, ob die wichtigsten Tabellen plausible geschätzte Zeilenanzahlen haben (n_live_tup ist ein statistischer Schätzwert, keine exakte Zählung), nicht, ob jede einzelne Zeile korrekt ist. Ein gründlicherer Test würde die Health Checks der Anwendung gegen die wiederhergestellte Datenbank ausführen. Die Prüfung der Zeilenanzahl erkennt jedoch die häufigsten Fehler: ein abgeschnittenes Backup, eine fehlgeschlagene Wiederherstellung mit leerer Datenbank oder eine Versionsinkompatibilität, die unbemerkt Datenverlust verursacht hat. Die Wiederherstellung läuft mit ON_ERROR_STOP=1, sodass die erste fehlgeschlagene Anweisung den Test scheitern lässt, und der trap entfernt den Container in jedem Fall. pg_dump enthält keine Rollen: Verweist der Dump auf andere Rollen als postgres, übergibt man die Ausgabe von pg_dumpall --roles-only vom Quellserver als zweites Argument (ein Fehler für die bereits vorhandene Rolle postgres ist in diesem Schritt zu erwarten). Das Test-Image sollte der PostgreSQL-Hauptversion der Quelle entsprechen. Der Supabase-Dump verweist zusätzlich auf Supabase-spezifische Schemas und Erweiterungen, die dem einfachen postgres-Image fehlen. Er braucht deshalb eine Testwiederherstellung in eine Supabase-Instanz nach Supabases eigenem Wiederherstellungsverfahren.
Wie die tatsächliche Wiederherstellung aussieht
Eine Backup-Strategie ist nur so nützlich wie das Wiederherstellungsverfahren, das sie nutzt. Das Betriebshandbuch zur Wiederherstellung liegt als Markdown-Dokument im Konfigurationsrepository des Servers und beschreibt die genaue Schrittfolge, um jeden Dienst auf einem neuen VPS wiederherzustellen.
Bei einem vollständigen Serverausfall lautet die Reihenfolge: einen neuen VPS bereitstellen, Docker und den Anwendungs-Stack aus dem Konfigurationsrepository installieren, die neuesten Backup-Dateien aus dem Objektspeicher herunterladen, die Datenbanken in der Reihenfolge ihrer Abhängigkeiten wiederherstellen (zuerst Supabase, dann CRM, dann die übrigen), den Dateispeicher wiederherstellen, DNS aktualisieren und jeden Dienst überprüfen. Die geschätzte Dauer für eine vollständige Wiederherstellung liegt unter zwei Stunden, sofern alle Backups aktuell sind und das Betriebshandbuch ohne Improvisation befolgt wird.
Entscheidend an dieser Schätzung ist „ohne Improvisation befolgt“. Das Betriebshandbuch sollte so konkret sein, dass auch jemand, der das System noch nie betreut hat, es erfolgreich ausführen kann. Befehle sollten sich kopieren und einfügen lassen, statt nur im Fließtext beschrieben zu sein. Jeder Schritt sollte eine Überprüfung enthalten. Unklarheiten im Betriebshandbuch sind ein Risiko, das unter dem Stress eines tatsächlichen Vorfalls noch größer wird.
Wir testen das Betriebshandbuch jährlich, indem wir eine vollständige Wiederherstellung in einer Staging-Umgebung durchführen. Dabei fällt zuverlässig mindestens ein Schritt auf, der sich seit der letzten Überarbeitung geändert hat: eine andere Docker-Image-Version, ein umbenannter Konfigurationsschlüssel oder eine hinzugefügte Umgebungsvariable. Solche Änderungen bei einem geplanten Test zu erkennen, ist deutlich besser, als sie während einer tatsächlichen Wiederherstellung zu entdecken.
Weiterführende Beiträge
- Ein mehrstufiges Sicherheitsaudit: Härtung einer Next.js-Anwendung – Sicherheitshärtung als Ergänzung zur Backup-Strategie, um die Wahrscheinlichkeit von Vorfällen zu verringern, die eine Wiederherstellung erforderlich machen.
- Eine App zur Prüfungsvorbereitung mit Tausenden von Fragen entwickeln – Supabase als Backend im Produktivbetrieb, einschließlich der Entscheidungen zu RLS und Schemadesign, die bestimmen, was ein Backup enthalten muss.