Zum Inhalt

Backup & Restore

Vesana schreibt drei Klassen von Daten, die du sichern musst:

Klasse Wo Wiederherstellbar ohne Backup?
Datenbank (Postgres) Compose-Volume pgdata Nein
Konfiguration DB + .env (Secrets) Bedingt — .env lässt sich mit neuem Setup-Script generieren, aber FIELD_ENCRYPTION_KEY ist unwiederbringlich
Wiki-Anhänge / Uploads Volume uploads Nein

FIELD_ENCRYPTION_KEY ist die einzige Sache, die wirklich nicht ersetzt werden kann

Verschlüsselte DB-Felder (z. B. SNMP-Communities) lassen sich ohne diesen Schlüssel niemals wieder entschlüsseln. Der Schlüssel muss außerhalb des Backup-Volumes liegen.

Automatisches Backup (Sidecar)

Vesana enthält einen optionalen Backup-Sidecar im Compose-Stack.

Aktivieren

docker compose -f /opt/vesana/docker-compose.prod.yml --profile backup up -d

Konfiguration

In .env:

Variable Default Bedeutung
BACKUP_SCHEDULE 0 2 * * * Cron-Ausdruck (täglich 02:00 UTC)
BACKUP_RETENTION_DAYS 7 Älter als X Tage werden gelöscht
BACKUP_API_TOKEN leer Optional: Admin-Token für zusätzlichen Config-Export

Nach Änderungen den Sidecar neu starten:

docker compose -f /opt/vesana/docker-compose.prod.yml restart backup

Was wird gesichert

  1. Datenbank-Dumppg_dump als komprimiertes SQL: backup-YYYYMMDD-HHMMSS.sql.gz
  2. Config-Export (wenn BACKUP_API_TOKEN gesetzt) — JSON-Snapshot von Tenants, Hosts, Profilen, Alert-Rules etc. via /api/v1/admin/export
  3. Compose-File-Snapshot — die zur Backup-Zeit aktive docker-compose.prod.yml

Alle Backups landen im Volume backups, gemountet auf /opt/vesana/backups.

Icon-Assets-Volume separat sichern

Das icon-assets-Volume (/data/icon-assets) wird nicht vom Standard-Sidecar erfasst — es enthält die hochgeladenen Hersteller-/Profil-/Host-Logos als Binär- Dateien. Bei einem Restore ohne dieses Volume kennen die DB-Tabellen icon_assets + icon_bindings zwar noch jedes Asset, aber die Datei fehlt → der API-Endpoint /api/v1/icons/{id}/file liefert 404 und das Frontend fällt auf Lucide-Icons zurück. Empfehlung: in dasselbe Backup-Skript aufnehmen, das auch das nginx-SSL-Volume sichert. Beispiel:

docker run --rm -v vesana_icon-assets:/src -v $(pwd):/dst alpine \
  tar czf /dst/icon-assets-$(date +%Y%m%d).tar.gz -C /src .

Inhalt prüfen

ls -la /opt/vesana/backups/
docker compose -f /opt/vesana/docker-compose.prod.yml run --rm backup ls -la /backups/

Manuelles Backup

Ohne Sidecar:

cd /opt/vesana

docker compose -f docker-compose.prod.yml exec postgres \
  pg_dump -U vesana vesana | gzip > backup-$(date +%Y%m%d-%H%M%S).sql.gz

Config-Export per API:

curl -H "Authorization: Bearer <ADMIN_JWT>" \
  https://deine-domain.tld/api/v1/admin/export \
  > config-export-$(date +%Y%m%d).json

Sicheres Aufbewahren

Inhalt Wohin Wer braucht Zugriff
pgdata-Backups Externes NAS, S3, Veeam, USB-Disk im Tresor Backup-Operator
FIELD_ENCRYPTION_KEY Passwort-Manager (Bitwarden, 1Password, KeePass) Lukas / Operations-Eigentümer
SECRET_KEY (JWT) Passwort-Manager nur bei Disaster-Recovery
.env als Ganzes Verschlüsselter Container, getrennt vom DB-Backup Backup-Operator

Keys nicht ins gleiche Backup wie die Datenbank

Wer die Datenbank UND den FIELD_ENCRYPTION_KEY zusammen besitzt, kann SNMP-Communities und alle anderen verschlüsselten Felder im Klartext lesen. Backup und Schlüssel räumlich/logisch trennen.

Restore

Datenbank

cd /opt/vesana

# 1. Schreibende Services stoppen
docker compose -f docker-compose.prod.yml stop api receiver worker

# 2. Backup einspielen
gunzip -c backup-YYYYMMDD-HHMMSS.sql.gz | \
  docker compose -f docker-compose.prod.yml exec -T postgres \
    psql -U vesana vesana

# 3. Services hochfahren
docker compose -f docker-compose.prod.yml up -d

Wenn der Restore in eine frische Instanz geht, vorher das pgdata-Volume leeren:

docker compose -f docker-compose.prod.yml down
docker volume rm vesana_pgdata
docker compose -f docker-compose.prod.yml up -d postgres
# warten bis postgres healthy
gunzip -c backup-YYYYMMDD-HHMMSS.sql.gz | \
  docker compose -f docker-compose.prod.yml exec -T postgres \
    psql -U vesana vesana
docker compose -f docker-compose.prod.yml up -d

Config-Import

curl -X POST \
  -H "Authorization: Bearer <ADMIN_JWT>" \
  -H "Content-Type: application/json" \
  -d @config-export.json \
  https://deine-domain.tld/api/v1/admin/import

Achtung: ein vollständiger Config-Import überschreibt bestehende Tenants und Hosts. Nur in eine leere Instanz importieren.

Disaster-Drill

Restore ohne regelmäßigen Test ist kein Backup, sondern Hoffnung. Empfohlen: einmal pro Quartal.

# 1. Test-Server / lokale VM mit aktuellem Backup
# 2. Aktuellsten Backup-Tarball + .env (mit FIELD_ENCRYPTION_KEY) bereitstellen
# 3. Setup-Script ausführen, dabei SECRET_KEY und FIELD_ENCRYPTION_KEY aus dem Original-.env übernehmen
# 4. pgdata leeren, gunzip-restore wie oben
# 5. Login mit Original-Admin-Account testen
# 6. Mindestens einen Host öffnen, einen Service mit verschlüsseltem snmp_community ansehen → muss funktionieren

Wenn Schritt 6 fehlschlägt (snmp_community erscheint maskiert obwohl Wert eigentlich da war), ist der FIELD_ENCRYPTION_KEY aus dem Backup nicht der richtige.

Empfohlene Strategie

  1. Backup-Sidecar aktiv für tägliche DB-Dumps
  2. FIELD_ENCRYPTION_KEY extern im Passwort-Manager + Print-Backup im Tresor
  3. Wöchentliche Off-Site-Kopie der Backup-Files (S3, NFS, USB-Rotation)
  4. Quartals-Drill auf einer separaten Maschine

Anschluss