Runbook: Off-Site-Backup (restic + backrest → Strato HiDrive S3)
Ziel: Bei VPS-Totalverlust ist alles aus einer verschlüsselten, versionierten, externen Quelle wiederherstellbar. Eingeführt mit SILV-185 (11.06.2026).
Additiv, nicht ersetzend. Die lokalen Backups bleiben der schnelle Restore-Pfad:
silverscale.db.backup03:30 (7-Tage-Wochentag-Rotation) + Planepg_dump+MinIO-Tar 03:35 (app/agents/plane-backup.sh). restic/backrest ist der DR-Pfad off-site.
Architektur (privilegienarm — kein docker.sock)
03:30 silverscale.db → app/data/backups/silverscale-{1..7}.db (lokal, konsistent)
03:35 plane-backup.sh → /home/dennis/plane/backups/*.sql.gz,*.tar.gz (lokal, atomar)
03:50 backrest (im Container, eigener Scheduler)
→ restic backup /userdata → s3:s3.hidrive.strato.com/silverscale-backup/main
→ Hook (CONDITION_SNAPSHOT_SUCCESS): app/data/offsite-last-backup.txt (ISO-Heartbeat)
backrest spiegelt die bereits konsistent erzeugten lokalen Dumps + Secrets als read-only File-Mounts (kein direkter DB-Zugriff, kein docker.sock). restic verschlüsselt + dedupliziert + inkrementell. Retention 7d/4w/12m, Prune + Check monatlich (1. des Monats 04:00/05:00).
Was gesichert wird (Container-Pfad /userdata)
| Quelle (Host) | Mount | Inhalt |
|---|---|---|
./app/data (rw¹) |
/userdata/app-data |
DB-Day-Files (backups/), HF-/eGYM-/Garmin-Marker, APNs-.p8 |
/home/dennis/plane/backups (ro) |
/userdata/plane-backups |
Plane Postgres-Dump + MinIO-Uploads |
./pocket-id-data (ro) |
/userdata/pocket-id |
Pocket-ID-DB + Uploads |
./.env (ro) |
/userdata/secrets/root.env |
POCKET_ID_ENCRYPTION_KEY, VAPID, alle Secrets |
./lidl-grocy/.env (ro) |
/userdata/secrets/lidl.env |
LIDL_REFRESH_TOKEN |
/home/dennis/plane/plane.env (ro) |
/userdata/secrets/plane.env |
Plane-Postgres-Passwort |
./traefik/acme.json (ro) |
/userdata/secrets/acme.json |
Let's-Encrypt-Zertifikate |
¹ app/data rw nur, damit der Erfolgs-Hook den Heartbeat schreiben kann. Die Live-DB
(silverscale.db/-wal/-shm) ist im Plan exkludiert — gesichert wird das konsistente
03:30-Day-File, nicht die offene Datei.
Secrets / Zugänge (NICHT im Repo)
- S3-Creds + neues
RESTIC_PASSWORDstehen in der gitignored Repo-Wurzel-.env(AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,RESTIC_PASSWORD). backrest bekommtRESTIC_PASSWORDalsRESTIC_REPO_PWins Env (Issue #1139: ein rohesRESTIC_PASSWORDim Prozess-Env würde das Config-Passwort still überschreiben → Schutz-Sperre). - backrest-Config
./backrest/config/config.json(gitignored): Passwort-Feld leer,RESTIC_PASSWORD=${RESTIC_REPO_PW}in der repo-env-Liste (dort expandiert backrest${VAR}, im Passwort-Feld nicht). Auth aktiv (dennis, bcrypt-Hash in der Config). - ⚠️
RESTIC_PASSWORDMUSS zusätzlich off-VPS liegen (Dennis' Passwort-Manager). Ohne das Passwort ist das verschlüsselte Repo bei VPS-Tod unentschlüsselbar (Chicken-Egg).
UI / Zugriff
backrest-Web-UI: http://127.0.0.1:9898 bzw. über WireGuard http://192.168.3.8:9898
(kein öffentlicher Traefik-Router — Admin-Tool, WG/127.0.0.1-only). Login: dennis.
Health / Observability (kein blindes Silo)
Der Erfolgs-Hook schreibt app/data/offsite-last-backup.txt (ISO). /api/status liefert
last_offsite_backup_at + last_offsite_backup_age_s + Schwelle offsite_backup_s (36 h).
Der Maintenance-Sweep (03:40, SILV-139) alarmiert bei Stillstand (>30 h) per Push + Bus —
deckt S3-Ausfall, abgelaufene Creds und einen toten backrest gleichermaßen ab.
Restore (getestet 11.06.2026 — integrity_check: ok, .env byte-identisch)
# Passwort aus der .env; Repo + Creds kommen aus dem backrest-Container-Env.
RPW=$(grep '^RESTIC_PASSWORD=' /home/dennis/silverscale/.env | cut -d= -f2-)
REPO="s3:s3.hidrive.strato.com/silverscale-backup/main"
# Snapshots listen
docker exec backrest sh -c "RESTIC_PASSWORD='$RPW' RESTIC_REPOSITORY='$REPO' /bin/restic snapshots"
# Kompletten letzten Snapshot zurückholen
docker exec backrest sh -c "RESTIC_PASSWORD='$RPW' RESTIC_REPOSITORY='$REPO' \
/bin/restic restore latest --target /tmp/restore"
# Einzelne Datei (z. B. die DB von Dienstag) gezielt holen
docker exec backrest sh -c "RESTIC_PASSWORD='$RPW' RESTIC_REPOSITORY='$REPO' \
/bin/restic restore latest --target /tmp/restore --include /userdata/app-data/backups/silverscale-2.db"
Wiederanlauf-Reihenfolge bei VPS-Tod (Skizze → SILV-134 macht daraus das volle DR-Runbook):
1. Neuer Host, Docker, Repo klonen. RESTIC_PASSWORD + S3-Creds aus dem Passwort-Manager → .env.
2. restic restore latest → secrets/* zurück (.env, plane.env, acme.json, lidl).
3. pocket-id/ + app-data/ zurücklegen; jüngstes silverscale-{DOW}.db → app/data/silverscale.db.
4. Plane: jüngsten plane-pg-*.sql.gz in eine frische Plane-PG laden, plane-uploads-*.tar.gz ins MinIO-Volume.
5. docker compose up -d; Pocket-ID prüft Login; App /healthz grün.
Troubleshooting
wrong password or no key found→ Passwort-Feld der Config muss leer sein,RESTIC_PASSWORD=${RESTIC_REPO_PW}in der repo-env-Liste (Expansion gilt nur dort).guid is required … auto_initialize→"autoInitialize": trueam Repo.- backrest startet nicht, #1139 → keine rohe
RESTIC_PASSWORD-Variable ins Container-Env (eigener NameRESTIC_REPO_PW). - Backup manuell auslösen (statt 03:50 zu warten): Login → JWT →
POST /v1.Backrest/Backupmit{"value":"silverscale-nightly"}.