24 Commits

Author SHA1 Message Date
renovate 157480b83e chore(deps): update minor-and-patch-updates 2026-06-03 10:20:55 +00:00
Micha 14de2f4801 docs(restore): komodo mongo restore successful, update matrix and backlog
Komodo-Mongo-Daten-Restore am 2026-06-03 erfolgreich: mongorestore
von komodo-mongo.archive.gz in Wegwerf-Mongo, 86904 Dokumente
(inkl. 32 Stack-Definitionen). Damit ist die kanonische Quelle fuer
KOMODO_*-Stack-ENV-Werte im DR-Fall als wiederherstellbar belegt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:25:32 +02:00
Micha 90d1595285 fix(restore): komodo mongo restore own compose to avoid container name collision
Zweiter Lauf scheiterte mit Auth-Failure weil der Container-Name
restoretest-komodo-mongo mit dem alten Bootstrap-Test kollidierte
(stale Datadir auf shfs mit anderen Credentials).

Fix: eigenes Compose mit eigenem Container-Namen
(restoretest-komodo-mongorestore) und eigenem Project-Name, damit
keine Namenskollision mit dem bestehenden Bootstrap-Test entsteht.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:23:17 +02:00
Micha c1985e177b fix(restore): komodo mongorestore --noIndexRestore for auth compat
Erstlauf 2026-06-03: 86904 Dokumente (inkl. 32 Stack-Dokumente)
erfolgreich restored, aber Exit 1 weil der Index-Rebuild mit
"Command createIndexes requires authentication" scheitert (Test-User
hat keine dbAdmin-Rolle).

Fix: --noIndexRestore. Fuer den Smoke-Zweck (Stack-Definitionen lesbar,
KOMODO_*-ENV-Werte rekonstruierbar) reicht das. Indexe werden bei einem
echten Komodo-Restart ohnehin neu aufgebaut.

Nebenbefund: produktive Mongo ist 8.0.23, Test-Compose pinnt 7.0.32.
Cross-Version-Warning ist fuer den Lesetest harmlos, aber der
Bootstrap-Compose-Pin sollte separat auf 8.0 nachgezogen werden.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:20:53 +02:00
Micha a244f2d677 feat(restore): komodo mongo data restore test
Neuer Test: mongorestore von komodo-mongo.archive.gz in eine frische
Wegwerf-Mongo. Beweist, dass die Stack-Definitionen und damit die
KOMODO_*-Stack-ENV-Werte aus dem Dump rekonstruiert werden koennen
(kanonische Quelle laut docs/DISASTER_RECOVERY.md 6.2.1).

Machbarkeit vorab verifiziert: Dump 6.0M auf Host vorhanden,
mongorestore im mongo:7.0.32-Image verfuegbar, shfs-Write funktioniert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:18:39 +02:00
Micha ef032f2dde docs(restore): document nextcloud shfs-chmod blocker
Nextcloud-Restore-Test Erstlauf 2026-06-03 nach 5 Iterationen als
strukturell blockiert durch Unraid shfs/FUSE eingestuft.

Ursache: Nextcloud 33 fuehrt zur Laufzeit chmod() auf Dateien unter
/var/www/html aus (OC_Util.php#486). Auf Unraids FUSE/shfs User Shares
ist chmod nicht moeglich - weder vom Host (chown ignoriert) noch aus dem
Container (Operation not permitted), auch nicht ohne no-new-privileges.
In Produktion funktioniert Nextcloud, weil die Daten dort auf einem
Cache-Drive (XFS/BTRFS direkt) statt ueber shfs liegen.

Scaffold (Skript + Compose) bleibt im Repo als Ausgangspunkt fuer die
Loesung. Drei Optionen dokumentiert:
a) Restore-Lab auf Cache-Drive
b) Docker-Volumes statt Bind-Mounts
c) tmpfs + rsync

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:14:39 +02:00
Micha 6fec64d0a1 fix(restore): nextcloud dump from host path instead of borg extract
Erstlauf 2026-06-03: borg_extract fuer den Nextcloud-Dump scheiterte
still (Pfad local/borg-dumps/latest/nextcloud.dump existiert im
Archiv moeglicherweise unter einem anderen Prefix). Der Dump liegt
taeglich frisch auf dem Host unter /mnt/user/backups/borg/dumps/latest/
und wird von dort in Borg gesichert - der Smoke-Wert ist identisch.

HTML (App-Code + config) kommt weiterhin aus dem Borg-Archiv.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 11:03:45 +02:00
Micha 5d1ae68705 fix(restore): nextcloud permissions on unraid shfs (no-new-privileges removal)
Zweiter Erstlauf 2026-06-03 scheiterte weiterhin mit 503, obwohl
config.php korrekt gepatcht war.

Ursache: Unraid's FUSE/shfs-Dateisystem auf User-Shares ignoriert
chown -R 33:33 still — Dateien bleiben bei sshd:sshd. Der
Nextcloud-Entrypoint versucht intern chmod/chown auf /var/www/html und
/var/www/html/data, was mit no-new-privileges:true blockiert wird.

Fix:
- no-new-privileges vom restoretest-nextcloud Container entfernt,
  damit der Entrypoint Rechte im Container selbst setzen kann
  (Test-Postgres und Test-Redis behalten no-new-privileges)
- Host-seitiger chown durch chmod a+rwX ersetzt (funktioniert auf shfs)
- Vertretbar im isolierten Smoke-Kontext (127.0.0.1, Wegwerf-Daten,
  kein Traefik)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 10:55:56 +02:00
Micha 2913e1005f fix(restore): nextcloud chown 33:33 for www-data after borg extract
Erstlauf 2026-06-03 scheiterte mit dauerhaft 503. config.php-Patching
(Redis-Host + trusted_domains) war korrekt, aber Nextcloud konnte die
restaurierten Dateien nicht lesen/schreiben: "chmod(): Operation not
permitted at OC_Util.php#486".

Ursache: Borg-Extract ueber den borg-ui Container legt Dateien mit dem
borg-ui-User (sshd o.ae.) an. Nextcloud im Container laeuft als
www-data (UID 33). Mit no-new-privileges:true scheitert jeder chmod/
chown-Versuch im Container.

Fix: chown -R 33:33 auf html/ und data/ nach dem Extract, bevor der
Nextcloud-Container startet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 10:44:12 +02:00
Micha 6f0e6f0d5a fix(restore): nextcloud config.php patching for redis host and trusted_domains
Erstlauf 2026-06-03 scheiterte mit 503: Redis-Host war noch auf dem
produktiven 'nextcloud-redis' statt 'restoretest-nextcloud-redis', und
trusted_domains enthielt kein 127.0.0.1 (Nextcloud blockt mit
"Access through untrusted domain").

Ursache: das sed-Pattern fuer Redis versuchte den ganzen Array-Block
einzeilig zu ersetzen, traf aber das PHP-Mehrzeilenformat nicht. Und
das trusted_domains-sed fand das Schliessmuster nicht zuverlaessig.

Fix:
- Redis-Host separat per sed patchen (nur den 'host'-Wert im Block)
- trusted_domains per PHP-CLI rewrite (robuster als sed auf PHP-Arrays)
- Fallback auf sed fuer Hosts ohne php

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 10:34:30 +02:00
Micha f473fbaa8b feat(restore): nextcloud restore smoke test scaffold
Nextcloud-Restore-Test nach dem Muster der anderen Restore-Smokes:
- Borg-Extract von html (App-Code + config.php) und nextcloud.dump
- pg_restore in isoliertes Test-Postgres (mit Retry-Schleife)
- config.php wird im Restore-Lab auf Test-DB-Credentials gepatcht
  (produktive Secrets werden nicht gemountet)
- Nextcloud startet gegen restaurierte Daten + Test-Redis
- Smoke prueft HTTP /status.php und occ status (maintenance mode)
- Produktive Nutzdaten unter /mnt/user/documents/nextcloud-data
  werden bewusst NICHT gemountet (zu gross fuer regelmaessigen Smoke)

Erster Lauf steht aus und braucht Operator-Freigabe auf dem Host.

Dispatcher und ntfy-Wrapper um Nextcloud erweitert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 10:05:10 +02:00
Micha c922d1f241 docs(restore): finalize audit - handbook update, reifegrad matrix, backlog
Schliesst das Restore-Skills-Audit 2026-06-02/03 ab:

- RESTORE_HANDBOOK.md auf Stand 2026-06-03: alle 6 verifizierten Tests
  (Vaultwarden, Gitea, Paperless, Immich, Authelia, Komodo-Bootstrap)
  dokumentiert, Frequenz-Tabelle aktualisiert, Betriebsmodus auf V1+
  (mit ntfy), Schnellstart um Immich/Authelia/Komodo ergaenzt,
  Report-Aufbewahrungsregel dokumentiert, Ausbaustufen priorisiert.

- RESTORE_MATRIX.md: neue Sektion "Restore-Test-Reifegrad" mit
  Uebersichtstabelle (pro Dienst: Tier, letzter Test, Typ, naechster
  Lauf) und priorisierter Kandidatenliste fuer fehlende Tests.

- Gitea-Restore: SSH-Check im Report korrekt als "TCP connect only"
  benannt statt "SSH port open" (war Audit-Finding M3).

- AUDIT_2026-05-25_TODO.md: Restore-Audit-Backlog ergaenzt mit den
  verbleibenden 8 offenen Punkten (Nextcloud, Shared PG18, Komodo-Mongo,
  Mailarchiver, Mealie, Traefik, Negativ-Test, E2E-DR-Drill).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 09:31:19 +02:00
Micha ba3ef8fcfc docs(restore): mark authelia smoke successful and schedule 2026-06-03 08:55:04 +02:00
Micha 52fc007123 fix(restore): authelia smoke without dump-restore, drop bogus env, disable ntp
Erstlauf 2026-06-03 hat einen by-design-Konflikt offengelegt: pg_restore des
produktiven postgresql17-authelia.dump in eine Test-Instanz mit Wegwerf
AUTHELIA_STORAGE_ENCRYPTION_KEY scheitert im Authelia-Startup-Check mit
"the configured encryption key does not appear to be valid for this database".
Productive Storage-Werte werden mit dem produktiven Key verschluesselt; ein
Wegwerf-Key kann sie nicht entschluesseln. Smoke ist deshalb explizit auf
Config-Restore + Boot reduziert, nicht Daten-Decrypt.

Zwei Nebenbefunde aus demselben Lauf:
- AUTHELIA__SERVER__ADDRESS (Doppel-Underscore) wurde von Authelia 4.39
  abgelehnt ("configuration environment variable not expected"). ENV
  entfernt; server.address kommt eh aus der generierten configuration.yml.
- ntp-Startup-Check schlug fehl ("Could not determine the clock offset
  ... lookup time.cloudflare.com on 127.0.0.1:53: server misbehaving"),
  weil das isolierte Test-Compose-Netz keinen DNS-Resolver fuer NTP hat.
  Neuer Test-Config-Block setzt ntp.disable_startup_check: true.

Doku nachgezogen (Plan + Runbook): Encryption-Key-Konflikt ist explizit
als "nicht Teil dieses Smokes" dokumentiert; Fehler-Matrix hat Eintraege
fuer Doppel-Underscore-ENV und NTP-Lookup.

Frische des produktiven authelia-Dumps wird unveraendert ueber
check-restore-freshness.sh ueberwacht; Daten-Decrypt-Drill ist eine eigene
DR-Aufgabe mit kontrollierter Schluessel-Verwendung.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 08:27:40 +02:00
Micha 8d71dfb9ad fix(restore): authelia smoke default_policy two_factor (rules-less)
Authelia 4.39 verlangt: ohne access_control.rules muss default_policy
'two_factor' oder 'one_factor' sein. 'bypass' war nur historisch zulaessig,
mit 4.39 schlaegt config validate fehl mit "'default_policy' option
'bypass' is invalid: when no rules are specified it must be 'two_factor'
or 'one_factor'". /api/health ist public und laeuft nicht durch
access_control - die Smoke-Semantik bleibt unveraendert.

Beobachtet im Erstlauf 2026-06-03 nach Refactor auf Minimal-Testkonfig
(Commits 541c7be..440000c). Mit diesem Fix sollte 'authelia config
validate' durchlaufen; HTTP /api/health-Smoke ist der Folgeschritt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 08:09:35 +02:00
Micha 440000c085 fix(restore): generate minimal authelia smoke config 2026-06-03 08:04:59 +02:00
Micha cacf77bfb0 fix(restore): avoid authelia smtp env in smoke test 2026-06-03 08:01:10 +02:00
Micha cd4dd178ed fix(restore): isolate authelia runtime config mount 2026-06-03 07:57:57 +02:00
Micha 541c7be853 fix(restore): generate sanitized authelia test config 2026-06-03 07:43:57 +02:00
Micha b1ae9f3c26 fix(restore): harden restore checks and add authelia smoke scaffold 2026-06-03 07:39:05 +02:00
Micha e2624796f0 fix: set vaultwarden DNS resolvers 2026-06-02 20:05:55 +02:00
Micha 9f63e6e3bc docs: archive rollback volumes after burn-in 2026-06-02 19:55:02 +02:00
Micha 8eb367f0b5 revert: remove social-to-mealie-plus stack 2026-06-02 19:44:35 +02:00
Micha 745761f518 feat: add social-to-mealie-plus stack 2026-06-02 19:17:59 +02:00
44 changed files with 1671 additions and 189 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
services:
n8n:
image: docker.n8n.io/n8nio/n8n:2.22.6@sha256:07138bb60aee990651e9c2090d7dde330cba3a5bd84fcc5cba63b2997243bc45
image: docker.n8n.io/n8nio/n8n:2.25.2@sha256:213380272bfe06f1700cdd398a6e996b496e37329c9c116d13b04811c10e7452
container_name: n8n
restart: unless-stopped
+2 -3
View File
@@ -1,6 +1,6 @@
# AI Context
Stand: 2026-06-01
Stand: 2026-06-02
Kurzer Kontext fuer KI-Agenten. Nicht als Ersatz fuer die echten Runbooks lesen.
@@ -47,7 +47,6 @@ Authoritativ: `docs/AUDIT_2026-05-25_TODO.md`.
Kurzfassung:
- Alt-Volumes fruehestens ab 2026-06-02 freigeben
- Auth-/OIDC-/CrowdSec-/Hermes-Themen bewusst geparkt
Letzte Bestaetigung:
@@ -55,7 +54,7 @@ Letzte Bestaetigung:
- Borg-Nachlauf 2026-06-01 erfolgreich: Archiv `Taegliche-Sicherung-2026-06-01T04:30:26.913`, Freshness Critical 0 / Warnings 0.
- H:/ Nearline-Pull 2026-06-01 repariert: Borg-Dumps werden kuratiert kopiert, Gitea-Bundles aktuell.
- Family-Status-Dashboard liegt als `monitoring/grafana/dashboards/family-status.json` im Repo.
- Alt-Volume-Freigabe ist per `ops/maintenance/release-alt-volumes.sh` vorbereitet; `--execute` nicht vor 2026-06-02.
- Alt-Volumes nach PG18/VectorChord-Burn-in sind seit 2026-06-02 reversibel archiviert unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602`; die alten Originalpfade sind nicht mehr aktiv gemountet.
- Family-Onboarding ist auf drei Nutzungsziele fokussiert: Vaultwarden, Immich und Mealie; praktischer Ablauf in `docs/FAMILY_ONBOARDING.md`.
- Externer Betreibercheck: `ops/maintenance/check-external-operator.sh`; FRITZ!Box 7590 meldet FRITZ!OS `154.08.25`, DNS fuer Public Apps hat keine AAAA-Records, Host hat keine globale Provider-IPv6.
- FRITZ!Box-UI 2026-06-01: Remote-HTTPS auf FRITZ!Box-UI aus, FTP/FTPS auf Speichermedien aus, WAN-Freigabe nur `443/tcp`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus.
+16 -2
View File
@@ -7,9 +7,23 @@ Audit-Snapshots wurden aus der Arbeitskopie entfernt; Detailhistorie liegt in Gi
| Prioritaet | Punkt | Naechster Schritt |
|---|---|---|
| P0 | Alt-Volumes nach Burn-in freigeben | Ab 2026-06-02 `ops/maintenance/release-alt-volumes.sh --dry-run` pruefen, danach nur bei sauberem Ergebnis mit `--execute` freigeben |
| P2 | Family-Onboarding praktisch starten | Fokus: Vaultwarden als Passwortbasis, Immich-Mobile-Backup auf jedem Handy, Mealie mit erstem Rezept/Einkaufsliste; Ablauf steht in `docs/FAMILY_ONBOARDING.md` |
## Restore-Audit Backlog (Stand 2026-06-03)
Ergebnis des Restore-Skills-Audits (Session 2026-06-02/03). Die kritischen Bugfixes (Cron-OR-Semantik, ntfy-Race, Cleanup-Trap, Pfad-Inkonsistenz, Vaultwarden-Token, Paperless-Retry, Header-Validierung, Authelia-Test) sind erledigt und committed. Die folgenden Punkte sind bewusst offener Backlog:
| Prioritaet | Punkt | Status | Naechster Schritt |
|---|---|---|---|
| P1 | Nextcloud-Restore-Test | offen | Test nach Paperless-Muster, zusaetzlich `occ maintenance:mode`-Choreographie und `oc_admin`-Rolle. Hoechster Lerngewinn unter den fehlenden Tier-2-Tests |
| P1 | Shared PostgreSQL 18 Cluster Restore Drill | offen | Komplett-Drill: `pg_dumpall --globals-only` + per-DB Custom-Format-Dumps, inkl. `mailarchiver`-Bootstrap-Rollenkonflikt. Aktuell nur als Doku in RESTORE_MATRIX, kein automatischer Test |
| P1 | Komodo-Mongo-Daten-Restore | **erledigt 2026-06-03** | 86904 Dokumente erfolgreich restored, Report `/mnt/user/backups/restore-reports/komodo-mongo-restore-2026-06-03.md`. Nebenbefund: Dump von Mongo 8.0.23, Test auf 7.0.32 — Cross-Version-Warning, fuer Lesetest harmlos |
| P2 | Mailarchiver-Restore-Test | offen | Shared Postgres + Authelia-ForwardAuth; nach Nextcloud-Test |
| P2 | Mealie-Restore-Test | offen | Eigene Postgres + File-Restore |
| P2 | Traefik-Restore-Test | offen | Tier 1, aber komplex: `dynamic/` ist manuell-sync-Ausnahme, LE-State und CF-Token-Mount sind heikel |
| P3 | Negativ-Test fuer Frische-Check | offen | Einmal pro Quartal bewusst kaputten Dump einfuettern und pruefen ob `homelab-alerts` feuert |
| P3 | End-to-end-DR-Drill | offen | Komplett-Bootstrap Phase 1-5 auf einem Wegwerf-Host; realistisch nur mit zweiter Hardware |
## Bewusst geparkt
| Punkt | Entscheidung |
@@ -25,13 +39,13 @@ Audit-Snapshots wurden aus der Arbeitskopie entfernt; Detailhistorie liegt in Gi
## Zuletzt geschlossen
- Alt-Volumes nach Burn-in freigegeben und reversibel archiviert: Shared PG17, Mealie PG17, Nextcloud PG17 und Immich pgvecto.rs liegen seit 2026-06-02 unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602`; Manifest auf dem Host: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/MANIFEST.txt`. Keine harte Loeschung, keine aktiven Container-Mounts auf die alten Pfade.
- Externer Betreibercheck vorbereitet: `docs/EXTERNAL_OPERATOR_RUNBOOK.md` und `ops/maintenance/check-external-operator.sh`; Live-Baseline am 2026-06-01: FRITZ!OS `154.08.25`, keine Public-AAAA-Records fuer `*.kaleschke.info`, Host ohne globale Provider-IPv6, WAN `443/tcp` offen und `80/tcp`/`222/tcp` geschlossen.
- FRITZ!Box-Servicefenster UI-seitig abgeschlossen: FRITZ!Box-Dienste aus dem Internet sind aus (HTTPS auf FRITZ!Box-UI, FTP/FTPS auf Speichermedien), aktive WAN-Freigabe bleibt nur `443/tcp -> 192.168.178.58`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus.
- FRITZ!Box-Konfig-Backup exportiert und extern/off-system in Vaultwarden abgelegt: `Einstellungen_FRITZ.Box_7590_154.08.25_01.06.26_1318.export`; Kennwort und Datei bleiben ausserhalb des Repos.
- Hetzner-Account-Hygiene erledigt: externe Kontakt-/Rechnungs-Mail bestaetigt, Zahlung ok, 2FA mit Google Authenticator aktiv, Recovery Key offline ausgedruckt.
- Hetzner Storage Box geprueft: `storage-box-1`, `u565255.your-storagebox.de`, SSH-Port `23`, SSH aktiv, SMB/WebDAV aus, 64,94 GB / 1 TB belegt; Borg-UI-Key und separater Maintenance-Key funktionieren wieder nach Passwort-Recovery. Borg `append-only` ist bewusst nicht umgesetzt.
- Family-View Dashboard ist repo-seitig gebaut: `monitoring/grafana/dashboards/family-status.json` zeigt Family-App-Uptime, Backup-Alter, TLS-Restlaufzeit, Critical-Container und Image-Drift.
- Alt-Volume-Freigabe ist vorbereitet: `ops/maintenance/release-alt-volumes.sh --dry-run` validiert aktive Pfade, Container-Health, Restore-Freshness und gemountete Altpfade; Test am 2026-06-01 fand vier Kandidaten und keine Blocker, Ausfuehrung bleibt wegen Cutoff bis 2026-06-02 gesperrt.
- Borg-Nachlauf nach dem 2026-05-31-Sprint ist belegt: Archiv `Taegliche-Sicherung-2026-06-01T04:30:26.913`, 101669 Dateien, `rc=0`; Freshness-Check am 2026-06-01: Critical 0, Warnings 0.
- H:/ Nearline-Pull am 2026-06-01 repariert und manuell validiert: kuratierte Borg-Dumps Exit 0, Gitea-Bundles Exit 1 (Robocopy-Erfolg mit Kopien), Report `nearline-pull-2026-06-01-082553.md`.
- Immich-, Paperless-, Gitea- und Vaultwarden-Restore-Pfade sind belegt.
+6 -6
View File
@@ -382,7 +382,7 @@ Vor dem Start muessen vorhanden sein:
- `/mnt/user/appdata/secrets/authelia_smtp_password.txt`
- SMTP-Zugang fuer `michideheld@gmx.de`
Beim Smoke-Test muss `authelia validate-config` erfolgreich sein; der SMTP-Startup-Check darf den Start nicht blockieren.
Beim Smoke-Test muss `authelia config validate` erfolgreich sein; der SMTP-Startup-Check darf den Start nicht blockieren.
### `nextcloud`
@@ -440,11 +440,11 @@ Aktive Datenpfade:
- Mealie PostgreSQL: `/mnt/user/appdata/mealie/postgres18`
- Nextcloud PostgreSQL: `/mnt/user/appdata/nextcloud/postgres18`
Rollback-Altstaende, bis zur separaten Loeschfreigabe nicht entfernen:
Rollback-Altstaende wurden nach Burn-in am 2026-06-02 reversibel archiviert:
- Shared PostgreSQL 17: `/mnt/user/appdata/postgresql17`
- Mealie PostgreSQL 17: `/mnt/user/appdata/mealie/postgres`
- Nextcloud PostgreSQL 17: `/mnt/user/appdata/nextcloud/postgres`
- Shared PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17`
- Mealie PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`
- Nextcloud PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`
Restore-Reihenfolge fuer den Shared-Cluster:
@@ -454,7 +454,7 @@ Restore-Reihenfolge fuer den Shared-Cluster:
4. Datenbanken anlegen und Custom-Format-Dumps mit `pg_restore` einspielen.
5. Restore-Logs auf echte `ERROR`, `FATAL` und `PANIC` pruefen.
Immich ist bewusst nicht Teil dieses PostgreSQL-18-Laufs: Die produktive DB bleibt auf PostgreSQL 14 und nutzt das Immich-Postgres-Image mit VectorChord/pgvector. VectorChord-Backups brauchen zum Restore ein Image mit VectorChord; der alte pgvecto.rs-Datenpfad `/mnt/user/appdata/immich_postgres` bleibt bis zur separaten Loeschfreigabe als Rollback-Altstand erhalten.
Immich ist bewusst nicht Teil dieses PostgreSQL-18-Laufs: Die produktive DB bleibt auf PostgreSQL 14 und nutzt das Immich-Postgres-Image mit VectorChord/pgvector. VectorChord-Backups brauchen zum Restore ein Image mit VectorChord; der alte pgvecto.rs-Datenpfad ist als Rollback-Altstand unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs` archiviert.
### Hermes Agent
+90 -46
View File
@@ -1,6 +1,6 @@
# Restore Handbook - KalliLab CORE
Stand: 2026-05-07
Stand: 2026-06-03
Dieses Handbuch ist die praktische Betriebsanleitung fuer Restore-Checks und Restore-Lab in KalliLab CORE.
@@ -41,28 +41,36 @@ Alle validierten Restore-Tests folgen demselben Muster:
### Vaultwarden
- Report: `/mnt/user/backups/restore-reports/vaultwarden-2026-05-07.md`
- Nachweis:
- Borg-Restore erfolgreich
- Testcontainer startete
- Login-Seite war erreichbar
- Erstlauf: 2026-05-07
- Nachweis: Borg-Restore, Testcontainer, Login-Seite erreichbar
### Gitea
- Report: `/mnt/user/backups/restore-reports/gitea-2026-05-07.md`
- Nachweis:
- Borg-Restore erfolgreich
- Web-UI antwortete
- SSH-Port reagierte
- Erstlauf: 2026-05-07
- Nachweis: Borg-Restore, Web-UI, SSH-TCP-Port
### Paperless
- Report: `/mnt/user/backups/restore-reports/paperless-2026-05-07.md`
- Nachweis:
- Borg-Datei-Restore erfolgreich
- Paperless-Dump aus Borg importiert
- Login-Seite war erreichbar
- Test-DB enthielt `25` Dokumente
- Erstlauf: 2026-05-07, Folgelauf: 2026-05-31
- Nachweis: Borg-Datei-Restore, Dump-Import in Test-Postgres, Login-Seite, Doc-Count
### Immich
- Erstlauf: 2026-05-27
- Nachweis: DB-Dump-Restore in VectorChord-Test-Postgres, HTTP-Smoke, Asset-Count
- Hinweis: Foto-Dateien-Restore ist bewusst nicht Teil des Smokes
### Authelia
- Erstlauf: 2026-06-03
- Nachweis: Config-Borg-Restore, `authelia config validate`, HTTP-Health `/api/health`
- Hinweis: Daten-Restore des produktiven Dumps ist bewusst nicht Teil des Smokes (Storage-Encryption-Key-Kopplung)
### Komodo Bootstrap
- Erstlauf: 2026-05-30
- Nachweis: Compose-Validierung, Mongo healthy, Core HTTP, Periphery running
- Hinweis: Daten-Restore aus `komodo-mongo.archive.gz` ist noch nicht getestet
---
@@ -80,6 +88,10 @@ Alle validierten Restore-Tests folgen demselben Muster:
- `/mnt/user/backups/restore-lab/vaultwarden`
- `/mnt/user/backups/restore-lab/gitea`
- `/mnt/user/backups/restore-lab/paperless`
- `/mnt/user/backups/restore-lab/immich`
- `/mnt/user/backups/restore-lab/authelia`
- `/mnt/user/backups/restore-lab/komodo`
- `/mnt/user/backups/restore-lab/_failed` (Diagnose-Material bei Fehllaeufen)
### Reports
@@ -89,31 +101,33 @@ Alle validierten Restore-Tests folgen demselben Muster:
## 5. Restore-Frequenz
- jeden Montag, 06:30:
- Frische-Check fuer Dumps und Reports
- 1. Samstag im Monat, 07:00:
- Vaultwarden
- 3. Samstag im Monat, 07:00:
- Gitea
- jeder 2. Monat, 2. Samstag, 08:00:
- Paperless
- jeden Montag, 06:30: Frische-Check fuer Dumps und Reports
- 1. Samstag im Monat, 07:00: Vaultwarden
- 3. Samstag im Monat, 07:15: Gitea
- 2. Samstag in ungeraden Monaten, 08:00: Paperless
- 2. Sonntag in Feb/Mai/Aug/Nov, 08:30: Immich
- 2. Samstag in geraden Monaten, 07:30: Authelia
- 1. Kalendertag im Monat, 09:00: Zufaelliger Restore aus Pool
Vollstaendiger Kalender mit Cron-Ausdruecken und Shell-Guards steht in `ops/restore-tests/schedule.md`.
---
## 6. Betriebsmodi
## 6. Betriebsmodus
### V1
Stand 2026-06-03 ist der Betrieb auf V1+ (V1 mit ntfy):
- validierte Bash-Host-Jobs
- Host-Job-Definitionen liegen im Repo
- Scheduler kann bereits echte Frische- und Restore-Checks fahren
- `ntfy` und Hermes-Auswertung folgen danach
- validierte Bash-Host-Jobs fuer Vaultwarden, Gitea, Paperless, Immich, Authelia, Komodo-Bootstrap
- Host-Job-Definitionen und Cron-Vorlagen liegen im Repo (`ops/restore-tests/unraid-user-scripts.md`)
- `ntfy`-Wrapper sendet Erfolg an `homelab-info`, Fehler an `homelab-alerts`
- Frische-Check prueft zusaetzlich pg-Custom-Format-Dumps per `pg_restore --list` Header-Validierung
- bei Fehlschlag wird das Restore-Lab nach `_failed/` verschoben statt geloescht
### V2
Noch geplant fuer V2:
- `ntfy` bei Erfolg/Fehler
- Hermes liest Reports und baut Uebersichten
- zusaetzliche Rotation, Sammelreports und weitere Dienste
- Hermes-Zusammenfassung ueber vorhandene Reports
- Sammelreports und Report-Rotation
- weitere Dienste (Nextcloud, Mailarchiver, Mealie)
---
@@ -126,15 +140,18 @@ Die Vorlagen stehen in:
Host-Repo-Pfad:
```text
/mnt/user/services/homelab
/mnt/user/services/homelab-infra
```
V1-Jobs:
Jobs:
1. `restore-freshness-weekly`
2. `restore-vaultwarden-monthly`
3. `restore-gitea-monthly`
4. `restore-paperless-bimonthly`
5. `restore-immich-quarterly`
6. `restore-authelia-bimonthly`
7. `monthly-random-restore`
---
@@ -169,38 +186,65 @@ Nur `Container laeuft` reicht nicht.
Auf dem Unraid-Host:
```bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh freshness
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
```
### Vaultwarden Restore-Check
```bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh vaultwarden
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh vaultwarden
```
### Gitea Restore-Check
```bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh gitea
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh gitea
```
### Paperless Restore-Check
```bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh paperless
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh paperless
```
### Immich Restore-Check
```bash
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh immich
```
### Authelia Restore-Check
```bash
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh authelia
```
### Komodo Bootstrap Trockenlauf
```bash
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh komodo-bootstrap
```
### Optional mit `ntfy`
```bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-info
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-info
```
---
## 11. Naechste Ausbaustufen
1. Vollautomatik fuer Vaultwarden, Gitea und Paperless
2. `ntfy`-Meldungen fuer Erfolg/Fehler
3. Hermes-Zusammenfassung ueber vorhandene Reports
4. naechster Referenz-Restore fuer `mail-archiver` oder `mealie`
1. Nextcloud-Restore-Test (mit `occ maintenance:mode`-Choreographie)
2. Mailarchiver-Restore-Test
3. Mealie-Restore-Test
4. Komodo-Mongo-Daten-Restore (echtes `mongorestore` statt reinem Bootstrap)
5. Shared-PostgreSQL-18-Cluster-Restore-Drill (globals + per-DB-Dumps)
6. Traefik-Restore-Test (mit `dynamic/` und LE-State)
7. Hermes-Zusammenfassung ueber vorhandene Reports
8. Report-Rotation (archivieren nach 12 Monaten)
9. Negativ-Test: bewusst kaputten Dump in den Frische-Check einfuettern
## 12. Report-Aufbewahrung
Reports unter `/mnt/user/backups/restore-reports` werden dauerhaft aufbewahrt. Bei wachsender Anzahl (ca. 50-60 pro Jahr) empfiehlt sich eine jaehrliche Archivierung alter Reports in einen Unterordner `_archive/YYYY/`. Der Frische-Check warnt bei `MAX_REPORT_AGE_DAYS=45`, loescht aber bewusst nicht automatisch.
+42 -15
View File
@@ -30,13 +30,13 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
| Traefik | Share / Borg | `/mnt/user/appdata/traefik`, besonders `dynamic/`, `letsencrypt`, `secrets` | keine eigene DB | `cloudflare_dns_api_token` | `frontend_net`, `backend_net` | `https://traefik.kaleschke.info` erreichbar, Dashboard ueber Authelia |
| AdGuard Home | Share / Borg | `/mnt/user/appdata/adguard/conf` | keine | keine zusaetzlichen Repo-Secrets dokumentiert | `dns_net`, `frontend_net` | DNS-Aufloesung funktioniert |
| Tailscale | Share / Borg | `/mnt/user/appdata/tailscale` | keine | Tailscale-State im Pfad | Host-Netz | Tailscale verbunden |
| PostgreSQL 18 | Share + Dumps | `/mnt/user/appdata/postgresql18` (Rollback-Altstand: `/mnt/user/appdata/postgresql17`) | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt`, App-Rollen-Passwoerter aus den jeweiligen Stack-ENV/Secret-Dateien | `backend_net` | DB startet, Ziel-Datenbanken vorhanden; `SHOW data_checksums` ist `on` |
| PostgreSQL 18 | Share + Dumps | `/mnt/user/appdata/postgresql18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17`) | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt`, App-Rollen-Passwoerter aus den jeweiligen Stack-ENV/Secret-Dateien | `backend_net` | DB startet, Ziel-Datenbanken vorhanden; `SHOW data_checksums` ist `on` |
| Redis 8 | Share / Host | `/mnt/user/appdata/redis`; Rollback-Backup unter `/mnt/user/backups/borg/dumps/latest/shared-redis-pre-redis8-<ts>` | RDB/AOF-Dateien im Datenpfad | `redis_password.txt` | `backend_net` | Redis startet, `redis_version` ist 8.x, Apps verbinden sich |
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL 18, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 18, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut |
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL 18, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 18, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut; Restore-Smoke am 2026-06-03 erfolgreich: Config aus Borg, minimale Test-Config, frisches Test-Postgres, HTTP `/api/health` 200, Report `/mnt/user/backups/restore-reports/authelia-2026-06-03.md` |
| Gitea | GitHub-Mirror + Gitea-Bundles fuer Repo-Bootstrap, Borg + Dump fuer Gitea-Appstate | `/mnt/user/services/gitea/data`, `/mnt/user/backups/git-bundles/gitea` | `gitea.sqlite.dump`, Bundle-Report `latest-report.md` | `borg_repo_passphrase.txt` fuer Restore-Tests; GitHub-Push-Mirror-PAT liegt nur in Gitea-Mirror-Settings | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert; Bundle laesst sich klonen und `git fsck` ist sauber; GitHub-Push-Mirror synchronisiert ohne `last_error`; Mini-Restore nach `/mnt/user/backups/restore-lab/gitea` am 2026-05-07 erfolgreich validiert |
| Komodo | Borg / Share | `/mnt/user/appdata/komodo/core`, `/mnt/user/appdata/komodo/periphery`, `/mnt/user/services/stacks` | `komodo-mongo.archive.gz` falls verifiziert | `komodo_mongo_password.txt`, `KOMODO_*` Stack ENV | Traefik, Mongo, Gitea | UI erreichbar, Periphery verbunden |
| GitOps Host Automation | Borg / Git | `/mnt/user/services/homelab-infra`, `/mnt/user/services/posture-check` | keine eigene DB | keine | Gitea, Komodo, Unraid User Scripts | `posture-check` laeuft vom Host-Pfad und liefert `warning_count: 0` im bekannten Uebergangszustand |
| Vaultwarden | Borg + Dump | `/mnt/user/appdata/vaultwarden` | `vaultwarden.sqlite.dump` | `vaultwarden_admin_token.txt`, `borg_repo_passphrase.txt` fuer Restore-Tests | Traefik | Login-Seite erreichbar, Tresor-Daten sichtbar; Mini-Restore nach `/mnt/user/backups/restore-lab/vaultwarden` am 2026-05-07 erfolgreich validiert |
| Vaultwarden | Borg + Dump | `/mnt/user/appdata/vaultwarden` | `vaultwarden.sqlite.dump` | `vaultwarden_admin_token.txt` fuer Produktion; Restore-Test nutzt Wegwerf-Admin-Token und `borg_repo_passphrase.txt` | Traefik | Login-Seite erreichbar, Tresor-Daten sichtbar; Mini-Restore nach `/mnt/user/backups/restore-lab/vaultwarden` am 2026-05-07 erfolgreich validiert |
---
@@ -45,10 +45,10 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|---|---|---|---|---|---|---|
| Paperless-ngx | Borg + Dumps | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/paperless/export`, `/mnt/user/documents/scans_inbox` | `postgresql17-paperless.dump` | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS`, `borg_repo_passphrase.txt` fuer Restore-Tests | PostgreSQL 18, Redis, Traefik | Web-UI startet, Dokumente vorhanden; Restore-Test am 2026-05-31 erfolgreich: Borg-Archiv `Tägliche-Sicherung-2026-05-31T04:30:13.181`, isolierter PostgreSQL-18-/Redis-8-Testpfad, HTTP `200`, `32` Dokumente im Test-DB-Check, Report `/mnt/user/backups/restore-reports/paperless-2026-05-31.md` |
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, `/mnt/user/appdata/mealie/postgres18` (Rollback-Altstand: `/mnt/user/appdata/mealie/postgres`) | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden |
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive`, `/mnt/user/appdata/immich_postgres_vectorchord`; Rollback-Altstand: `/mnt/user/appdata/immich_postgres` | `immich.dump`; nach VectorChord braucht ein Restore ein Postgres-Image mit VectorChord | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt`, `borg_repo_passphrase.txt` fuer Restore-Tests | `immich_postgres`, `immich_redis`, Traefik | DB- und UI-Smoke gegen produktives Borg-Archiv am 2026-05-27 erfolgreich validiert; VectorChord-Migration am 2026-05-31: `11977` Assets, `11107` Smart-Search-Zeilen, `7092` Face-Search-Zeilen, `vchord 0.4.3`, `vector 0.8.1`, HTTP/API-Smoke 200. Voll-Restore der Foto-Dateien bleibt separater DR-Drill |
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, `/mnt/user/appdata/mealie/postgres18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`) | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden |
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive`, `/mnt/user/appdata/immich_postgres_vectorchord`; archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs` | `immich.dump`; nach VectorChord braucht ein Restore ein Postgres-Image mit VectorChord | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt`, `borg_repo_passphrase.txt` fuer Restore-Tests | `immich_postgres`, `immich_redis`, Traefik | DB- und UI-Smoke gegen produktives Borg-Archiv am 2026-05-27 erfolgreich validiert; VectorChord-Migration am 2026-05-31: `11977` Assets, `11107` Smart-Search-Zeilen, `7092` Face-Search-Zeilen, `vchord 0.4.3`, `vector 0.8.1`, HTTP/API-Smoke 200. Voll-Restore der Foto-Dateien bleibt separater DR-Drill |
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 18, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen |
| Nextcloud | Borg + Dump | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data`, `/mnt/user/appdata/nextcloud/postgres18` (Rollback-Altstand: `/mnt/user/appdata/nextcloud/postgres`), `/mnt/user/appdata/nextcloud/redis` | `nextcloud.dump`; Redis-Backup vor Redis-8-Cutover unter `/mnt/user/backups/borg/dumps/latest/nextcloud-redis-pre-redis8-<ts>` | `nextcloud_admin_user.txt`, `nextcloud_admin_password.txt`, `nextcloud_postgres_password.txt`; produktive DB-Rolle laut `config.php` ist `oc_admin` | `nextcloud-postgres`, `nextcloud-redis`, Traefik | Web-UI startet, Login funktioniert, Dateien sichtbar; `occ status` zeigt `maintenance: false` |
| Nextcloud | Borg + Dump | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data`, `/mnt/user/appdata/nextcloud/postgres18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`), `/mnt/user/appdata/nextcloud/redis` | `nextcloud.dump`; Redis-Backup vor Redis-8-Cutover unter `/mnt/user/backups/borg/dumps/latest/nextcloud-redis-pre-redis8-<ts>` | `nextcloud_admin_user.txt`, `nextcloud_admin_password.txt`, `nextcloud_postgres_password.txt`; produktive DB-Rolle laut `config.php` ist `oc_admin` | `nextcloud-postgres`, `nextcloud-redis`, Traefik | Web-UI startet, Login funktioniert, Dateien sichtbar; `occ status` zeigt `maintenance: false` |
| Glance | Git / Borg-Repo | Repo-Konfiguration unter `ops/glance/config/glance.yml`; keine kritische Datenpersistenz | keine | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY` | Traefik, Authelia, optional interne API-Ziele | Dashboard startet, Widgets laden, Docker-Status laeuft nur ueber `glance-docker-socket-proxy` |
| ntfy | Borg / Share | `/mnt/user/appdata/ntfy` | keine | keine besonderen Secret-Dateien dokumentiert | Traefik | UI und Push-Endpunkt erreichbar |
| Paperless-GPT | Borg / Share | `/mnt/user/appdata/paperless-gpt` | keine eigene DB | `PAPERLESS_API_TOKEN`, `OPENAI_API_KEY` | Traefik, Paperless, OpenAI API | UI startet, Konfiguration vorhanden; LLM-Provider zeigt `openai` / `gpt-5.4-mini` |
@@ -100,10 +100,10 @@ Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.s
### PostgreSQL 18 Restore- und Rollback-Regeln
- PostgreSQL-18-Container verwenden das Docker-Image-Layout mit Mount auf `/var/lib/postgresql` und `PGDATA=/var/lib/postgresql/18/docker`.
- Die alten PostgreSQL-17-Datenpfade bleiben nach dem Major-Upgrade als Rollback-Altstand erhalten und duerfen erst nach separater Freigabe geloescht werden.
- Die alten PostgreSQL-17-Datenpfade wurden nach Burn-in am 2026-06-02 aus den aktiven Appdata-Pfaden entfernt und unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602` archiviert.
- Shared-Cluster-Restore: zuerst `pg_dumpall --globals-only` einspielen, dann die einzelnen Custom-Format-Dumps per `pg_restore`. Der Bootstrap-Rollenkonflikt fuer `mailarchiver` ist benign, solange `CREATE ROLE mailarchiver;` gezielt ausgelassen und das folgende `ALTER ROLE mailarchiver ...` eingespielt wird.
- Nextcloud-Restore: vor dem Dump `occ maintenance:mode --on`, nach erfolgreichem Restore und `occ status` wieder `occ maintenance:mode --off`. Die Rolle `oc_admin` muss mit dem in `config.php` hinterlegten DB-Passwort existieren.
- Rollback: betroffene App(s) und DB stoppen, Compose auf das vorherige PostgreSQL-17-Image und den alten Datenpfad zuruecksetzen, dann DB und App wieder starten.
- Rollback: betroffene App(s) und DB stoppen, archivierten Altstand zurueck an den frueheren Datenpfad verschieben, Compose auf das vorherige PostgreSQL-17-Image und den alten Datenpfad zuruecksetzen, dann DB und App wieder starten.
---
@@ -125,13 +125,40 @@ Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.s
---
## Erste sinnvolle Referenz-Restores
## Restore-Test-Reifegrad
Wenn weitere Restore-Uebungen dokumentiert werden sollen, sind diese Dienste besonders geeignet:
Stand 2026-06-03. Pro Dienst auf einen Blick: Wurde der Restore schon einmal real getestet?
1. `mail-archiver`
2. `paperless-ngx`
3. `gitea`
4. `vaultwarden`
| Dienst | Tier | Letzter Restore-Test | Typ | Naechster Lauf |
|---|---|---|---|---|
| Vaultwarden | 1 | 2026-05-07 | File + Container + HTTP | monatlich (1. Sa) |
| Gitea | 1 | 2026-05-07 | File + Container + HTTP + TCP | monatlich (3. Sa) |
| Authelia | 1 | 2026-06-03 | Config + Validate + HTTP Health | zweimonatlich (2. Sa gerade Mon.) |
| Komodo Bootstrap | 1 | 2026-05-30 | Compose + Mongo + HTTP | quartalsweise |
| Paperless | 2 | 2026-05-31 | File + Dump + Container + HTTP + Doc-Count | zweimonatlich (2. Sa ungerade Mon.) |
| Immich | 2 | 2026-05-27 | Dump + Container + HTTP + Asset-Count | quartalsweise (2. So Feb/Mai/Aug/Nov) |
| Unraid OS Flash | 1 | - | noch kein Test | - |
| Traefik | 1 | - | noch kein Test | naechster Kandidat |
| AdGuard Home | 1 | - | noch kein Test | - |
| Tailscale | 1 | - | noch kein Test | - |
| PostgreSQL 18 Cluster | 1 | - | noch kein Test (globals + per-DB) | naechster Kandidat |
| Redis 8 | 1 | - | noch kein Test | - |
| Komodo Mongo Daten | 1 | 2026-06-03 | mongorestore --archive --gzip, 86904 docs | quartalsweise |
| Nextcloud | 2 | - | noch kein Test | **hoechste Prio** (occ-Choreographie) |
| Mealie | 2 | - | noch kein Test | - |
| Mail-Archiver | 2 | - | noch kein Test | - |
| Glance | 2 | - | rebuildbar, kein Test noetig | - |
| ntfy | 2 | - | rebuildbar, kein Test noetig | - |
| Borg UI | 3 | - | rebuildbar | - |
| Filebrowser | 3 | - | rebuildbar | - |
Sie liefern hohen Erkenntnisgewinn ohne den kompletten Homelab-Neuaufbau zu brauchen.
---
## Naechste Restore-Test-Kandidaten (priorisiert)
1. **Nextcloud** - hoechste Prio wegen `occ maintenance:mode`-Choreographie und `oc_admin`-Rolle
2. **Shared PostgreSQL 18 Cluster** - globals + per-DB-Dumps, Bootstrap-Konflikt `mailarchiver`
3. **Komodo Mongo Daten** - echtes `mongorestore` aus `komodo-mongo.archive.gz` (Quelle fuer `KOMODO_*`-Stack-ENVs im DR)
4. **Mailarchiver** - Tier 2, shared Postgres + Authelia ForwardAuth
5. **Mealie** - Tier 2, eigene Postgres
6. **Traefik** - Tier 1, aber komplex (dynamic/, LE-State, CF-Token-Mount)
+4 -4
View File
@@ -27,7 +27,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|---|---|---|---|---|---|---|---|---|
| `postgresql17` | shared PostgreSQL 18 Cluster (historischer Service-Name bleibt fuer DNS/Clients stabil) | `infra/postgresql17/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/postgresql18`, Rollback-Altstand `/mnt/user/appdata/postgresql17`, `postgres_password.txt` | Tier 1; Dumps unter `/mnt/user/backups/borg/dumps/latest` | nein | keine Host-Ports; raw DB nicht primaerer Restore-Weg |
| `postgresql17` | shared PostgreSQL 18 Cluster (historischer Service-Name bleibt fuer DNS/Clients stabil) | `infra/postgresql17/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/postgresql18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17`, `postgres_password.txt` | Tier 1; Dumps unter `/mnt/user/backups/borg/dumps/latest` | nein | keine Host-Ports; raw DB nicht primaerer Restore-Weg |
| `Redis` | primaer Paperless-Redis (App-Cache); historisch als "shared" angelegt, faktisch nur von Paperless genutzt | `infra/redis/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/redis`, `redis_password.txt` | transiente Daten, bewusst nicht kritisch | nein | Redis 8.8; Passwort-Datei; optional named volume offen. Immich, Nextcloud und Mealie nutzen jeweils eigene Redis-Instanzen; Authelia laeuft bewusst ohne Redis-Session-Backend. Bei Wegfall ist Paperless der einzige betroffene Stack. |
| `ddns-updater` | Cloudflare/DDNS Aktualisierung | `infra/ddns-updater/docker-compose.yml` | intern | Internetzugang, `frontend_net` | `/mnt/user/appdata/ddns-updater` | rebuildbar | nein | bleibt bewusst in `frontend_net`, weil `backend_net` internal ist |
@@ -38,14 +38,14 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
| `paperless-ngx` | Dokumentenmanagement | `apps/paperless/docker-compose.yml` | `https://paperless.kaleschke.info` | PostgreSQL 18, Redis 8, Traefik | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/scans_inbox` | Tier 2, Borg + `postgresql17-paperless.dump` | ja | DB/Redis Secrets bleiben bewusst Stack ENV; Dump-Dateiname behaelt den historischen Cluster-Namen |
| `paperless-gpt` | KI-Ergaenzung fuer Paperless | `apps/paperless-gpt/docker-compose.yml` | `https://paperless-gpt.kaleschke.info` | Paperless API, OpenAI API, Traefik | `/mnt/user/appdata/paperless-gpt/data`, `/mnt/user/appdata/paperless-gpt/prompts` | Tier 2 | ja + Authelia | `PAPERLESS_API_TOKEN` und `OPENAI_API_KEY` als Stack ENV; LLM und Vision-OCR laufen ueber `gpt-5.4-mini`, kein Zugriff mehr auf lokale Ollama-VM. **Behalten-Entscheidung 2026-05-28:** Container bleibt aktiv, auch wenn aktuell keine Traefik-Zugriffe in der Woche; Ablouseplanung erst mit Paperless-NGX 3.0 (eigene KI-Features erwartet) - dann neu bewerten. |
| `immich_server` | Foto-/Video-App | `apps/immich/docker-compose.yml` | `https://immich.kaleschke.info` | Immich Postgres, Immich Redis, ML, Traefik | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | Tier 2, Borg + `immich.dump` | ja | native App-Auth; externes Fotoarchiv gemountet |
| `immich_postgres` | Immich-Datenbank | `apps/immich/docker-compose.yml` | intern | `immich_default` | `/mnt/user/appdata/immich_postgres_vectorchord`, Rollback-Altstand `/mnt/user/appdata/immich_postgres`, `immich_postgres_password.txt` | Dump `immich.dump`; Restore braucht ein Image mit VectorChord/pgvector | nein | PG14 bleibt bewusst; Immich-DB-Image `ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0`; nie ins `frontend_net` |
| `immich_postgres` | Immich-Datenbank | `apps/immich/docker-compose.yml` | intern | `immich_default` | `/mnt/user/appdata/immich_postgres_vectorchord`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs`, `immich_postgres_password.txt` | Dump `immich.dump`; Restore braucht ein Image mit VectorChord/pgvector | nein | PG14 bleibt bewusst; Immich-DB-Image `ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0`; nie ins `frontend_net` |
| `immich_redis` | Immich Cache | `apps/immich/docker-compose.yml` | intern | `immich_default` | kein kritischer Pfad dokumentiert | rebuildbar | nein | Redis 8.8; Architektur nennt anonymes Volume -> named volume als offenes Thema |
| `immich_machine_learning` | Immich ML | `apps/immich/docker-compose.yml` | intern | `immich_default` | `model-cache` | rebuildbar | nein | intern-only |
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, Rollback-Altstand `/mnt/user/appdata/mealie/postgres`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 |
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 |
| `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 18, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht; Dump-Dateiname behaelt den historischen Cluster-Namen |
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres18`, Rollback-Altstand `/mnt/user/appdata/nextcloud/postgres`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
| `nextcloud-redis` | Nextcloud Cache/Locking | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/redis` | Teil von Nextcloud-Restore | nein | interne Redis 8.8 |
| `plex` | Medienserver mit LAN-/Client-Discovery | `host-services/plex/docker-compose.yml` | Plex native, **LAN/Tailscale-only**, Remote Access deaktiviert | Host-Netz | `/mnt/user/appdata/plex/config`, `/mnt/user/appdata/plex/transcode`, `/mnt/user/media`, `/mnt/user/photos` | Tier 2, Appdata + Medienpfade im Borg-/Share-Scope | nein | Repo-Compose-Stack; `network_mode: host` bleibt dokumentierte Discovery-Ausnahme. Server geclaimt von `Xeridos` (Reclaim 2026-05-28 nach Preferences-Reset vom 18.05.). Smart-TVs greifen ueber WLAN-LAN per mDNS/Plex-GDM direkt zu. `PublishServerOnPlexOnlineKey=0` (Remote Access aus), `RelayEnabled` ebenfalls aus. |
| `ntfy` | Push-Benachrichtigungen | `apps/ntfy/docker-compose.yml` | `https://ntfy.kaleschke.info` | Traefik, upstream mobile push | `/mnt/user/appdata/ntfy` | Tier 2 | ja | `NTFY_BEHIND_PROXY=true`; Problem-Alerts gehen gebuendelt an `homelab-alerts`, optionale Erfolgsmeldungen an `homelab-info` |
+1 -1
View File
@@ -1,6 +1,6 @@
services:
adguard:
image: adguard/adguardhome:v0.107.76@sha256:7157eb1dc3b26c7af1d6898759a7b3f7d0fa09891fbd2d3caa6abc1057a9179b
image: adguard/adguardhome:v0.107.77@sha256:e6f2b8bcda06064ab055b44933a4f0e983c35558b9cdb8d2e7ab1efcee36d890
container_name: adguard
restart: unless-stopped
volumes:
+2 -2
View File
@@ -115,7 +115,7 @@ services:
- loki
grafana:
image: grafana/grafana:13.0.1@sha256:0f86bada30d65ef9d0183b90c1e2682ac92d53d95da8bed322b984ea78a4a73a
image: grafana/grafana:13.0.2@sha256:5dad0df181cb644a14e13617b913b261a54f7d4fd4510721dba420929f35bea2
container_name: monitoring-grafana
user: "0"
restart: unless-stopped
@@ -318,7 +318,7 @@ services:
- no-new-privileges:true
influxdb3-core:
image: influxdb:3.9.2-core@sha256:31ad94df2248134989b2cf73d965e51dd5f35dfae22d7ed8f4776b12e6f69f4e
image: influxdb:3.9.3-core@sha256:c27c9b2ca2625b5b6966f0b09baa448102310e63a471fd60dff22646a2522e29
container_name: monitoring-influxdb3-core
user: "0"
restart: unless-stopped
+2 -4
View File
@@ -90,18 +90,16 @@ The live Unraid User Scripts execute repo scripts from `/mnt/user/services/homel
## Explicitly Not Backed Up as Raw Live DB Files
- `/mnt/user/appdata/postgresql17`
- `/mnt/user/appdata/postgresql18`
- `/mnt/user/appdata/mealie/postgres`
- `/mnt/user/appdata/mealie/postgres18`
- `/mnt/user/appdata/immich_postgres`
- `/mnt/user/appdata/immich_postgres_vectorchord`
- `/mnt/user/appdata/nextcloud/postgres`
- `/mnt/user/appdata/nextcloud/postgres18`
- `/mnt/user/appdata/komodo/mongo`
- `/mnt/user/appdata/redis`
- `/mnt/user/appdata/scrutiny/influxdb`
Archived PG18/VectorChord rollback volumes under `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602` are retained only as temporary rollback material, not as primary backup truth.
## Low-Priority / Rebuildable
These are not part of the first-class Borg scope:
+1 -1
View File
@@ -1,6 +1,6 @@
services:
borg-ui:
image: ainullcode/borg-ui@sha256:b44c0a92b650d80f215a986dadda5c2604c61eb28a7571e19c046eff41d761e7
image: ainullcode/borg-ui@sha256:acb0fbe83dc4a3843abc06f814c5f1061a0701b2cfc574da2e851d17a34ab745
container_name: borg-ui
restart: unless-stopped
security_opt:
+1 -1
View File
@@ -1,6 +1,6 @@
services:
code-server:
image: lscr.io/linuxserver/code-server:4.122.0@sha256:0caf1b65ebec84b94397108b56da6c33f124c5390f5832da94e75f4609c0e2ad
image: lscr.io/linuxserver/code-server:4.122.1@sha256:21302fbcedc15e78a6f542cb78e4b77cf660f19664a01cd359d81d666b6cb6fd
container_name: code-server
restart: unless-stopped
security_opt:
+1 -1
View File
@@ -1,6 +1,6 @@
services:
filebrowser:
image: filebrowser/filebrowser:v2.63.5@sha256:aefb0c20de10ef8b617995ca5522479ad40d41e6386bd01946a345c6026ff31c
image: filebrowser/filebrowser:v2.63.7@sha256:40ba654524feb29afd28de458fdcf996b119cd1b53be2630d7658e01419f5891
container_name: filebrowser
restart: unless-stopped
security_opt:
+2 -2
View File
@@ -99,7 +99,7 @@
"dump_file": null,
"data_paths": ["/mnt/user/appdata/postgresql18"],
"first_check": "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?",
"notes": "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad bleibt nur Rollback-Altstand"
"notes": "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17 archiviert"
},
"komodo-core": {
"description": "GitOps UI / API / Stack-Manager",
@@ -202,7 +202,7 @@
"dump_file": "immich.dump",
"data_paths": ["/mnt/user/appdata/immich_postgres_vectorchord"],
"first_check": "immich_default Netz? Disk-Space? pg_isready?",
"notes": "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad bleibt nur Rollback-Altstand"
"notes": "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs archiviert"
},
"immich_redis": {
"description": "Immich Cache",
+2 -2
View File
@@ -138,7 +138,7 @@ services:
data_paths:
- /mnt/user/appdata/postgresql18
first_check: "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?"
notes: "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad bleibt nur Rollback-Altstand"
notes: "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17 archiviert"
komodo-core:
description: GitOps UI / API / Stack-Manager
@@ -263,7 +263,7 @@ services:
data_paths:
- /mnt/user/appdata/immich_postgres_vectorchord
first_check: "immich_default Netz? Disk-Space? pg_isready?"
notes: "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad bleibt nur Rollback-Altstand"
notes: "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs archiviert"
immich_redis:
description: Immich Cache
+38 -15
View File
@@ -3,6 +3,7 @@ set -euo pipefail
MODE="dry-run"
CUTOFF_DATE="2026-06-02"
ARCHIVE_ROOT="/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602"
if [[ "${1:-}" == "--execute" ]]; then
MODE="execute"
@@ -23,10 +24,10 @@ if [[ "$MODE" == "execute" && "$today" < "$CUTOFF_DATE" ]]; then
fi
declare -a CANDIDATES=(
"/mnt/user/appdata/postgresql17|/mnt/user/appdata/postgresql18|shared PostgreSQL 17 rollback"
"/mnt/user/appdata/mealie/postgres|/mnt/user/appdata/mealie/postgres18|Mealie PostgreSQL 17 rollback"
"/mnt/user/appdata/nextcloud/postgres|/mnt/user/appdata/nextcloud/postgres18|Nextcloud PostgreSQL 17 rollback"
"/mnt/user/appdata/immich_postgres|/mnt/user/appdata/immich_postgres_vectorchord|Immich pgvecto.rs rollback"
"/mnt/user/appdata/postgresql17|/mnt/user/appdata/postgresql18|postgresql17|shared PostgreSQL 17 rollback"
"/mnt/user/appdata/mealie/postgres|/mnt/user/appdata/mealie/postgres18|mealie-postgres17|Mealie PostgreSQL 17 rollback"
"/mnt/user/appdata/nextcloud/postgres|/mnt/user/appdata/nextcloud/postgres18|nextcloud-postgres17|Nextcloud PostgreSQL 17 rollback"
"/mnt/user/appdata/immich_postgres|/mnt/user/appdata/immich_postgres_vectorchord|immich-postgres-pgvecto-rs|Immich pgvecto.rs rollback"
)
require_container_healthy() {
@@ -48,9 +49,10 @@ require_container_healthy() {
fi
}
echo "Alt-volume release check"
echo "Alt-volume archive check"
echo "Mode: $MODE"
echo "Date: $today"
echo "Archive: $ARCHIVE_ROOT"
echo
require_container_healthy postgresql17
@@ -68,37 +70,58 @@ if [[ -x /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.s
/mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
fi
mapfile -t active_mounts < <(docker inspect $(docker ps -q) --format '{{range .Mounts}}{{println .Source}}{{end}}' 2>/dev/null || true)
mapfile -t active_mounts < <(docker inspect $(docker ps -aq) --format '{{range .Mounts}}{{println .Source}}{{end}}' 2>/dev/null || true)
if [[ "$MODE" == "execute" ]]; then
mkdir -p "$ARCHIVE_ROOT"
fi
for entry in "${CANDIDATES[@]}"; do
IFS='|' read -r old_path active_path label <<< "$entry"
IFS='|' read -r old_path active_path archive_name label <<< "$entry"
archive_path="$ARCHIVE_ROOT/$archive_name"
if [[ ! -d "$active_path" ]]; then
echo "Missing active path for $label: $active_path" >&2
exit 1
fi
if [[ ! -d "$old_path" ]]; then
echo "Already absent: $old_path ($label)"
if printf '%s\n' "${active_mounts[@]}" | grep -Fxq "$old_path"; then
echo "Refusing: old path is still mounted by a container: $old_path" >&2
exit 1
fi
if [[ -d "$old_path" && -d "$archive_path" ]]; then
echo "Refusing: both old path and archive path exist for $label." >&2
echo "Old: $old_path" >&2
echo "Archive: $archive_path" >&2
exit 1
fi
if [[ -d "$archive_path" ]]; then
size="$(du -sh "$archive_path" 2>/dev/null | awk '{print $1}')"
echo "Archived: $archive_path ($label, $size)"
echo
continue
fi
if printf '%s\n' "${active_mounts[@]}" | grep -Fxq "$old_path"; then
echo "Refusing: old path is still mounted by a running container: $old_path" >&2
if [[ ! -d "$old_path" ]]; then
echo "Absent and not archived: $old_path ($label)" >&2
exit 1
fi
size="$(du -sh "$old_path" 2>/dev/null | awk '{print $1}')"
echo "Candidate: $old_path ($label, $size)"
echo "Active: $active_path"
echo "Archive: $archive_path"
if [[ "$MODE" == "execute" ]]; then
rm -rf --one-file-system "$old_path"
echo "Removed: $old_path"
mv "$old_path" "$archive_path"
printf '%s MOVE %s -> %s size=%s\n' "$(date -Is)" "$old_path" "$archive_path" "$size" >> "$ARCHIVE_ROOT/MANIFEST.txt"
echo "Moved: $archive_path"
else
echo "Dry-run: would remove $old_path"
echo "Dry-run: would move $old_path to $archive_path"
fi
echo
done
echo "Alt-volume release check completed."
echo "Alt-volume archive check completed."
+14 -3
View File
@@ -20,6 +20,7 @@ Ziel:
## Geplante Struktur
- `schedule.md`: Intervalle und Verantwortlichkeiten
- `common.sh`: gemeinsame Helfer fuer Borg-Lookup, Borg-Extract und Compose-Cleanup; prueft vor Borg-Operationen auch `borg-ui:/data/borg.db` und `borg-ui:/local/secrets/borg_repo_passphrase.txt`
- `vaultwarden-restore-test.ps1`: erster Mini-Restore-Ablauf
- `vaultwarden-restore-test.sh`: hosttauglicher Vaultwarden-Restore-Job
- `vaultwarden-plan.md`: konkreter Vaultwarden-Testplan
@@ -37,6 +38,13 @@ Ziel:
- `immich-plan.md`: konkreter Immich-Testplan
- `immich-runbook.md`: Operator-Runbook fuer den ersten Immich-Lauf
- `immich-compose.test.yml`: isolierte Testinstanz fuer Immich inkl. VectorChord/pgvector-Test-Postgres und Test-Redis
- `authelia-restore-test.sh`: Authelia-Restore-Job (Config-Smoke; Erstlauf 2026-06-03 erfolgreich)
- `authelia-compose.test.yml`: isolierte Testinstanz fuer Authelia inkl. Test-Postgres, Filesystem-Notifier (kein echter SMTP-Versand)
- `authelia-plan.md`: konkreter Authelia-Testplan
- `authelia-runbook.md`: Operator-Runbook fuer den ersten Authelia-Lauf
- `nextcloud-restore-test.sh`: Nextcloud-Restore-Job (Scaffold; **blockiert** durch Unraid shfs-chmod-Inkompatibilitaet - siehe unten)
- `nextcloud-compose.test.yml`: isolierte Testinstanz fuer Nextcloud inkl. Test-Postgres und Test-Redis
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
@@ -82,9 +90,12 @@ Aktuell ist das erste validierte Muster vorhanden.
- echter Gitea-Restore am 2026-05-07 erfolgreich verifiziert
- echter Paperless-Restore am 2026-05-07 erfolgreich verifiziert
- Immich-Restore-Test am 2026-05-27 erfolgreich verifiziert; Test-Postgres wurde nach der VectorChord-Migration am 2026-05-31 auf das produktive Immich-Postgres-Image umgestellt
- Authelia-Restore-Smoke am 2026-06-03 erfolgreich verifiziert; bewusst ohne produktiven Dump-Restore wegen Storage-Encryption-Key-Kopplung
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
- Restore-Lab und Report-Pfade auf dem Host angelegt
- V1-Ablauf weiter ohne `ntfy`, mit Bereinigung nach Erfolg
- naechster grosser Kandidat ist ein erneuter Immich-Lauf nach VectorChord-Migration mit Zeitmessung; danach in die Rotation aufnehmen
- `ntfy`-Wrapper ist fuer Host-Jobs verfuegbar
- Nextcloud-Restore-Test: Scaffold existiert, aber **blockiert**. Nextcloud 33 fuehrt zur Laufzeit `chmod()` auf Dateien unter `/var/www/html` aus (`OC_Util.php:486`). Auf Unraids FUSE/shfs User-Shares ist `chmod` strukturell nicht moeglich, was zu permanenter 503 fuehrt. Loesungsoptionen: (a) Restore-Lab auf ein Cache-Drive statt User Share legen, (b) Docker-Volumes statt Bind-Mounts verwenden, (c) tmpfs-Mount fuer html/ + `rsync` der Borg-Daten hinein. Bis dahin ist Nextcloud als Backlog-Item dokumentiert.
- Komodo-Mongo-Daten-Restore am 2026-06-03 erfolgreich: 86904 Dokumente (inkl. 32 Stacks), Report `/mnt/user/backups/restore-reports/komodo-mongo-restore-2026-06-03.md`
- naechste grosse Kandidaten sind Mailarchiver und Mealie; Nextcloud bleibt blockiert (shfs-chmod)
Vor dem ersten echten Testlauf muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
Vor dem ersten echten Testlauf je neuem Dienst muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
@@ -0,0 +1,53 @@
services:
restoretest-authelia-postgres:
# Gleiche Major-Version wie shared PostgreSQL 18 in Produktion.
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
container_name: restoretest-authelia-postgres
restart: "no"
environment:
TZ: Europe/Berlin
POSTGRES_USER: authelia
POSTGRES_DB: authelia
POSTGRES_PASSWORD: restoretest-authelia-db
PGDATA: /var/lib/postgresql/18/docker
volumes:
- /mnt/user/backups/restore-lab/authelia/postgres:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U authelia -d authelia"]
interval: 10s
timeout: 5s
retries: 10
security_opt:
- no-new-privileges:true
restoretest-authelia:
# Gleicher Image-Digest wie security/authelia/docker-compose.yml in Produktion.
image: authelia/authelia:4.39.20@sha256:1b363e9279e742397966333f364e0876ae02bf5c876de73e83af6d48c57ff51b
container_name: restoretest-authelia
restart: "no"
depends_on:
restoretest-authelia-postgres:
condition: service_healthy
command:
- authelia
- --config=/config/configuration.yml
environment:
TZ: Europe/Berlin
# Wegwerf-Secrets nur fuer den isolierten Smoke. Niemals produktive
# Authelia-Secrets in diesem Compose verwenden. Die produktiven
# authelia_*_FILE-Mounts werden bewusst NICHT eingebunden.
AUTHELIA_SESSION_SECRET: restoretest-authelia-session-secret-placeholder-32
AUTHELIA_STORAGE_ENCRYPTION_KEY: restoretest-authelia-storage-enc-key-placeholder-32
AUTHELIA_STORAGE_POSTGRES_PASSWORD: restoretest-authelia-db
# server.address wird in der vom Skript erzeugten configuration.yml
# gesetzt (tcp://0.0.0.0:9091). Eine zusaetzliche ENV waere
# redundant - und in Authelia 4.39 nicht als Doppel-Underscore
# akzeptiert (war Ursache des "configuration environment variable
# not expected"-Warnings im Lauf 2026-06-03).
volumes:
- /mnt/user/backups/restore-lab/authelia/test-config:/config
ports:
# nur 127.0.0.1, keine Public-Route, keine Traefik-Labels
- "127.0.0.1:19091:9091"
security_opt:
- no-new-privileges:true
+89
View File
@@ -0,0 +1,89 @@
# Authelia Restore Test Plan
## Ziel
Nachweisen, dass die Authelia-Konfiguration aus dem produktiven Borg-Archiv in einer isolierten Testumgebung wieder lauffaehig ist und der HTTP-Health-Endpunkt antwortet, ohne dass dabei produktive Secrets, produktives Postgres oder produktiver SMTP-Versand beruehrt werden.
Bewusst **nicht** Teil dieses Tests:
- Restore mit produktiven Authelia-Secrets. Der Test nutzt ausschliesslich Wegwerf-Werte fuer `AUTHELIA_SESSION_SECRET`, `AUTHELIA_STORAGE_ENCRYPTION_KEY` und `AUTHELIA_STORAGE_POSTGRES_PASSWORD`. SMTP- und Legacy-JWT-Env-Werte werden bewusst nicht gesetzt, damit Authelia keinen `notifier.smtp`-Block oder deprecated `jwt_secret` aus Env erzeugt.
- SMTP-Realanruf an GMX. Die minimale Test-Konfiguration setzt nur den Filesystem-Notifier.
- Forward-Auth gegen Traefik. Test laeuft nur auf `127.0.0.1:19091`, keine Traefik-Route.
- WebAuthn-/Duo-/OIDC-Identity-Provider-Endpunkte. Smoke prueft `/api/health`.
- **pg_restore des produktiven `postgresql17-authelia.dump`**. Authelia verschluesselt Storage-Werte mit `AUTHELIA_STORAGE_ENCRYPTION_KEY`. Ein Restore mit produktiven Daten in eine Test-Instanz mit Wegwerf-Key schlaegt im Startup-Check **by design** fehl ("the configured encryption key does not appear to be valid for this database"). Frische des produktiven Dumps wird ueber `check-restore-freshness.sh` ueberwacht; Daten-Decrypt-Drill ist eine separate DR-Aufgabe und braucht eine eigene Sicherheits-Choreographie mit kontrollierter Schluessel-Verwendung. Beobachtet im Erstlauf 2026-06-03 (Commit-Reihe `cacf77b..8d71dfb`); seit dem 2026-06-03-Folgecommit ist der Dump-Restore explizit aus dem Smoke entfernt.
## Quelle
- Backup-Quelle: produktives Borg-Archiv (`hetzner_borg_appdata_critical`)
- fachlich relevante Pfade im Archiv:
- `local/appdata/authelia/config` (verpflichtend)
- `local/borg-dumps/latest/postgresql17-authelia.dump` (existiert ggf. im Archiv; wird vom Smoke bewusst NICHT eingespielt, siehe oben)
- produktive Secrets unter `/mnt/user/appdata/secrets/authelia_*.txt` werden **nicht** gemountet
## Test-Ziel
- Restore-Lab: `/mnt/user/backups/restore-lab/authelia`
- Testdatenpfade:
- `/mnt/user/backups/restore-lab/authelia/config` (restaurierte Originalkonfiguration + `configuration.yml.original`)
- `/mnt/user/backups/restore-lab/authelia/test-config` (Runtime-Mount mit minimaler Test-`configuration.yml`)
- `/mnt/user/backups/restore-lab/authelia/postgres` (Test-Postgres-Datadir)
- `/mnt/user/backups/restore-lab/authelia/dumps/latest/postgresql17-authelia.dump` (falls extrahiert)
- `/mnt/user/backups/restore-lab/authelia/test-config/notifier/notifications.txt` (Filesystem-Notifier-Ausgabe)
- Testcontainer:
- `restoretest-authelia` (Image-Pin wie Produktion)
- `restoretest-authelia-postgres` (postgres:18.4, gleiche Major wie shared Postgres)
- Testport: `127.0.0.1:19091:9091`
- Report-Ziel: `/mnt/user/backups/restore-reports/authelia-YYYY-MM-DD.md`
## Schutzregeln
- produktive Pfade `/mnt/user/appdata/authelia/*` werden **nicht** beschrieben
- produktive Secret-Dateien `/mnt/user/appdata/secrets/authelia_*.txt` werden **nicht** gemountet
- produktive shared PostgreSQL 18 wird **nicht** angesprochen (`test-config/configuration.yml` definiert nur Test-Postgres)
- echter SMTP-Versand wird **nicht** ausgeloest (`test-config/configuration.yml` definiert nur Filesystem-Notifier)
- produktive Domain `auth.kaleschke.info` wird **nicht** uebernommen
- Testcontainer publishen nur auf `127.0.0.1`, keine LAN-/Tailscale-Bindung
- Borg-Passphrase wird aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` gelesen und nirgendwo geloggt
## Geplanter Ablauf
1. Restore-Lab-Pfade leer anlegen
2. `local/appdata/authelia/config` aus dem aktuellsten Borg-Archiv extrahieren
3. minimale `test-config/configuration.yml` erzeugen; restaurierte Begleitdateien wie `users_database.yml` bleiben im Runtime-Mount, produktive externe Abhaengigkeiten werden nicht uebernommen; `notifier` auf Filesystem, `ntp.disable_startup_check: true`, `storage` auf Test-Postgres
4. Test-Postgres mit `ops/restore-tests/authelia-compose.test.yml` **frisch** hochfahren (keine Daten aus Dump - siehe Encryption-Key-Begruendung oben)
5. `authelia config validate` gegen `test-config/configuration.yml` laufen lassen
6. `restoretest-authelia` starten und HTTP-Health `http://127.0.0.1:19091/api/health` pollen
7. Report unter `/mnt/user/backups/restore-reports/authelia-YYYY-MM-DD.md` schreiben
8. Testcontainer stoppen und Restore-Lab bereinigen (`--keep-data` ueberschreibt)
## Smoke-Test
Minimal erfolgreich:
- Borg-Extract der Authelia-Config gelingt
- Test-Postgres startet `healthy`
- `authelia config validate` laeuft ohne Fehler durch
- HTTP `200` auf `/api/health` innerhalb 120 s
Optional spaeter:
- vollstaendigen Auth-Flow gegen Test-User aus `users_database.yml` durchspielen
- WebAuthn-Endpunkt /api/secondfactor/webauthn pruefen
- ForwardAuth-Pfad gegen Mock-Backend testen
## Bekannte Komplikationen
| Risiko | Beschreibung | Mitigation |
|---|---|---|
| Testkonfig-Schema-Drift | Authelia erwartet nach Upgrade andere Keys in der Minimal-Konfig | bei `config validate`-Fehler Test-Block im Skript anpassen |
| SMTP-Startup-Check blockiert Start | Wenn Authelia trotz `disable_startup_check` SMTP probiert | Container-Logs lesen, ggf. Notifier-Block weiter haerten |
| NTP-Lookup im Test-Netz | Container hat keinen DNS-Resolver fuer `time.cloudflare.com` | im Smoke per `ntp.disable_startup_check: true` deaktiviert |
| Storage-Encryption-Key vs. Dump | siehe "Bewusst nicht Teil dieses Tests" - der Smoke laeuft FRISCH ohne Dump | by design - Daten-Decrypt-Drill ist separate Aufgabe |
| identity_validation Schema-Drift | Aelteres/neueres Authelia-Schema erwartet andere Keys | Validate-Config Output lesen, ggf. Test-Block anpassen |
| users_database.yml mit produktiven Hashes | Daten werden ins Restore-Lab kopiert, aber niemals gemountet auf produktive Domain | OK; Testpfad ist isoliert, kein Browser-Zugang ueber LAN |
## Status
- Skript- und Compose-Scaffold abgelegt am 2026-06-02
- Erstlauf am 2026-06-03 erfolgreich: Config aus Borg, minimale Test-Konfiguration, frisches Test-Postgres, HTTP `/api/health` `200`, Report `/mnt/user/backups/restore-reports/authelia-2026-06-03.md`
- Fuer die Rotation vorgesehen: zweiter Samstag in geraden Monaten, 07:30
+311
View File
@@ -0,0 +1,311 @@
#!/bin/bash
set -euo pipefail
# Authelia Restore Smoke Test
#
# Nicht-destruktiver Restore-Smoke-Test fuer Authelia.
#
# Was dieser Smoke nachweist:
# - Authelia-Config kann aus dem produktiven Borg-Archiv extrahiert werden
# - die restaurierten Begleitdateien (users_database.yml etc.) sind lesbar
# - eine minimale Test-Konfiguration, die diese Begleitdateien nutzt und
# produktive externe Abhaengigkeiten (Postgres/SMTP) durch Wegwerf-Backends
# ersetzt, ist gegen den produktiven Authelia-Image-Pin valide
# (`authelia config validate`)
# - Authelia startet damit gegen ein frisches Test-Postgres und antwortet
# auf `/api/health`
#
# Was dieser Smoke bewusst NICHT nachweist:
# - Daten-Restore des produktiven authelia.dump. Authelia verschluesselt
# Storage-Werte mit AUTHELIA_STORAGE_ENCRYPTION_KEY; ein Restore mit
# produktiven Daten in eine Test-Instanz mit Wegwerf-Encryption-Key
# schlaegt im Startup-Check fehl ("the configured encryption key does
# not appear to be valid for this database"). Daten-Decrypt ist eine
# eigene DR-Aufgabe mit kontrollierter Schluessel-Verwendung, nicht
# Teil dieses Smokes. Frische des Dumps wird ueber
# check-restore-freshness.sh ueberwacht.
# - vollstaendiger Login-/2FA-/ForwardAuth-Flow.
#
# Produktive Authelia-Container, produktive Postgres-DB, produktive Secrets
# und produktiver SMTP-Versand werden NICHT angefasst.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
WHATIF=0
KEEP_DATA=0
for arg in "$@"; do
case "$arg" in
--what-if) WHATIF=1 ;;
--keep-data) KEEP_DATA=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
RESTORE_ROOT="/mnt/user/backups/restore-lab/authelia"
RESTORED_CONFIG_DIR="$RESTORE_ROOT/config"
TEST_CONFIG_DIR="$RESTORE_ROOT/test-config"
REPORT_ROOT="/mnt/user/backups/restore-reports"
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/authelia-extract"
COMPOSE_FILE="$SCRIPT_DIR/authelia-compose.test.yml"
REPORT_FILE="$REPORT_ROOT/authelia-$(date +%F).md"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Authelia restore test
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
ReportRoot: $REPORT_ROOT
Expected Borg source paths:
- local/appdata/authelia/config
Planned isolation:
- Test-Postgres: postgres:18.4 mit Wegwerf-Credentials, FRISCH
- Test-Authelia: authelia/authelia:4.39.20 (Image-Pin wie Produktion)
- Wegwerf-Secrets ausschliesslich im Test-Compose
- test-config/configuration.yml wird im Restore-Lab erzeugt:
* storage -> Test-Postgres (kein produktives Postgres erreicht)
* notifier -> Filesystem (KEIN SMTP-Versand)
* session -> lokaler Smoke ohne produktive Session-Secrets
* ntp -> disable_startup_check (kein DNS im isolierten Test-Netz)
- Test endpoint: 127.0.0.1:19091/api/health (no Traefik, no public domain)
Bewusst NICHT Teil dieses Smokes:
- pg_restore von postgresql17-authelia.dump. Authelia verschluesselt
Storage-Werte mit AUTHELIA_STORAGE_ENCRYPTION_KEY; ein Restore in eine
Test-Instanz mit Wegwerf-Key ist by design nicht boot-faehig.
Dump-Frische wird via check-restore-freshness.sh ueberwacht.
Smoke-Test:
- authelia config validate gegen test-config/configuration.yml
- HTTP 200 von /api/health
EOF
exit 0
fi
require_cmd docker
require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
RESTORE_SUCCESS=0
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "authelia" "$RESTORE_ROOT"
rm -rf "$EXTRACT_DIR"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
rm -rf "$EXTRACT_DIR"
}
trap cleanup EXIT
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
mkdir -p "$RESTORED_CONFIG_DIR" "$TEST_CONFIG_DIR" "$RESTORE_ROOT/postgres"
archive="$(latest_archive_name)"
repo="$(borg_repo_url)"
if [ -z "$archive" ] || [ -z "$repo" ]; then
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
exit 1
fi
# Stufe 1: Config aus Borg extrahieren
borg_extract "/restore/authelia-extract" "local/appdata/authelia/config"
if [ ! -d "$EXTRACT_DIR/local/appdata/authelia/config" ]; then
echo "Authelia config path missing in Borg archive" >&2
exit 1
fi
cp -a "$EXTRACT_DIR/local/appdata/authelia/config/." "$RESTORED_CONFIG_DIR/"
# Stufe 2: Minimale Test-Konfiguration erzeugen.
# Die restaurierte Originalkonfig bleibt als Diagnosematerial erhalten. Der
# Smoke nutzt bewusst eine neu geschriebene Test-Config, damit keine produktiven
# Blocks (SMTP, echtes Postgres, Session/JWT-Altkeys) hineinmergen koennen.
ORIGINAL_CONFIG_FILE="$RESTORED_CONFIG_DIR/configuration.yml"
TEST_CONFIG_FILE="$TEST_CONFIG_DIR/configuration.yml"
if [ ! -f "$ORIGINAL_CONFIG_FILE" ]; then
echo "configuration.yml missing in restored config dir" >&2
exit 1
fi
# Kopiere alle Begleitdateien (z. B. users_database.yml) in einen separaten
# Runtime-Mount. configuration.yml wird danach vollstaendig neu geschrieben.
cp -a "$RESTORED_CONFIG_DIR/." "$TEST_CONFIG_DIR/"
cp "$ORIGINAL_CONFIG_FILE" "$RESTORED_CONFIG_DIR/configuration.yml.original"
cat > "$TEST_CONFIG_FILE" <<'YAML'
---
# Minimal-Konfiguration nur fuer den Restore-Smoke.
theme: dark
server:
address: tcp://0.0.0.0:9091
log:
level: info
authentication_backend:
file:
path: /config/users_database.yml
password:
algorithm: argon2id
iterations: 3
key_length: 32
salt_length: 16
memory: 65536
parallelism: 4
access_control:
# Authelia 4.39 verlangt: wenn KEINE Regeln gesetzt sind, muss default_policy
# 'two_factor' oder 'one_factor' sein. 'bypass' ist als Default-Policy ohne
# explizite Regeln nicht erlaubt. Fuer den Smoke ist das egal: /api/health
# ist ein public Endpunkt und laeuft nicht durch access_control.
default_policy: two_factor
regulation:
max_retries: 3
find_time: 2m
ban_time: 5m
totp:
issuer: kaleschke.info
period: 30
skew: 1
storage:
postgres:
address: tcp://restoretest-authelia-postgres:5432
database: authelia
username: authelia
# Passwort kommt ueber AUTHELIA_STORAGE_POSTGRES_PASSWORD ENV.
notifier:
disable_startup_check: true
filesystem:
filename: /config/notifier/notifications.txt
ntp:
# Test-Netz hat keinen DNS-Resolver fuer time.cloudflare.com; ohne diesen
# Schalter loggt Authelia "Could not determine the clock offset" und der
# Startup-Check kann fehlschlagen.
disable_startup_check: true
session:
cookies:
- name: authelia_session_restoretest
domain: kaleschke.info
authelia_url: https://auth.kaleschke.info
default_redirection_url: https://glance.kaleschke.info
expiration: 1h
inactivity: 5m
identity_validation:
reset_password:
jwt_secret: restoretest-authelia-reset-password-jwt-secret-placeholder-64bytes
jwt_lifespan: 5m
jwt_algorithm: HS256
YAML
mkdir -p "$TEST_CONFIG_DIR/notifier"
chmod -R a+rwX "$TEST_CONFIG_DIR/notifier"
# Stufe 3: Test-Postgres hochfahren (FRISCH, keine Daten aus Dump).
# Authelia legt sein Schema beim ersten Start selbst an und schreibt eine
# Encryption-Probe mit AUTHELIA_STORAGE_ENCRYPTION_KEY. Ein Restore des
# produktiven authelia.dump in diese Instanz wuerde die Encryption-Probe
# mit einem anderen Key vorbelegen und Authelia beim Startup-Check
# ablehnen lassen ("the configured encryption key does not appear to be
# valid for this database"). Genau aus diesem Grund laeuft der Smoke
# bewusst auf einer leeren DB. Frische des produktiven Dumps wird
# separat in check-restore-freshness.sh ueberwacht.
docker compose -f "$COMPOSE_FILE" up -d restoretest-authelia-postgres >/dev/null
until docker exec restoretest-authelia-postgres pg_isready -U authelia -d authelia >/dev/null 2>&1; do
sleep 2
done
# Stufe 4: config validate im Container-Kontext, gegen minimale Test-Config
validate_status="ok"
if ! docker run --rm \
-e AUTHELIA_SESSION_SECRET=restoretest-authelia-session-secret-placeholder-32 \
-e AUTHELIA_STORAGE_ENCRYPTION_KEY=restoretest-authelia-storage-enc-key-placeholder-32 \
-e AUTHELIA_STORAGE_POSTGRES_PASSWORD=restoretest-authelia-db \
-v "$TEST_CONFIG_DIR:/config" \
authelia/authelia:4.39.20@sha256:1b363e9279e742397966333f364e0876ae02bf5c876de73e83af6d48c57ff51b \
authelia config validate --config /config/configuration.yml \
>/tmp/authelia-validate.log 2>&1; then
validate_status="failed"
cat /tmp/authelia-validate.log >&2
exit 1
fi
# Stufe 5: Authelia-Container starten. Das Compose nutzt test-config als
# /config-Mount mit isolierten Test-Backends.
docker compose -f "$COMPOSE_FILE" up -d restoretest-authelia >/dev/null
http_status=""
for _ in $(seq 1 60); do
http_status="$(curl -s -o /tmp/authelia-body.html -w '%{http_code}' \
http://127.0.0.1:19091/api/health || true)"
if [ "$http_status" = "200" ]; then
break
fi
sleep 2
done
if [ "$http_status" != "200" ]; then
echo "Authelia HTTP health failed: status=$http_status" >&2
docker logs --tail 120 restoretest-authelia >&2 || true
exit 1
fi
write_report "$REPORT_FILE" <<EOF
# Authelia Restore Test Report - $(date +%F)
- Service: \`authelia\`
- Source repo: \`$repo\`
- Archive: \`$archive\`
- Restore root: \`$RESTORE_ROOT\`
- Test containers:
- \`restoretest-authelia\`
- \`restoretest-authelia-postgres\` (fresh schema, no productive dump)
- Test endpoint: \`http://127.0.0.1:19091/api/health\`
- Result: \`SUCCESS\`
## Checks
- Borg extract of config: \`ok\`
- configuration.yml present in archive: \`ok\`
- test runtime configuration.yml written: \`ok\`
- \`authelia config validate\`: \`$validate_status\`
- HTTP /api/health status: \`$http_status\`
## Scope
Dieser Smoke prueft: Borg-Restore der Config, Validate gegen Produktions-Image,
Authelia-Boot gegen frische Test-Postgres + Wegwerf-Encryption-Key,
HTTP-Health-Endpoint antwortet.
Bewusst NICHT Teil des Smokes: pg_restore des produktiven authelia.dump.
Authelia verschluesselt Storage-Werte mit \`AUTHELIA_STORAGE_ENCRYPTION_KEY\`;
ein Restore mit produktiven Daten in eine Test-Instanz mit Wegwerf-Key
schlaegt im Startup-Check by design fehl. Frische des produktiven Dumps
wird in \`check-restore-freshness.sh\` ueberwacht; Daten-Decrypt-Drill ist
eine separate DR-Aufgabe.
## Notes
- Test ran without Traefik and without the productive domain \`auth.kaleschke.info\`.
- Productive Authelia secrets under \`/mnt/user/appdata/secrets/authelia_*.txt\` were NOT mounted.
- Notifier was forced to filesystem (\`/config/notifier/notifications.txt\`); no SMTP call to GMX.
- Storage forced to isolated test postgres; productive shared PostgreSQL 18 was NOT touched.
- NTP startup-check disabled in test config (kein DNS-Resolver im isolierten Compose-Netz).
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
RESTORE_SUCCESS=1
echo "Authelia restore test ok -> $REPORT_FILE"
+86
View File
@@ -0,0 +1,86 @@
# Authelia Restore Runbook
## Status
Skript und Test-Compose sind validiert. **Erstlauf 2026-06-03 erfolgreich**: Config aus Borg extrahiert, minimale Test-Konfiguration validiert, frisches Test-Postgres gestartet, HTTP `/api/health` `200`. Report: `/mnt/user/backups/restore-reports/authelia-2026-06-03.md`. Authelia ist Tier-1-kritisch, deshalb bleibt dieser Test bewusst konservativ: Smoke-Test prueft nur Config-Validate + HTTP-Health, kein vollstaendiger Auth-Flow und kein produktiver Dump-Restore.
## Vorbedingungen
- Borg-Quelle ist verfuegbar
- `borg-ui`-Container laeuft
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
- `borg-ui` mountet die Passphrase im Container als `/local/secrets/borg_repo_passphrase.txt`
- aktuelles Borg-Archiv enthaelt `local/appdata/authelia/config`
- optional: `local/borg-dumps/latest/postgresql17-authelia.dump`
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
- Port `127.0.0.1:19091` frei
- freier Speicher unter `/mnt/user/backups/restore-lab/authelia` (~200 MB reichen)
## Bestaetigter Host-Stand (Soll)
- produktiver Authelia-Container: `authelia` mit Image `authelia/authelia:4.39.20@sha256:1b363e9279e742397966333f364e0876ae02bf5c876de73e83af6d48c57ff51b`
- produktiver Config-Pfad: `/mnt/user/appdata/authelia/config`
- produktive Secrets: `/mnt/user/appdata/secrets/authelia_*.txt` (werden vom Test **nicht** gebraucht)
- produktive Storage: shared PostgreSQL 18 (wird vom Test **nicht** angesprochen)
## Erster Lauf - trockene Variante
```bash
bash /mnt/user/services/homelab-infra/ops/restore-tests/authelia-restore-test.sh --what-if
```
Erwartete Ausgabe: nur Plan-Output, kein Docker-Start, kein Borg-Extract.
## Erster Lauf - echter Test (Operator-freigegeben)
```bash
bash /mnt/user/services/homelab-infra/ops/restore-tests/authelia-restore-test.sh --keep-data
```
Bei Erfolg:
- Report unter `/mnt/user/backups/restore-reports/authelia-YYYY-MM-DD.md`
- Restore-Lab-Daten bleiben mit `--keep-data` erhalten
- ohne `--keep-data` wird das Restore-Lab geloescht; bei Fehler wird es nach `/mnt/user/backups/restore-lab/_failed/authelia-...` verschoben
## Smoke-Test-Pruefungen
Minimal erwartet im Report:
- Borg extract of config: `ok`
- Test-Postgres healthy
- `authelia config validate`: `ok`
- HTTP /api/health status: `200`
## Fehlerfaelle
| Symptom | Ursache | Massnahme |
|---|---|---|
| `config validate` failt mit `notifier` Block | Testkonfig enthaelt mehr als einen Notifier | `test-config/configuration.yml` pruefen; Minimal-Test-Block im Skript anpassen |
| `config validate` failt mit `session.domain` | aelteres/neueres Schema | Test-`session:`-Block an reales Authelia-Schema anpassen |
| `config validate` failt mit `access_control` default_policy | Authelia >=4.39 verlangt ohne Rules `two_factor`/`one_factor` | Test-Block ist bereits auf `two_factor` gesetzt; bei weiterer Schema-Aenderung anpassen |
| HTTP-Timeout 120 s | Authelia haengt in Postgres-Schema-Migration | `docker logs --tail 200 restoretest-authelia` lesen, ggf. Wartezeit erhoehen |
| `encryption key does not appear to be valid for this database` | jemand hat `pg_restore` des produktiven Dumps wieder eingebaut | `pg_restore` ist seit `2026-06-03` bewusst NICHT mehr Teil dieses Smokes - siehe Plan/Skript-Doku; nicht re-aktivieren ohne kontrollierte Encryption-Key-Choreographie |
| SMTP-Connect im Log | Testkonfig oder Env erzeugt unerwartet SMTP | `test-config/configuration.yml` und `AUTHELIA_*SMTP*` Env pruefen |
| `Could not determine the clock offset` | DNS-Lookup `time.cloudflare.com` failt im isolierten Test-Netz | `ntp.disable_startup_check: true` ist im Test-Config-Block bereits gesetzt; bei Aenderung beibehalten |
| `configuration environment variable not expected: AUTHELIA__SERVER__ADDRESS` | Doppel-Underscore ENV im Compose | seit `2026-06-03` entfernt; `server.address` kommt aus configuration.yml |
## Cleanup
- bei Erfolg ohne `--keep-data`: `rm -rf /mnt/user/backups/restore-lab/authelia` und Extract-Cache
- bei Fehler: Datenpfad wird via `preserve_on_failure` nach `/mnt/user/backups/restore-lab/_failed/authelia-...` umbenannt
Produktive Authelia-Container, produktive Secrets, produktive Postgres-DB und produktiver SMTP-Account werden niemals beruehrt.
## Schedule
Empfohlener Schedule nach erfolgreichem Erstlauf: zweimonatlich (2. Samstag in geraden Monaten), damit nicht mit Paperless kollidierend.
## Festgelegte Entscheidungen
- Test-Compose nutzt denselben Image-Digest wie Produktion.
- Wegwerf-Secrets ausschliesslich im Test-Compose; niemals produktive Authelia-Secrets einsetzen.
- Test-Postgres ist isoliert; produktive shared PostgreSQL 18 wird nicht angesprochen.
- Notifier wird auf Filesystem umgebogen; KEIN echter SMTP-Versand.
- Test-Port nur auf `127.0.0.1:19091`, keine LAN-/Traefik-Anbindung.
- Borg-Passphrase wird aus Host-Secret-Datei gelesen und nirgendwo geloggt.
@@ -25,6 +25,65 @@ check_file_age_days() {
echo $(( (now_epoch - mtime) / 86400 ))
}
# pg_restore --list als billiger Header-Check fuer Custom-Format-Dumps;
# erkennt Korruption, die mit reinem "exists+nonempty" durchrutscht. Wir
# brauchen kein laufendes Postgres; der Check liest nur die Toc-Section.
PG_DUMPS="postgresql17-paperless.dump postgresql17-mailarchiver.dump postgresql17-authelia.dump mealie.dump immich.dump nextcloud.dump"
is_pg_custom_dump() {
case " $PG_DUMPS " in *" $1 "*) return 0;; *) return 1;; esac
}
pg_header_ok() {
local path="$1"
if ! command -v pg_restore >/dev/null 2>&1; then
# ohne Host-pg_restore: in laufendem Postgres-Container probieren
if command -v docker >/dev/null 2>&1 && docker inspect postgresql17 >/dev/null 2>&1; then
docker exec -i postgresql17 pg_restore --list </"$path" >/dev/null 2>&1 && return 0
fi
return 2 # nicht pruefbar
fi
pg_restore --list "$path" >/dev/null 2>&1
}
check_pg_header() {
local dump="$1"
local path="$2"
local age="$3"
local missing_mode="${4:-critical}"
if [ ! -f "$path" ]; then
if [ "$missing_mode" = "optional" ]; then
info+=("DUMP_OPTIONAL_MISSING $dump")
else
critical+=("DUMP_MISSING $dump")
fi
return
fi
if [ ! -s "$path" ]; then
critical+=("DUMP_EMPTY $dump")
return
fi
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
if [ "$missing_mode" = "optional" ]; then
warnings+=("DUMP_OPTIONAL_STALE $dump age=${age}h")
else
critical+=("DUMP_STALE $dump age=${age}h")
fi
return
fi
if pg_header_ok "$path"; then
rc=0
else
rc=$?
fi
case "$rc" in
0) info+=("DUMP_OK $dump age=${age}h header=ok") ;;
1) critical+=("DUMP_HEADER_INVALID $dump (pg_restore --list failed)") ;;
2) info+=("DUMP_OK $dump age=${age}h header=unchecked") ;;
esac
}
for dump in \
postgresql17-paperless.dump \
postgresql17-mailarchiver.dump \
@@ -48,11 +107,24 @@ for dump in \
age="$(check_file_age_hours "$path")"
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
critical+=("DUMP_STALE $dump age=${age}h")
continue
fi
if is_pg_custom_dump "$dump"; then
check_pg_header "$dump" "$path" "$age"
else
info+=("DUMP_OK $dump age=${age}h")
fi
done
optional_dump="postgresql17-authelia.dump"
optional_path="$DUMP_ROOT/$optional_dump"
optional_age=0
if [ -f "$optional_path" ]; then
optional_age="$(check_file_age_hours "$optional_path")"
fi
check_pg_header "$optional_dump" "$optional_path" "$optional_age" optional
for service in vaultwarden gitea paperless; do
if [ ! -d "$REPORT_ROOT" ]; then
warnings+=("REPORT_ROOT_MISSING $REPORT_ROOT")
+42
View File
@@ -20,7 +20,28 @@ require_path() {
}
}
require_borg_container() {
docker inspect "$BORG_CONTAINER" >/dev/null 2>&1 || {
echo "Missing Borg container: $BORG_CONTAINER" >&2
exit 1
}
[ "$(docker inspect -f '{{.State.Running}}' "$BORG_CONTAINER" 2>/dev/null)" = "true" ] || {
echo "Borg container is not running: $BORG_CONTAINER" >&2
exit 1
}
docker exec "$BORG_CONTAINER" test -r /data/borg.db >/dev/null 2>&1 || {
echo "Missing borg-ui database in container: $BORG_CONTAINER:/data/borg.db" >&2
exit 1
}
docker exec "$BORG_CONTAINER" test -r /local/secrets/borg_repo_passphrase.txt >/dev/null 2>&1 || {
echo "Missing Borg passphrase in container: $BORG_CONTAINER:/local/secrets/borg_repo_passphrase.txt" >&2
echo "Host path exists, but borg-ui must mount it as /local/secrets/borg_repo_passphrase.txt." >&2
exit 1
}
}
latest_archive_name() {
require_borg_container
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
import sqlite3
conn = sqlite3.connect('/data/borg.db')
@@ -34,6 +55,7 @@ PY
}
borg_repo_url() {
require_borg_container
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
import sqlite3
conn = sqlite3.connect('/data/borg.db')
@@ -50,6 +72,7 @@ borg_extract() {
local extract_dir="$1"
shift
local paths=("$@")
require_borg_container
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
import os, sys, subprocess
extract_dir = sys.argv[1]
@@ -88,3 +111,22 @@ cleanup_compose() {
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
fi
}
# Hilfsfunktion: bei Fehler-Exit Restore-Lab-Pfad nicht loeschen, sondern in
# einen `_failed/<service>-<date>-<pid>`-Pfad umbenennen, damit Post-Mortem
# moeglich bleibt. Aufrufer setzt vor Erfolg `RESTORE_SUCCESS=1`.
RESTORE_FAILED_ROOT="${RESTORE_FAILED_ROOT:-/mnt/user/backups/restore-lab/_failed}"
preserve_on_failure() {
local service="$1"
local path="$2"
if [ ! -e "$path" ]; then
return 0
fi
mkdir -p "$RESTORE_FAILED_ROOT"
local target="$RESTORE_FAILED_ROOT/${service}-$(date +%F)-$$"
if mv "$path" "$target" 2>/dev/null; then
echo "preserved failed restore data: $target" >&2
else
echo "failed to preserve restore data: $path -> $target" >&2
fi
}
+10 -3
View File
@@ -37,8 +37,14 @@ require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
RESTORE_SUCCESS=0
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "gitea" "$RESTORE_ROOT"
rm -rf "$EXTRACT_DIR"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$DATA_DIR"
fi
@@ -61,9 +67,9 @@ sleep 8
status="$(curl -s -o /tmp/gitea-body.html -w '%{http_code}' http://127.0.0.1:13000)"
grep -qi "Gitea" /tmp/gitea-body.html
if timeout 5 bash -lc '</dev/tcp/127.0.0.1/12222' >/dev/null 2>&1; then
ssh_state="open"
ssh_state="tcp-open"
else
echo "Gitea SSH port not reachable" >&2
echo "Gitea SSH port not reachable (TCP connect failed)" >&2
exit 1
fi
@@ -85,7 +91,7 @@ write_report "$REPORT_FILE" <<EOF
- Borg extract into isolated restore-lab: \`ok\`
- HTTP status: \`$status\`
- HTML content: \`Gitea\`
- SSH port: \`$ssh_state\`
- SSH TCP port: \`$ssh_state\` (TCP connect only, not a full SSH handshake)
- Repository sample: \`$repo_sample\`
## Notes
@@ -94,4 +100,5 @@ write_report "$REPORT_FILE" <<EOF
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
RESTORE_SUCCESS=1
echo "Gitea restore test ok -> $REPORT_FILE"
+2 -2
View File
@@ -62,7 +62,7 @@ Wenn das Archiv den Pfad anders ablegt, zuerst mit `borg list "$BORG_REPO" "::AR
3. Testcontainer starten
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml up -d
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/gitea-compose.test.yml up -d
```
4. Smoke-Test
@@ -83,7 +83,7 @@ Minimal erfolgreich:
5. Testcontainer wieder stoppen
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml down
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/gitea-compose.test.yml down
```
6. Report schreiben
+7
View File
@@ -64,8 +64,14 @@ require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
RESTORE_SUCCESS=0
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "immich" "$RESTORE_ROOT"
rm -rf "$EXTRACT_DIR"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
@@ -244,4 +250,5 @@ write_report "$REPORT_FILE" <<EOF
- Restore-Quelle Dump: \`local/borg-dumps/latest/immich.dump\` aus aktuellem Borg-Archiv.
EOF
RESTORE_SUCCESS=1
echo "Immich restore test ok -> $REPORT_FILE"
@@ -53,8 +53,13 @@ fi
require_cmd docker
require_path "$COMPOSE_FILE"
RESTORE_SUCCESS=0
cleanup() {
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down -v >/dev/null 2>&1 || true
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "komodo-bootstrap" "$RESTORE_ROOT"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
@@ -132,4 +137,5 @@ write_report "$REPORT_FILE" <<EOF
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
EOF
RESTORE_SUCCESS=1
echo "Komodo bootstrap trockenlauf ok -> $REPORT_FILE"
@@ -0,0 +1,19 @@
services:
restoretest-komodo-mongorestore:
image: mongo:7.0.32@sha256:32979a1189dfdc44da3f5ed40d910495f5ad8f6f7f77556646f890a30b2d3f56
container_name: restoretest-komodo-mongorestore
restart: "no"
command: --quiet
environment:
MONGO_INITDB_ROOT_USERNAME: komodo
MONGO_INITDB_ROOT_PASSWORD: restoretest-komodo-mongo-pwd
volumes:
- /mnt/user/backups/restore-lab/komodo-mongo-restore/mongo:/data/db
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
security_opt:
- no-new-privileges:true
@@ -0,0 +1,163 @@
#!/bin/bash
set -euo pipefail
# Komodo Mongo Daten-Restore Test
#
# Baut auf dem bestehenden Komodo-Bootstrap-Test auf und fuegt hinzu:
# - mongorestore von komodo-mongo.archive.gz in die Test-Mongo
# - Liest danach die stack-Collection, um zu pruefen, dass Komodo-
# Stack-Definitionen wiederhergestellt sind
#
# Das ist der Test, der im DR-Fall beweist, dass die KOMODO_*-Stack-
# ENV-Werte aus dem Mongo-Dump rekonstruiert werden koennen (die
# kanonische Quelle gemaess docs/DISASTER_RECOVERY.md 6.2.1).
#
# Produktive Komodo-Container und produktive Mongo-Datadir werden
# NICHT angefasst.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
WHATIF=0
KEEP_DATA=0
for arg in "$@"; do
case "$arg" in
--what-if) WHATIF=1 ;;
--keep-data) KEEP_DATA=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
RESTORE_ROOT="/mnt/user/backups/restore-lab/komodo-mongo-restore"
REPORT_ROOT="/mnt/user/backups/restore-reports"
COMPOSE_FILE="$SCRIPT_DIR/komodo-mongo-restore-compose.test.yml"
PROJECT_NAME="restoretest-komodo-mongorestore"
REPORT_FILE="$REPORT_ROOT/komodo-mongo-restore-$(date +%F).md"
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/komodo-mongo.archive.gz"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Komodo Mongo Daten-Restore Test
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
ReportRoot: $REPORT_ROOT
DumpPath: $DUMP_HOST_PATH
Planned steps:
1. Frische Test-Mongo hochfahren (gleiche Compose wie Bootstrap-Test)
2. mongorestore --archive --gzip aus $DUMP_HOST_PATH
3. Stack-Collection auslesen (Beweis: Stack-Definitionen sind da)
4. Report schreiben
5. Cleanup
EOF
exit 0
fi
require_cmd docker
require_path "$COMPOSE_FILE"
require_path "$DUMP_HOST_PATH"
RESTORE_SUCCESS=0
cleanup() {
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down -v >/dev/null 2>&1 || true
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "komodo-mongo-restore" "$RESTORE_ROOT"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
}
trap cleanup EXIT
rm -rf "$RESTORE_ROOT"
mkdir -p "$RESTORE_ROOT/mongo" "$RESTORE_ROOT/core" "$RESTORE_ROOT/keys" "$RESTORE_ROOT/periphery"
# Stufe 1: Nur Test-Mongo starten (kein Core/Periphery noetig fuer Dump-Restore)
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" up -d \
restoretest-komodo-mongorestore >/dev/null
mongo_ok=0
for _ in $(seq 1 30); do
s="$(docker inspect restoretest-komodo-mongorestore --format '{{.State.Health.Status}}' 2>/dev/null || true)"
if [ "$s" = "healthy" ]; then mongo_ok=1; break; fi
sleep 2
done
if [ "$mongo_ok" -ne 1 ]; then
echo "Test-Mongo never reported healthy" >&2
docker logs --tail 80 restoretest-komodo-mongorestore >&2 || true
exit 1
fi
# Stufe 2: mongorestore aus dem Host-Dump
# --drop loescht existierende Collections vor dem Restore (frische DB, also harmlos).
# --gzip weil der Dump als .archive.gz erzeugt wurde.
# Auth mit den Wegwerf-Credentials aus dem Test-Compose.
restore_status="ok"
# --noIndexRestore: der Smoke prueft nur, dass Daten lesbar sind, nicht dass
# alle Indexe sauber aufgebaut werden. mongorestore scheitert sonst am
# Index-Rebuild weil der Test-User keine dbAdmin-Rolle hat. Fuer den
# DR-Nachweis (Stack-ENV-Werte lesbar) reicht das.
if ! docker exec -i restoretest-komodo-mongorestore \
mongorestore --archive --gzip --noIndexRestore \
-u komodo -p restoretest-komodo-mongo-pwd --authenticationDatabase admin \
--drop \
< "$DUMP_HOST_PATH" 2>/tmp/komodo-mongorestore.err; then
restore_status="failed"
cat /tmp/komodo-mongorestore.err >&2
exit 1
fi
# Stufe 3: Stack-Collection auslesen
# Komodo speichert Stack-Definitionen in der DB "komodo", Collection "Stack"
# (oder "stack" je nach Version). Wir zaehlen Dokumente als Beweis.
stack_count="n/a"
for coll in Stack stack; do
count="$(docker exec restoretest-komodo-mongorestore mongosh --quiet \
-u komodo -p restoretest-komodo-mongo-pwd --authenticationDatabase admin \
--eval "db.getSiblingDB('komodo').getCollection('$coll').countDocuments({})" \
2>/dev/null | tr -d '[:space:]' || true)"
if [ -n "$count" ] && [ "$count" != "0" ] && [ "$count" != "n/a" ]; then
stack_count="$count (collection: $coll)"
break
fi
done
# Alle DBs + Collections auflisten als zusaetzlicher Nachweis
db_list="$(docker exec restoretest-komodo-mongorestore mongosh --quiet \
-u komodo -p restoretest-komodo-mongo-pwd --authenticationDatabase admin \
--eval "db.adminCommand({listDatabases:1}).databases.map(d=>d.name).join(', ')" \
2>/dev/null | tr -d '\r' || echo "n/a")"
write_report "$REPORT_FILE" <<EOF
# Komodo Mongo Daten-Restore Test - $(date +%F)
- Dump: \`$DUMP_HOST_PATH\`
- Dump size: \`$(ls -lh "$DUMP_HOST_PATH" | awk '{print $5}')\`
- Project: \`$PROJECT_NAME\`
- Restore root: \`$RESTORE_ROOT\`
- Result: \`SUCCESS\`
## Checks
- Test-Mongo healthy: \`ok\`
- mongorestore --archive --gzip: \`$restore_status\`
- Databases after restore: \`$db_list\`
- Stack documents: \`$stack_count\`
## Scope
Dieser Test beweist, dass \`komodo-mongo.archive.gz\` in eine frische
Mongo-Instanz eingespielt werden kann und die Stack-Definitionen danach
lesbar sind. Im DR-Fall ist das die kanonische Quelle fuer
\`KOMODO_*\`-Stack-ENV-Werte (docs/DISASTER_RECOVERY.md 6.2.1).
## Notes
- Produktive Komodo-Container und produktive Mongo-Datadir wurden nicht beruehrt.
- Test-Mongo nutzt Wegwerf-Credentials (restoretest-komodo-mongo-pwd).
- Kein Komodo-Core gestartet (nicht noetig fuer Dump-Restore-Nachweis).
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
EOF
RESTORE_SUCCESS=1
echo "Komodo Mongo restore test ok -> $REPORT_FILE"
+1 -1
View File
@@ -3,7 +3,7 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TOPIC="${TOPIC:-homelab-info}"
TESTS="${TESTS:-vaultwarden gitea paperless}"
TESTS="${TESTS:-vaultwarden gitea paperless authelia}"
pick_random() {
printf '%s\n' $TESTS | awk 'BEGIN { srand() } { items[++count] = $0 } END { print items[int(rand() * count) + 1] }'
@@ -0,0 +1,66 @@
services:
restoretest-nextcloud-postgres:
# Gleiche Major-Version wie apps/nextcloud/docker-compose.yml in Produktion.
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
container_name: restoretest-nextcloud-postgres
restart: "no"
environment:
TZ: Europe/Berlin
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: restoretest-nextcloud-db
PGDATA: /var/lib/postgresql/18/docker
volumes:
- /mnt/user/backups/restore-lab/nextcloud/postgres:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nextcloud -d nextcloud"]
interval: 10s
timeout: 5s
retries: 10
security_opt:
- no-new-privileges:true
restoretest-nextcloud-redis:
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1
container_name: restoretest-nextcloud-redis
restart: "no"
command: redis-server --save "" --appendonly no
security_opt:
- no-new-privileges:true
restoretest-nextcloud:
# Gleicher Image-Digest wie apps/nextcloud/docker-compose.yml.
image: nextcloud:33.0.4-apache@sha256:caa40b8beaf0057ac213d8dfc515c36ce64f7a8f0825b6a287e6f7cf2f4a095d
container_name: restoretest-nextcloud
restart: "no"
depends_on:
restoretest-nextcloud-postgres:
condition: service_healthy
restoretest-nextcloud-redis:
condition: service_started
environment:
TZ: Europe/Berlin
POSTGRES_HOST: restoretest-nextcloud-postgres
POSTGRES_DB: nextcloud
POSTGRES_USER: nextcloud
POSTGRES_PASSWORD: restoretest-nextcloud-db
REDIS_HOST: restoretest-nextcloud-redis
NEXTCLOUD_ADMIN_USER: restoretest-admin
NEXTCLOUD_ADMIN_PASSWORD: restoretest-nextcloud-admin-pass
NEXTCLOUD_DATA_DIR: /var/www/html/data
# Bewusst keine Trusted-Domain/Proxy-Konfiguration: Smoke prueft
# nur localhost-HTTP, keine Traefik-Route.
ports:
# nur 127.0.0.1, keine Public-Route, keine Traefik-Labels
- "127.0.0.1:18180:80"
volumes:
# Restore-Lab-Pfade: alles isoliert, keine produktiven Mounts.
- /mnt/user/backups/restore-lab/nextcloud/html:/var/www/html
- /mnt/user/backups/restore-lab/nextcloud/data:/var/www/html/data
# KEIN no-new-privileges fuer den Smoke-Test-Container.
# Der Nextcloud-Entrypoint fuehrt intern chown/chmod auf /var/www/html
# und /var/www/html/data aus. Auf Unraid (FUSE/shfs) ignoriert das
# Host-Dateisystem chown-Aufrufe von aussen, deshalb muss der
# Container-Entrypoint die Rechte selbst setzen koennen. Im isolierten
# Smoke-Kontext (127.0.0.1, kein Traefik, Wegwerf-Daten) ist das
# vertretbar. Test-Postgres und Test-Redis behalten no-new-privileges.
+302
View File
@@ -0,0 +1,302 @@
#!/bin/bash
set -euo pipefail
# Nextcloud Restore Smoke Test
#
# Nicht-destruktiver Restore-Smoke-Test fuer Nextcloud.
#
# Was dieser Smoke nachweist:
# - Nextcloud-HTML und -Datenpfade koennen aus dem Borg-Archiv extrahiert werden
# - nextcloud.dump kann in eine isolierte Test-Postgres importiert werden
# - Nextcloud startet gegen die restaurierten Daten + Test-Redis und antwortet
# auf HTTP
# - occ status zeigt maintenance:mode = false
#
# Besonderheiten gegenueber den anderen Restore-Tests:
# - Nextcloud hat eine eigene Postgres (nicht shared), mit eigener DB-Rolle
# - Nextcloud nutzt eine eigene Redis-Instanz (Snapshot-Persistenz, kein Passwort)
# - occ maintenance:mode und die Rolle oc_admin sind im DR-Fall relevant;
# im Smoke pruefen wir occ status nach dem Boot
# - Produktive Secrets (admin_user, admin_password, postgres_password) werden
# durch Wegwerf-Werte im Test-Compose ersetzt
#
# Produktive Nextcloud-Container, produktive Postgres-DB, produktive Secrets,
# produktive Nutzdaten unter /mnt/user/documents/nextcloud-data und
# produktiver Traefik-Eintrag werden NICHT angefasst.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
WHATIF=0
KEEP_DATA=0
for arg in "$@"; do
case "$arg" in
--what-if) WHATIF=1 ;;
--keep-data) KEEP_DATA=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
RESTORE_ROOT="/mnt/user/backups/restore-lab/nextcloud"
REPORT_ROOT="/mnt/user/backups/restore-reports"
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/nextcloud-extract"
COMPOSE_FILE="$SCRIPT_DIR/nextcloud-compose.test.yml"
REPORT_FILE="$REPORT_ROOT/nextcloud-$(date +%F).md"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Nextcloud restore test
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
ReportRoot: $REPORT_ROOT
Expected Borg source paths:
- local/appdata/nextcloud/html (aus Borg-Archiv)
Host source paths:
- /mnt/user/backups/borg/dumps/latest/nextcloud.dump (vom Host, taeglich frisch)
Planned isolation:
- Test-Postgres: postgres:18.4 mit Wegwerf-Credentials
- Test-Redis: redis:8.8.0-alpine (rebuildbar, kein Restore)
- Test-Nextcloud: nextcloud:33.0.4-apache (Image-Pin wie Produktion)
- Wegwerf-Admin-Credentials im Test-Compose
- Produktive Secrets und Nutzdaten werden NICHT gemountet
- Test endpoint: 127.0.0.1:18180 (no Traefik, no public domain)
Smoke-Test:
- pg_restore -> nextcloud.dump
- HTTP 200/302/3xx von 127.0.0.1:18180
- occ status: maintenance=false
EOF
exit 0
fi
require_cmd docker
require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
RESTORE_SUCCESS=0
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "nextcloud" "$RESTORE_ROOT"
rm -rf "$EXTRACT_DIR"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
rm -rf "$EXTRACT_DIR"
}
trap cleanup EXIT
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
mkdir -p "$RESTORE_ROOT/html" "$RESTORE_ROOT/data" "$RESTORE_ROOT/postgres" "$RESTORE_ROOT/dumps/latest"
archive="$(latest_archive_name)"
repo="$(borg_repo_url)"
if [ -z "$archive" ] || [ -z "$repo" ]; then
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
exit 1
fi
# Stufe 1: Nextcloud-App-Pfade aus Borg, Dump vom Host.
# HTML (App-Code + config) kommt aus dem Borg-Archiv.
# Der Dump liegt frisch auf dem Host unter /mnt/user/backups/borg/dumps/latest/
# (wird taeglich von pre-backup-dumps.sh erzeugt und dann in Borg gesichert).
# Der Borg-Extract des Dumps wuerde dieselbe Datei liefern, braucht aber eine
# eigene Remote-Roundtrip-Zeit; wir nutzen die Host-Kopie direkt.
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/nextcloud.dump"
borg_extract "/restore/nextcloud-extract" \
"local/appdata/nextcloud/html"
if [ ! -d "$EXTRACT_DIR/local/appdata/nextcloud/html" ]; then
echo "Nextcloud html path missing in Borg archive" >&2
exit 1
fi
if [ ! -f "$DUMP_HOST_PATH" ]; then
echo "nextcloud.dump missing on host at $DUMP_HOST_PATH" >&2
exit 1
fi
# App-Code + Config ins Restore-Lab verschieben
cp -a "$EXTRACT_DIR/local/appdata/nextcloud/html/." "$RESTORE_ROOT/html/"
cp "$DUMP_HOST_PATH" "$RESTORE_ROOT/dumps/latest/nextcloud.dump"
# Nextcloud braucht einen beschreibbaren data-Pfad, auch wenn er leer ist.
# Im Restore-Lab ist das /mnt/user/backups/restore-lab/nextcloud/data.
mkdir -p "$RESTORE_ROOT/data"
# Unraid (FUSE/shfs) ignoriert chown auf User-Shares. Stattdessen setzen
# wir die Dateien auf world-writable, damit der Nextcloud-Entrypoint
# (der als root startet und intern auf www-data wechselt) die Dateien
# lesen und beschreiben kann. Im isolierten Smoke-Kontext vertretbar.
chmod -R a+rwX "$RESTORE_ROOT/html" "$RESTORE_ROOT/data"
# Falls config.php einen anderen dbuser als das Test-Compose hat, patchen
# wir die DB-Zugangsdaten in der restaurierten config.php fuer den Test.
CONFIG_PHP="$RESTORE_ROOT/html/config/config.php"
if [ -f "$CONFIG_PHP" ]; then
# Backup der Originalkonfig fuer Diagnose
cp "$CONFIG_PHP" "$RESTORE_ROOT/html/config/config.php.original"
# DB-Credentials auf die Test-Werte umbiegen. Nextcloud config.php
# ist PHP; wir patchen die relevanten Zeilen per sed.
sed -i \
-e "s|'dbhost'.*|'dbhost' => 'restoretest-nextcloud-postgres',|" \
-e "s|'dbuser'.*|'dbuser' => 'nextcloud',|" \
-e "s|'dbpassword'.*|'dbpassword' => 'restoretest-nextcloud-db',|" \
-e "s|'dbname'.*|'dbname' => 'nextcloud',|" \
-e "s|'dbport'.*|'dbport' => '',|" \
"$CONFIG_PHP"
# Redis-Host patchen. Die config.php hat ein verschachteltes Array:
# 'redis' => array( 'host' => 'nextcloud-redis', ... )
# Wir ersetzen nur den Host-Wert innerhalb des redis-Blocks.
sed -i "s|'host' => 'nextcloud-redis'|'host' => 'restoretest-nextcloud-redis'|g" "$CONFIG_PHP"
# trusted_domains: 127.0.0.1 hinzufuegen, damit der Smoke-Endpunkt akzeptiert wird.
# Nextcloud prueft trusted_domains und blockt sonst mit "Access through untrusted domain" (503).
# Wir fuegen per PHP-Code-Injection am Ende der config eine zweite trusted_domain hinzu.
# Das ist robuster als den Array-Block per sed zu finden.
php -r "
\$f = '$CONFIG_PHP';
\$c = file_get_contents(\$f);
if (strpos(\$c, \"'127.0.0.1'\") === false) {
include \$f;
\$CONFIG['trusted_domains'][] = '127.0.0.1';
\$out = '<?php' . PHP_EOL . '\$CONFIG = ' . var_export(\$CONFIG, true) . ';' . PHP_EOL;
file_put_contents(\$f, \$out);
}
" 2>/dev/null || {
# Fallback: wenn php nicht auf dem Host ist, per sed versuchen
if ! grep -q "127.0.0.1" "$CONFIG_PHP"; then
sed -i "/'trusted_domains'/,/^ )/s|^ )| 99 => '127.0.0.1',\n )|" "$CONFIG_PHP" || true
fi
}
config_patched="ok"
else
config_patched="no config.php found"
fi
# Stufe 2: Test-Postgres + Test-Redis hochfahren
docker compose -f "$COMPOSE_FILE" up -d restoretest-nextcloud-postgres restoretest-nextcloud-redis >/dev/null
until docker exec restoretest-nextcloud-postgres pg_isready -U nextcloud -d nextcloud >/dev/null 2>&1; do
sleep 2
done
# Stufe 3: Dump einspielen (mit Retry wie bei Paperless/Immich)
restore_ok=0
for attempt in $(seq 1 12); do
if docker exec -i restoretest-nextcloud-postgres \
pg_restore -U nextcloud -d nextcloud --clean --if-exists --no-owner --no-privileges \
< "$RESTORE_ROOT/dumps/latest/nextcloud.dump" 2>/tmp/nextcloud-pg-restore.err; then
restore_ok=1
break
fi
if grep -qiE "starting up|shutting down|connection refused|database .* does not exist" /tmp/nextcloud-pg-restore.err; then
sleep 5
continue
fi
# pg_restore mit --clean erzeugt "does not exist"-Warnungen fuer nicht vorhandene
# Objekte beim ersten Import. Diese sind erwartbar und kein echter Fehler.
# Wir pruefen auf harte Fehler.
if grep -qiE "FATAL|PANIC" /tmp/nextcloud-pg-restore.err; then
cat /tmp/nextcloud-pg-restore.err >&2
exit 1
fi
restore_ok=1
break
done
if [ "$restore_ok" -ne 1 ]; then
cat /tmp/nextcloud-pg-restore.err >&2
exit 1
fi
# Stufe 4: Nextcloud starten
docker compose -f "$COMPOSE_FILE" up -d restoretest-nextcloud >/dev/null
# Nextcloud braucht beim ersten Start mit existierender config.php einige
# Sekunden fuer DB-Migrations-Checks. Wir geben bis zu 180s.
http_status=""
for _ in $(seq 1 90); do
http_status="$(curl -s -o /tmp/nextcloud-body.html -w '%{http_code}' \
-L http://127.0.0.1:18180/status.php || true)"
if [ "$http_status" = "200" ]; then
break
fi
sleep 2
done
if [ "$http_status" != "200" ]; then
echo "Nextcloud HTTP smoke failed: status=$http_status" >&2
docker logs --tail 120 restoretest-nextcloud >&2 || true
exit 1
fi
# Stufe 5: occ status pruefen (maintenance mode)
occ_output="$(docker exec -u www-data restoretest-nextcloud php occ status --output=json 2>/dev/null || echo '{}')"
maintenance="$(echo "$occ_output" | grep -o '"maintenance":[a-z]*' | head -1 | cut -d: -f2)"
if [ -z "$maintenance" ]; then
maintenance="unknown"
fi
# DB-Tabellen-Count als fachlicher Sanity-Check
table_count="$(docker exec restoretest-nextcloud-postgres \
psql -U nextcloud -d nextcloud -tAc \
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" \
2>/dev/null | tr -d '[:space:]' || echo "n/a")"
write_report "$REPORT_FILE" <<EOF
# Nextcloud Restore Test Report - $(date +%F)
- Service: \`nextcloud\`
- Source repo: \`$repo\`
- Archive: \`$archive\`
- Restore root: \`$RESTORE_ROOT\`
- Test containers:
- \`restoretest-nextcloud\`
- \`restoretest-nextcloud-postgres\`
- \`restoretest-nextcloud-redis\`
- Test endpoint: \`http://127.0.0.1:18180/status.php\`
- Result: \`SUCCESS\`
## Checks
- Borg extract of html: \`ok\`
- Host dump copy: \`ok\`
- config.php patched for test DB: \`$config_patched\`
- Dump import into isolated Postgres: \`ok\`
- HTTP status from /status.php: \`$http_status\`
- occ status maintenance: \`$maintenance\`
- Public table count in test DB: \`$table_count\`
## Scope
Dieser Smoke prueft: Borg-Restore von App-Code + Config + DB-Dump,
Dump-Import in isoliertes Test-Postgres, Nextcloud-Boot mit restaurierter
config.php (DB-Credentials auf Test-Werte gepatcht), HTTP-Status und
occ-Maintenance-Status.
Bewusst NICHT Teil des Smokes:
- Voller Restore der Nutzdaten unter /mnt/user/documents/nextcloud-data
(zu gross fuer regelmaessigen Smoke; Pfad-Existenz im Archiv kann
separat geprueft werden)
- Produktive Secrets (admin_user/password, postgres_password)
- Traefik-Route und produktive Domain cloud.kaleschke.info
- occ maintenance:mode Toggle (der Test-Restore braucht keinen
vorhergehenden maintenance:mode --on, weil er gegen einen Dump laeuft)
## Notes
- Test ran without Traefik and without the productive domain.
- Productive Nextcloud secrets were NOT mounted; test uses throwaway credentials.
- Productive user data under /mnt/user/documents/nextcloud-data was NOT mounted.
- config.php.original preserved for diagnosis.
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
RESTORE_SUCCESS=1
echo "Nextcloud restore test ok -> $REPORT_FILE"
+31 -1
View File
@@ -41,8 +41,14 @@ require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
RESTORE_SUCCESS=0
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "paperless" "$RESTORE_ROOT"
rm -rf "$EXTRACT_DIR"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
@@ -70,7 +76,30 @@ mv "$EXTRACT_DIR/local/borg-dumps/latest/postgresql17-paperless.dump" "$RESTORE_
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless-postgres restoretest-paperless-redis >/dev/null
until docker exec restoretest-paperless-postgres pg_isready -U paperless -d paperless >/dev/null 2>&1; do sleep 2; done
cat "$RESTORE_ROOT/dumps/latest/postgresql17-paperless.dump" | docker exec -i restoretest-paperless-postgres pg_restore -U paperless -d paperless --clean --if-exists --no-owner --no-privileges
# Postgres-Entrypoint kann kurz nach "ready" noch vom Init- auf den finalen
# Server wechseln. pg_restore toleriert transiente Start-/Shutdown-Fehler und
# retried; harte Fehler (z. B. Dump-Korruption) brechen wie bisher ab.
restore_ok=0
for attempt in $(seq 1 12); do
if docker exec -i restoretest-paperless-postgres \
pg_restore -U paperless -d paperless --clean --if-exists --no-owner --no-privileges \
< "$RESTORE_ROOT/dumps/latest/postgresql17-paperless.dump" 2>/tmp/paperless-pg-restore.err; then
restore_ok=1
break
fi
if grep -qiE "starting up|shutting down|connection refused|database .* does not exist" /tmp/paperless-pg-restore.err; then
sleep 5
continue
fi
cat /tmp/paperless-pg-restore.err >&2
exit 1
done
if [ "$restore_ok" -ne 1 ]; then
cat /tmp/paperless-pg-restore.err >&2
exit 1
fi
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless >/dev/null
sleep 12
@@ -110,4 +139,5 @@ write_report "$REPORT_FILE" <<EOF
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
RESTORE_SUCCESS=1
echo "Paperless restore test ok -> $REPORT_FILE"
+3 -3
View File
@@ -66,7 +66,7 @@ mv /mnt/user/backups/restore-lab/paperless/local/paperless/consume /mnt/user/bac
3. Test-Postgres und Test-Redis starten
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless-postgres restoretest-paperless-redis
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless-postgres restoretest-paperless-redis
```
4. Dump in Test-Postgres importieren
@@ -78,7 +78,7 @@ docker exec -i restoretest-paperless-postgres pg_restore -U paperless -d paperle
5. Testinstanz starten
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless
```
6. Smoke-Test
@@ -98,7 +98,7 @@ Minimal erfolgreich:
7. Testcontainer wieder stoppen
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml down
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/paperless-compose.test.yml down
```
8. Testdaten nach erfolgreichem Lauf bereinigen
+25 -1
View File
@@ -34,8 +34,32 @@ case "$MODE" in
fi
exec "$SCRIPT_DIR/immich-restore-test.sh"
;;
authelia)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/authelia-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/authelia-restore-test.sh"
;;
nextcloud)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/nextcloud-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/nextcloud-restore-test.sh"
;;
komodo-bootstrap)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/komodo-bootstrap-test.sh" --what-if
fi
exec "$SCRIPT_DIR/komodo-bootstrap-test.sh"
;;
komodo-mongo-restore)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/komodo-mongo-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/komodo-mongo-restore-test.sh"
;;
*)
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless|immich} [--what-if]" >&2
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless|immich|authelia|nextcloud|komodo-bootstrap|komodo-mongo-restore} [--what-if]" >&2
exit 1
;;
esac
@@ -7,24 +7,29 @@ SUCCESS_TOPIC="${2:-${RESTORE_SUCCESS_TOPIC:-homelab-info}}"
FAILURE_TOPIC="${RESTORE_FAILURE_TOPIC:-homelab-alerts}"
if [ -z "$MODE" ]; then
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless|immich> [success_topic]" >&2
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless|immich|authelia|nextcloud|komodo-bootstrap> [success_topic]" >&2
exit 1
fi
REPORT_ROOT="/mnt/user/backups/restore-reports"
REPORT_FILE="$REPORT_ROOT/${MODE}-$(date +%F).md"
WRAPPER_LOG="$REPORT_ROOT/_wrapper-${MODE}-$(date +%F).log"
mkdir -p "$REPORT_ROOT"
echo "Running restore job: $MODE"
echo "Report target: $REPORT_FILE"
echo "Inner report (written by restore script): $REPORT_FILE"
echo "Wrapper log (stdout/stderr of dispatcher): $WRAPPER_LOG"
if "$SCRIPT_DIR/run-restore-checks.sh" "$MODE" > "$REPORT_FILE"; then
# Der Restore-Job schreibt seinen Markdown-Report selbst nach $REPORT_FILE.
# Wir leiten stdout/stderr in eine separate Wrapper-Log-Datei, damit hier
# kein zweiter Schreiber denselben Pfad ueberschreibt.
if "$SCRIPT_DIR/run-restore-checks.sh" "$MODE" >"$WRAPPER_LOG" 2>&1; then
echo "Restore job succeeded, sending ntfy..."
"$SCRIPT_DIR/send-ntfy.sh" "$SUCCESS_TOPIC" "Restore job ok: $MODE" "Restore job succeeded. Report: $REPORT_FILE" default || true
echo "Done"
else
echo "Restore job failed, sending ntfy..."
"$SCRIPT_DIR/send-ntfy.sh" "$FAILURE_TOPIC" "Restore job failed: $MODE" "Restore job failed. Report: $REPORT_FILE" high || true
"$SCRIPT_DIR/send-ntfy.sh" "$FAILURE_TOPIC" "Restore job failed: $MODE" "Restore job failed. Wrapper log: $WRAPPER_LOG (Report if written: $REPORT_FILE)" high || true
exit 1
fi
+20 -12
View File
@@ -44,7 +44,8 @@ Quartals-Belegung:
| Q4 | `vaultwarden` oder `gitea` | Externe Abhaengigkeiten, Hetzner, GitHub-Mirror |
Bestaetigte Mini-Restores: Vaultwarden, Gitea und Paperless am 2026-05-07;
Immich am 2026-05-27; Paperless erneut am 2026-05-31.
Immich am 2026-05-27; Paperless erneut am 2026-05-31; Authelia am
2026-06-03 (Config-Smoke ohne produktiven Dump-Restore).
## Konkreter Kalender
@@ -56,6 +57,8 @@ Immich am 2026-05-27; Paperless erneut am 2026-05-31.
- `gitea`
- Jeden 2. Samstag in ungeraden Monaten, 08:00:
- `paperless`
- Jeden 2. Samstag in geraden Monaten, 07:30:
- `authelia`
- Jeden 1. des Monats, 09:00:
- `monthly-random-restore.sh`
- Quartalsweise am 1. Werktag des Quartals:
@@ -65,24 +68,29 @@ Immich am 2026-05-27; Paperless erneut am 2026-05-31.
## Unraid User Scripts Cron
| Script | Cron | Bedeutung |
|---|---|---|
| `restore-freshness-weekly` | `30 6 * * 1` | jeden Montag 06:30 |
| `restore-vaultwarden-monthly` | `0 7 1-7 * 6` | erster Samstag im Monat 07:00 |
| `restore-gitea-monthly` | `15 7 15-21 * 6` | dritter Samstag im Monat 07:15 |
| `restore-paperless-bimonthly` | `0 8 8-14 1,3,5,7,9,11 *` | zweiter Samstag in ungeraden Monaten 08:00 |
| `restore-immich-quarterly` | `30 8 8-14 2,5,8,11 0` | zweiter Sonntag in Feb/Mai/Aug/Nov 08:30 |
| `monthly-random-restore` | `0 9 1 * *` | erster Kalendertag im Monat 09:00 |
Vixie-Cron (Unraid) verknuepft `day-of-month` und `day-of-week` mit **OR**, sobald beide gesetzt sind. "n-ter Samstag im Monat" laesst sich deshalb nicht direkt im Cron-Ausdruck ausdruecken. Wir triggern stattdessen an **jedem** Samstag/Sonntag und filtern den Monatstag im User-Script per Shell-Guard.
| Script | Cron | Shell-Guard (zusaetzlich) | Bedeutung |
|---|---|---|---|
| `restore-freshness-weekly` | `30 6 * * 1` | - | jeden Montag 06:30 |
| `restore-vaultwarden-monthly` | `0 7 * * 6` | `[ "$(date +%-d)" -le 7 ]` | erster Samstag im Monat 07:00 |
| `restore-gitea-monthly` | `15 7 * * 6` | `d=$(date +%-d); [ "$d" -ge 15 ] && [ "$d" -le 21 ]` | dritter Samstag im Monat 07:15 |
| `restore-paperless-bimonthly` | `0 8 * * 6` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 1\|3\|5\|7\|9\|11) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Samstag in ungeraden Monaten 08:00 |
| `restore-authelia-bimonthly` | `30 7 * * 6` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 2\|4\|6\|8\|10\|12) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Samstag in geraden Monaten 07:30 |
| `restore-immich-quarterly` | `30 8 * * 0` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 2\|5\|8\|11) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Sonntag in Feb/Mai/Aug/Nov 08:30 |
| `monthly-random-restore` | `0 9 1 * *` | - | erster Kalendertag im Monat 09:00 |
**Warum so**: ein frueheres Schema wie `0 7 1-7 * 6` haette in Vixie-Cron die OR-Semantik ausgeloest und an jedem Tag 1-7 zusaetzlich zu jedem Samstag gefeuert (~11 Laeufe statt 1 pro Monat). Die obige Trennung Cron-Trigger + Shell-Guard ist die einzige robuste Loesung in Standard-Cron.
## Betriebsmodus
- V1:
- Bash-Jobs laufen hostseitig manuell oder per User Script
- `ntfy` ist optional und folgt nach stabiler Basis
- Hermes wertet spaeter nur Reports aus
- `ntfy`-Wrapper ist vorhanden; Erfolg geht nach `homelab-info`, Fehler nach `homelab-alerts`
- Hermes wertet spaeter optional Reports aus
- V2:
- fester Host-Schedule
- `ntfy` bei Erfolg/Fehler
- `ntfy` bei Erfolg/Fehler ueber `run-restore-job-with-ntfy.sh`
- Hermes erzeugt Zusammenfassungen und Overviews
## Automatisierung
+105 -45
View File
@@ -10,18 +10,22 @@ Host-Repo-Pfad:
/mnt/user/services/homelab-infra
```
**Wichtig - Cron-Semantik**: Vixie-Cron verknuepft `day-of-month` und `day-of-week` mit **OR**, sobald beide gesetzt sind. Wir triggern daher an jedem Samstag/Sonntag und filtern den Monatstag per Shell-Guard im User-Script. Siehe `ops/restore-tests/schedule.md`.
**Wichtig - keine doppelten Schreiber**: die Restore-Skripte schreiben ihren Markdown-Report **selbst** nach `/mnt/user/backups/restore-reports/<service>-YYYY-MM-DD.md`. User-Scripts duerfen den Job-Output **nicht** in dieselbe Datei umleiten, sonst gewinnt der letzte Writer. Wrapper-Output landet stattdessen in `/mnt/user/backups/restore-reports/_wrapper-<mode>-YYYY-MM-DD.log`.
## Script 1 - `restore-freshness-weekly`
Zeit:
Cron:
- Montag, 06:30
- `30 6 * * 1` (Montag 06:30)
Inhalt:
```bash
#!/bin/bash
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness \
> /mnt/user/backups/restore-reports/freshness-$(date +%F).md
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
freshness homelab-info
```
Erwartung:
@@ -32,77 +36,133 @@ Erwartung:
## Script 2 - `restore-vaultwarden-monthly`
Zeit:
Cron:
- 1. Samstag im Monat, 07:00
- `0 7 * * 6` (jeden Samstag 07:00)
V1-Inhalt:
Guard: nur am ersten Samstag im Monat ausfuehren.
```bash
#!/bin/bash
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh vaultwarden \
> /mnt/user/backups/restore-reports/vaultwarden-$(date +%F).md
# Guard: nur 1.-7. Tag im Monat, damit "1. Samstag" eindeutig getroffen wird.
day=$(date +%-d)
if [ "$day" -lt 1 ] || [ "$day" -gt 7 ]; then
exit 0
fi
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
vaultwarden homelab-info
```
## Script 3 - `restore-gitea-monthly`
Zeit:
Cron:
- 3. Samstag im Monat, 07:00
- `15 7 * * 6` (jeden Samstag 07:15)
V1-Inhalt:
Guard: nur am dritten Samstag im Monat ausfuehren.
```bash
#!/bin/bash
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh gitea \
> /mnt/user/backups/restore-reports/gitea-$(date +%F).md
day=$(date +%-d)
if [ "$day" -lt 15 ] || [ "$day" -gt 21 ]; then
exit 0
fi
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
gitea homelab-info
```
## Script 4 - `restore-paperless-bimonthly`
Zeit:
Cron:
- jeder 2. Monat, 2. Samstag, 08:00
- `0 8 * * 6` (jeden Samstag 08:00)
V1-Inhalt:
Guard: nur am zweiten Samstag in ungeraden Monaten ausfuehren.
```bash
#!/bin/bash
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh paperless \
> /mnt/user/backups/restore-reports/paperless-$(date +%F).md
month=$(date +%-m)
day=$(date +%-d)
case "$month" in
1|3|5|7|9|11) ;;
*) exit 0 ;;
esac
if [ "$day" -lt 8 ] || [ "$day" -gt 14 ]; then
exit 0
fi
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
paperless homelab-info
```
## Script 5 - `restore-immich-quarterly`
Cron:
- `30 8 * * 0` (jeden Sonntag 08:30)
Guard: nur am zweiten Sonntag in Feb/Mai/Aug/Nov ausfuehren.
```bash
#!/bin/bash
month=$(date +%-m)
day=$(date +%-d)
case "$month" in
2|5|8|11) ;;
*) exit 0 ;;
esac
if [ "$day" -lt 8 ] || [ "$day" -gt 14 ]; then
exit 0
fi
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
immich homelab-info
```
## Script 6 - `restore-authelia-bimonthly`
Cron:
- `30 7 * * 6` (jeden Samstag 07:30)
Guard: nur am zweiten Samstag in geraden Monaten ausfuehren.
```bash
#!/bin/bash
month=$(date +%-m)
day=$(date +%-d)
case "$month" in
2|4|6|8|10|12) ;;
*) exit 0 ;;
esac
if [ "$day" -lt 8 ] || [ "$day" -gt 14 ]; then
exit 0
fi
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
authelia homelab-info
```
## Script 7 - `monthly-random-restore`
Cron:
- `0 9 1 * *` (erster Kalendertag im Monat 09:00) - kein Guard noetig.
```bash
#!/bin/bash
exec /mnt/user/services/homelab-infra/ops/restore-tests/monthly-random-restore.sh
```
## Stand
- die Bash-Jobs wurden am 2026-05-07 hostseitig erfolgreich verifiziert
- `freshness`, `vaultwarden`, `gitea` und `paperless` laufen damit prinzipiell automatisch
- `ntfy` kann jetzt optional per Wrapper-Skript ergaenzt werden
- die ersten Bash-Jobs wurden am 2026-05-07 hostseitig erfolgreich verifiziert
- `freshness`, `vaultwarden`, `gitea`, `paperless`, `immich` und `authelia` sind als Host-Jobs verfuegbar
- ntfy-Wrapper schreibt Erfolg/Fehler-Meldungen an die definierten Topics
## V2 Zielbild
## Fehler-Topic
Als naechster Ausbau kommen dazu:
Fehler gehen unabhaengig vom Erfolgstopic nach `homelab-alerts` (siehe `RESTORE_FAILURE_TOPIC` im Wrapper), damit Restore-Probleme auf demselben Handy-Topic landen wie Prometheus-, Docker-, Borg- und Posture-Alarme.
1. Restore aus Borg
2. Testcontainer starten
3. Smoke-Test
4. Report schreiben
5. optional `ntfy`
6. Bereinigung
## Optionales `ntfy` Wrapper-Muster
Wenn `ntfy` genutzt wird, soll der Host-Job nur Erfolg/Fehler referenzieren, nicht den ganzen Report in die Nachricht kippen.
Beispiel:
```bash
#!/bin/bash
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-info
```
Fehler gehen unabhaengig vom Erfolgstopic nach `homelab-alerts`, damit Restore-Probleme auf dem gleichen Handy-Topic landen wie Prometheus-, Docker-, Borg- und Posture-Alarme.
Verwendete Hilfsskripte:
## Verwendete Hilfsskripte
- `ops/restore-tests/send-ntfy.sh`
- `ops/restore-tests/run-restore-job-with-ntfy.sh`
- `ops/restore-tests/run-restore-checks.sh`
@@ -10,7 +10,12 @@ services:
WEBSOCKET_ENABLED: "true"
SIGNUPS_ALLOWED: "false"
INVITATIONS_ALLOWED: "false"
ADMIN_TOKEN_FILE: /run/secrets/admin_token
# Wegwerf-Admin-Token nur fuer den isolierten Smoke-Test.
# Bewusst KEIN Mount des produktiven vaultwarden_admin_token.txt,
# damit das echte Admin-Token nie in einem Test-Container-Lebenszyklus
# auftaucht. Smoke-Test prueft nur Login-Seite, das Token wird nicht
# zur Authentifizierung gebraucht.
ADMIN_TOKEN: restoretest-vaultwarden-admin-token-placeholder
ROCKET_PORT: 80
ROCKET_ADDRESS: 0.0.0.0
@@ -19,7 +24,6 @@ services:
volumes:
- /mnt/user/backups/restore-lab/vaultwarden/data:/data
- /mnt/user/appdata/secrets/vaultwarden_admin_token.txt:/run/secrets/admin_token:ro
security_opt:
- no-new-privileges:true
+3 -2
View File
@@ -8,7 +8,8 @@ Nachweisen, dass ein Vaultwarden-Backup in einer isolierten Testumgebung wieder
- Backup-Quelle: Borg / Share-Backup
- fachlich relevanter Datenpfad: `/mnt/user/appdata/vaultwarden`
- Secret: `/mnt/user/appdata/secrets/vaultwarden_admin_token.txt`
- Produktives Admin-Token wird fuer den Restore-Smoke bewusst nicht gemountet;
die Testinstanz nutzt einen Wegwerf-Wert aus `vaultwarden-compose.test.yml`.
## Test-Ziel
@@ -44,7 +45,7 @@ Minimal erfolgreich:
Optional spaeter:
- Admin-Endpunkt pruefen
- Admin-Endpunkt nur mit separatem Wegwerf-Token pruefen
- Websocket-Endpunkt pruefen
- Anzahl/Vorhandensein zentraler Daten artefaktisch verifizieren
@@ -37,8 +37,14 @@ require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
RESTORE_SUCCESS=0
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "vaultwarden" "$RESTORE_ROOT"
rm -rf "$EXTRACT_DIR"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$DATA_DIR"
fi
@@ -82,4 +88,5 @@ write_report "$REPORT_FILE" <<EOF
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
RESTORE_SUCCESS=1
echo "Vaultwarden restore test ok -> $REPORT_FILE"
+3 -3
View File
@@ -3,9 +3,9 @@
## Vorbedingungen
- Borg-Quelle ist verfuegbar
- Secret-Datei vorhanden: `/mnt/user/appdata/secrets/vaultwarden_admin_token.txt`
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
- **Hinweis**: das produktive `vaultwarden_admin_token.txt` wird im Testcontainer **nicht** mehr gemountet. Die Testinstanz nutzt einen Wegwerf-Token; der Smoke-Test prueft nur die Login-Seite, kein Admin-Endpunkt.
## Bestaetigter Host-Stand
@@ -76,7 +76,7 @@ Zielpfad nach dem Restore:
3. Testcontainer starten
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/vaultwarden-compose.test.yml up -d
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/vaultwarden-compose.test.yml up -d
```
4. Smoke-Test
@@ -95,7 +95,7 @@ Minimal erfolgreich:
5. Testcontainer wieder stoppen
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/vaultwarden-compose.test.yml down
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/vaultwarden-compose.test.yml down
```
6. Report schreiben
+1 -1
View File
@@ -1,6 +1,6 @@
services:
scrutiny:
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:41c5faefb96766d27d58a829fa19b3f4f27da4160926de3255cf142a85a90c12
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:d8be7c11950cfdd2ec8d85f8d76cc67fedfa2825eead6cc60d0ed753492fb5f5
container_name: scrutiny
restart: unless-stopped
privileged: true
+4
View File
@@ -25,6 +25,10 @@ services:
- /mnt/user/appdata/vaultwarden:/data
- /mnt/user/appdata/secrets/vaultwarden_admin_token.txt:/run/secrets/admin_token:ro
- /mnt/user/appdata/secrets/homelab_smtp_password.txt:/run/secrets/smtp_password:ro
dns:
- 192.168.178.58
- 1.1.1.1
- 8.8.8.8
networks:
- frontend_net