Compare commits
24 Commits
b9bb5da48c
...
157480b83e
| Author | SHA1 | Date | |
|---|---|---|---|
| 157480b83e | |||
| 14de2f4801 | |||
| 90d1595285 | |||
| c1985e177b | |||
| a244f2d677 | |||
| ef032f2dde | |||
| 6fec64d0a1 | |||
| 5d1ae68705 | |||
| 2913e1005f | |||
| 6f0e6f0d5a | |||
| f473fbaa8b | |||
| c922d1f241 | |||
| ba3ef8fcfc | |||
| 52fc007123 | |||
| 8d71dfb9ad | |||
| 440000c085 | |||
| cacf77bfb0 | |||
| cd4dd178ed | |||
| 541c7be853 | |||
| b1ae9f3c26 | |||
| e2624796f0 | |||
| 9f63e6e3bc | |||
| 8eb367f0b5 | |||
| 745761f518 |
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
n8n:
|
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
|
container_name: n8n
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -1,6 +1,6 @@
|
|||||||
# AI Context
|
# AI Context
|
||||||
|
|
||||||
Stand: 2026-06-01
|
Stand: 2026-06-02
|
||||||
|
|
||||||
Kurzer Kontext fuer KI-Agenten. Nicht als Ersatz fuer die echten Runbooks lesen.
|
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:
|
Kurzfassung:
|
||||||
|
|
||||||
- Alt-Volumes fruehestens ab 2026-06-02 freigeben
|
|
||||||
- Auth-/OIDC-/CrowdSec-/Hermes-Themen bewusst geparkt
|
- Auth-/OIDC-/CrowdSec-/Hermes-Themen bewusst geparkt
|
||||||
|
|
||||||
Letzte Bestaetigung:
|
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.
|
- 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.
|
- 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.
|
- 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`.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -7,9 +7,23 @@ Audit-Snapshots wurden aus der Arbeitskopie entfernt; Detailhistorie liegt in Gi
|
|||||||
|
|
||||||
| Prioritaet | Punkt | Naechster Schritt |
|
| 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` |
|
| 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
|
## Bewusst geparkt
|
||||||
|
|
||||||
| Punkt | Entscheidung |
|
| Punkt | Entscheidung |
|
||||||
@@ -25,13 +39,13 @@ Audit-Snapshots wurden aus der Arbeitskopie entfernt; Detailhistorie liegt in Gi
|
|||||||
|
|
||||||
## Zuletzt geschlossen
|
## 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.
|
- 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-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.
|
- 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-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.
|
- 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.
|
- 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.
|
- 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`.
|
- 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.
|
- Immich-, Paperless-, Gitea- und Vaultwarden-Restore-Pfade sind belegt.
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ Vor dem Start muessen vorhanden sein:
|
|||||||
- `/mnt/user/appdata/secrets/authelia_smtp_password.txt`
|
- `/mnt/user/appdata/secrets/authelia_smtp_password.txt`
|
||||||
- SMTP-Zugang fuer `michideheld@gmx.de`
|
- 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`
|
### `nextcloud`
|
||||||
|
|
||||||
@@ -440,11 +440,11 @@ Aktive Datenpfade:
|
|||||||
- Mealie PostgreSQL: `/mnt/user/appdata/mealie/postgres18`
|
- Mealie PostgreSQL: `/mnt/user/appdata/mealie/postgres18`
|
||||||
- Nextcloud PostgreSQL: `/mnt/user/appdata/nextcloud/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`
|
- Shared PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17`
|
||||||
- Mealie PostgreSQL 17: `/mnt/user/appdata/mealie/postgres`
|
- Mealie PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`
|
||||||
- Nextcloud PostgreSQL 17: `/mnt/user/appdata/nextcloud/postgres`
|
- Nextcloud PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`
|
||||||
|
|
||||||
Restore-Reihenfolge fuer den Shared-Cluster:
|
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.
|
4. Datenbanken anlegen und Custom-Format-Dumps mit `pg_restore` einspielen.
|
||||||
5. Restore-Logs auf echte `ERROR`, `FATAL` und `PANIC` pruefen.
|
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
|
### Hermes Agent
|
||||||
|
|
||||||
|
|||||||
+90
-46
@@ -1,6 +1,6 @@
|
|||||||
# Restore Handbook - KalliLab CORE
|
# 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.
|
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
|
### Vaultwarden
|
||||||
|
|
||||||
- Report: `/mnt/user/backups/restore-reports/vaultwarden-2026-05-07.md`
|
- Erstlauf: 2026-05-07
|
||||||
- Nachweis:
|
- Nachweis: Borg-Restore, Testcontainer, Login-Seite erreichbar
|
||||||
- Borg-Restore erfolgreich
|
|
||||||
- Testcontainer startete
|
|
||||||
- Login-Seite war erreichbar
|
|
||||||
|
|
||||||
### Gitea
|
### Gitea
|
||||||
|
|
||||||
- Report: `/mnt/user/backups/restore-reports/gitea-2026-05-07.md`
|
- Erstlauf: 2026-05-07
|
||||||
- Nachweis:
|
- Nachweis: Borg-Restore, Web-UI, SSH-TCP-Port
|
||||||
- Borg-Restore erfolgreich
|
|
||||||
- Web-UI antwortete
|
|
||||||
- SSH-Port reagierte
|
|
||||||
|
|
||||||
### Paperless
|
### Paperless
|
||||||
|
|
||||||
- Report: `/mnt/user/backups/restore-reports/paperless-2026-05-07.md`
|
- Erstlauf: 2026-05-07, Folgelauf: 2026-05-31
|
||||||
- Nachweis:
|
- Nachweis: Borg-Datei-Restore, Dump-Import in Test-Postgres, Login-Seite, Doc-Count
|
||||||
- Borg-Datei-Restore erfolgreich
|
|
||||||
- Paperless-Dump aus Borg importiert
|
### Immich
|
||||||
- Login-Seite war erreichbar
|
|
||||||
- Test-DB enthielt `25` Dokumente
|
- 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/vaultwarden`
|
||||||
- `/mnt/user/backups/restore-lab/gitea`
|
- `/mnt/user/backups/restore-lab/gitea`
|
||||||
- `/mnt/user/backups/restore-lab/paperless`
|
- `/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
|
### Reports
|
||||||
|
|
||||||
@@ -89,31 +101,33 @@ Alle validierten Restore-Tests folgen demselben Muster:
|
|||||||
|
|
||||||
## 5. Restore-Frequenz
|
## 5. Restore-Frequenz
|
||||||
|
|
||||||
- jeden Montag, 06:30:
|
- jeden Montag, 06:30: Frische-Check fuer Dumps und Reports
|
||||||
- Frische-Check fuer Dumps und Reports
|
- 1. Samstag im Monat, 07:00: Vaultwarden
|
||||||
- 1. Samstag im Monat, 07:00:
|
- 3. Samstag im Monat, 07:15: Gitea
|
||||||
- Vaultwarden
|
- 2. Samstag in ungeraden Monaten, 08:00: Paperless
|
||||||
- 3. Samstag im Monat, 07:00:
|
- 2. Sonntag in Feb/Mai/Aug/Nov, 08:30: Immich
|
||||||
- Gitea
|
- 2. Samstag in geraden Monaten, 07:30: Authelia
|
||||||
- jeder 2. Monat, 2. Samstag, 08:00:
|
- 1. Kalendertag im Monat, 09:00: Zufaelliger Restore aus Pool
|
||||||
- Paperless
|
|
||||||
|
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
|
- validierte Bash-Host-Jobs fuer Vaultwarden, Gitea, Paperless, Immich, Authelia, Komodo-Bootstrap
|
||||||
- Host-Job-Definitionen liegen im Repo
|
- Host-Job-Definitionen und Cron-Vorlagen liegen im Repo (`ops/restore-tests/unraid-user-scripts.md`)
|
||||||
- Scheduler kann bereits echte Frische- und Restore-Checks fahren
|
- `ntfy`-Wrapper sendet Erfolg an `homelab-info`, Fehler an `homelab-alerts`
|
||||||
- `ntfy` und Hermes-Auswertung folgen danach
|
- 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-Zusammenfassung ueber vorhandene Reports
|
||||||
- Hermes liest Reports und baut Uebersichten
|
- Sammelreports und Report-Rotation
|
||||||
- zusaetzliche Rotation, Sammelreports und weitere Dienste
|
- weitere Dienste (Nextcloud, Mailarchiver, Mealie)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -126,15 +140,18 @@ Die Vorlagen stehen in:
|
|||||||
Host-Repo-Pfad:
|
Host-Repo-Pfad:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/mnt/user/services/homelab
|
/mnt/user/services/homelab-infra
|
||||||
```
|
```
|
||||||
|
|
||||||
V1-Jobs:
|
Jobs:
|
||||||
|
|
||||||
1. `restore-freshness-weekly`
|
1. `restore-freshness-weekly`
|
||||||
2. `restore-vaultwarden-monthly`
|
2. `restore-vaultwarden-monthly`
|
||||||
3. `restore-gitea-monthly`
|
3. `restore-gitea-monthly`
|
||||||
4. `restore-paperless-bimonthly`
|
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:
|
Auf dem Unraid-Host:
|
||||||
|
|
||||||
```bash
|
```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
|
### Vaultwarden Restore-Check
|
||||||
|
|
||||||
```bash
|
```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
|
### Gitea Restore-Check
|
||||||
|
|
||||||
```bash
|
```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
|
### Paperless Restore-Check
|
||||||
|
|
||||||
```bash
|
```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`
|
### Optional mit `ntfy`
|
||||||
|
|
||||||
```bash
|
```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
|
## 11. Naechste Ausbaustufen
|
||||||
|
|
||||||
1. Vollautomatik fuer Vaultwarden, Gitea und Paperless
|
1. Nextcloud-Restore-Test (mit `occ maintenance:mode`-Choreographie)
|
||||||
2. `ntfy`-Meldungen fuer Erfolg/Fehler
|
2. Mailarchiver-Restore-Test
|
||||||
3. Hermes-Zusammenfassung ueber vorhandene Reports
|
3. Mealie-Restore-Test
|
||||||
4. naechster Referenz-Restore fuer `mail-archiver` oder `mealie`
|
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
@@ -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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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` |
|
| 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 |
|
| 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`; 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 |
|
| 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 |
|
| 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` |
|
| 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 |
|
| 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` |
|
| 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 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`.
|
- 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.
|
- 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.
|
- 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`
|
| Dienst | Tier | Letzter Restore-Test | Typ | Naechster Lauf |
|
||||||
2. `paperless-ngx`
|
|---|---|---|---|---|
|
||||||
3. `gitea`
|
| Vaultwarden | 1 | 2026-05-07 | File + Container + HTTP | monatlich (1. Sa) |
|
||||||
4. `vaultwarden`
|
| 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)
|
||||||
|
|||||||
@@ -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 |
|
| 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. |
|
| `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 |
|
| `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-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. |
|
| `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_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_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 |
|
| `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` | 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 |
|
| `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` | 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 |
|
| `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. |
|
| `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` |
|
| `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,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
adguard:
|
adguard:
|
||||||
image: adguard/adguardhome:v0.107.76@sha256:7157eb1dc3b26c7af1d6898759a7b3f7d0fa09891fbd2d3caa6abc1057a9179b
|
image: adguard/adguardhome:v0.107.77@sha256:e6f2b8bcda06064ab055b44933a4f0e983c35558b9cdb8d2e7ab1efcee36d890
|
||||||
container_name: adguard
|
container_name: adguard
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ services:
|
|||||||
- loki
|
- loki
|
||||||
|
|
||||||
grafana:
|
grafana:
|
||||||
image: grafana/grafana:13.0.1@sha256:0f86bada30d65ef9d0183b90c1e2682ac92d53d95da8bed322b984ea78a4a73a
|
image: grafana/grafana:13.0.2@sha256:5dad0df181cb644a14e13617b913b261a54f7d4fd4510721dba420929f35bea2
|
||||||
container_name: monitoring-grafana
|
container_name: monitoring-grafana
|
||||||
user: "0"
|
user: "0"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -318,7 +318,7 @@ services:
|
|||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
influxdb3-core:
|
influxdb3-core:
|
||||||
image: influxdb:3.9.2-core@sha256:31ad94df2248134989b2cf73d965e51dd5f35dfae22d7ed8f4776b12e6f69f4e
|
image: influxdb:3.9.3-core@sha256:c27c9b2ca2625b5b6966f0b09baa448102310e63a471fd60dff22646a2522e29
|
||||||
container_name: monitoring-influxdb3-core
|
container_name: monitoring-influxdb3-core
|
||||||
user: "0"
|
user: "0"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -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
|
## Explicitly Not Backed Up as Raw Live DB Files
|
||||||
|
|
||||||
- `/mnt/user/appdata/postgresql17`
|
|
||||||
- `/mnt/user/appdata/postgresql18`
|
- `/mnt/user/appdata/postgresql18`
|
||||||
- `/mnt/user/appdata/mealie/postgres`
|
|
||||||
- `/mnt/user/appdata/mealie/postgres18`
|
- `/mnt/user/appdata/mealie/postgres18`
|
||||||
- `/mnt/user/appdata/immich_postgres`
|
|
||||||
- `/mnt/user/appdata/immich_postgres_vectorchord`
|
- `/mnt/user/appdata/immich_postgres_vectorchord`
|
||||||
- `/mnt/user/appdata/nextcloud/postgres`
|
|
||||||
- `/mnt/user/appdata/nextcloud/postgres18`
|
- `/mnt/user/appdata/nextcloud/postgres18`
|
||||||
- `/mnt/user/appdata/komodo/mongo`
|
- `/mnt/user/appdata/komodo/mongo`
|
||||||
- `/mnt/user/appdata/redis`
|
- `/mnt/user/appdata/redis`
|
||||||
- `/mnt/user/appdata/scrutiny/influxdb`
|
- `/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
|
## Low-Priority / Rebuildable
|
||||||
|
|
||||||
These are not part of the first-class Borg scope:
|
These are not part of the first-class Borg scope:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
borg-ui:
|
borg-ui:
|
||||||
image: ainullcode/borg-ui@sha256:b44c0a92b650d80f215a986dadda5c2604c61eb28a7571e19c046eff41d761e7
|
image: ainullcode/borg-ui@sha256:acb0fbe83dc4a3843abc06f814c5f1061a0701b2cfc574da2e851d17a34ab745
|
||||||
container_name: borg-ui
|
container_name: borg-ui
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
code-server:
|
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
|
container_name: code-server
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
filebrowser:
|
filebrowser:
|
||||||
image: filebrowser/filebrowser:v2.63.5@sha256:aefb0c20de10ef8b617995ca5522479ad40d41e6386bd01946a345c6026ff31c
|
image: filebrowser/filebrowser:v2.63.7@sha256:40ba654524feb29afd28de458fdcf996b119cd1b53be2630d7658e01419f5891
|
||||||
container_name: filebrowser
|
container_name: filebrowser
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
"dump_file": null,
|
"dump_file": null,
|
||||||
"data_paths": ["/mnt/user/appdata/postgresql18"],
|
"data_paths": ["/mnt/user/appdata/postgresql18"],
|
||||||
"first_check": "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?",
|
"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": {
|
"komodo-core": {
|
||||||
"description": "GitOps UI / API / Stack-Manager",
|
"description": "GitOps UI / API / Stack-Manager",
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
"dump_file": "immich.dump",
|
"dump_file": "immich.dump",
|
||||||
"data_paths": ["/mnt/user/appdata/immich_postgres_vectorchord"],
|
"data_paths": ["/mnt/user/appdata/immich_postgres_vectorchord"],
|
||||||
"first_check": "immich_default Netz? Disk-Space? pg_isready?",
|
"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": {
|
"immich_redis": {
|
||||||
"description": "Immich Cache",
|
"description": "Immich Cache",
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ services:
|
|||||||
data_paths:
|
data_paths:
|
||||||
- /mnt/user/appdata/postgresql18
|
- /mnt/user/appdata/postgresql18
|
||||||
first_check: "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?"
|
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:
|
komodo-core:
|
||||||
description: GitOps UI / API / Stack-Manager
|
description: GitOps UI / API / Stack-Manager
|
||||||
@@ -263,7 +263,7 @@ services:
|
|||||||
data_paths:
|
data_paths:
|
||||||
- /mnt/user/appdata/immich_postgres_vectorchord
|
- /mnt/user/appdata/immich_postgres_vectorchord
|
||||||
first_check: "immich_default Netz? Disk-Space? pg_isready?"
|
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:
|
immich_redis:
|
||||||
description: Immich Cache
|
description: Immich Cache
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
MODE="dry-run"
|
MODE="dry-run"
|
||||||
CUTOFF_DATE="2026-06-02"
|
CUTOFF_DATE="2026-06-02"
|
||||||
|
ARCHIVE_ROOT="/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602"
|
||||||
|
|
||||||
if [[ "${1:-}" == "--execute" ]]; then
|
if [[ "${1:-}" == "--execute" ]]; then
|
||||||
MODE="execute"
|
MODE="execute"
|
||||||
@@ -23,10 +24,10 @@ if [[ "$MODE" == "execute" && "$today" < "$CUTOFF_DATE" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
declare -a CANDIDATES=(
|
declare -a CANDIDATES=(
|
||||||
"/mnt/user/appdata/postgresql17|/mnt/user/appdata/postgresql18|shared PostgreSQL 17 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 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 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 pgvecto.rs rollback"
|
"/mnt/user/appdata/immich_postgres|/mnt/user/appdata/immich_postgres_vectorchord|immich-postgres-pgvecto-rs|Immich pgvecto.rs rollback"
|
||||||
)
|
)
|
||||||
|
|
||||||
require_container_healthy() {
|
require_container_healthy() {
|
||||||
@@ -48,9 +49,10 @@ require_container_healthy() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "Alt-volume release check"
|
echo "Alt-volume archive check"
|
||||||
echo "Mode: $MODE"
|
echo "Mode: $MODE"
|
||||||
echo "Date: $today"
|
echo "Date: $today"
|
||||||
|
echo "Archive: $ARCHIVE_ROOT"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
require_container_healthy postgresql17
|
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
|
/mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
||||||
fi
|
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
|
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
|
if [[ ! -d "$active_path" ]]; then
|
||||||
echo "Missing active path for $label: $active_path" >&2
|
echo "Missing active path for $label: $active_path" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -d "$old_path" ]]; then
|
if printf '%s\n' "${active_mounts[@]}" | grep -Fxq "$old_path"; then
|
||||||
echo "Already absent: $old_path ($label)"
|
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
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if printf '%s\n' "${active_mounts[@]}" | grep -Fxq "$old_path"; then
|
if [[ ! -d "$old_path" ]]; then
|
||||||
echo "Refusing: old path is still mounted by a running container: $old_path" >&2
|
echo "Absent and not archived: $old_path ($label)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
size="$(du -sh "$old_path" 2>/dev/null | awk '{print $1}')"
|
size="$(du -sh "$old_path" 2>/dev/null | awk '{print $1}')"
|
||||||
echo "Candidate: $old_path ($label, $size)"
|
echo "Candidate: $old_path ($label, $size)"
|
||||||
echo "Active: $active_path"
|
echo "Active: $active_path"
|
||||||
|
echo "Archive: $archive_path"
|
||||||
|
|
||||||
if [[ "$MODE" == "execute" ]]; then
|
if [[ "$MODE" == "execute" ]]; then
|
||||||
rm -rf --one-file-system "$old_path"
|
mv "$old_path" "$archive_path"
|
||||||
echo "Removed: $old_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
|
else
|
||||||
echo "Dry-run: would remove $old_path"
|
echo "Dry-run: would move $old_path to $archive_path"
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Alt-volume release check completed."
|
echo "Alt-volume archive check completed."
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Ziel:
|
|||||||
## Geplante Struktur
|
## Geplante Struktur
|
||||||
|
|
||||||
- `schedule.md`: Intervalle und Verantwortlichkeiten
|
- `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.ps1`: erster Mini-Restore-Ablauf
|
||||||
- `vaultwarden-restore-test.sh`: hosttauglicher Vaultwarden-Restore-Job
|
- `vaultwarden-restore-test.sh`: hosttauglicher Vaultwarden-Restore-Job
|
||||||
- `vaultwarden-plan.md`: konkreter Vaultwarden-Testplan
|
- `vaultwarden-plan.md`: konkreter Vaultwarden-Testplan
|
||||||
@@ -37,6 +38,13 @@ Ziel:
|
|||||||
- `immich-plan.md`: konkreter Immich-Testplan
|
- `immich-plan.md`: konkreter Immich-Testplan
|
||||||
- `immich-runbook.md`: Operator-Runbook fuer den ersten Immich-Lauf
|
- `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
|
- `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
|
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
|
||||||
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
||||||
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
|
- `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 Gitea-Restore am 2026-05-07 erfolgreich verifiziert
|
||||||
- echter Paperless-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
|
- 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
|
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
|
||||||
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
||||||
- V1-Ablauf weiter ohne `ntfy`, mit Bereinigung nach Erfolg
|
- `ntfy`-Wrapper ist fuer Host-Jobs verfuegbar
|
||||||
- naechster grosser Kandidat ist ein erneuter Immich-Lauf nach VectorChord-Migration mit Zeitmessung; danach in die Rotation aufnehmen
|
- 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
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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 ))
|
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 \
|
for dump in \
|
||||||
postgresql17-paperless.dump \
|
postgresql17-paperless.dump \
|
||||||
postgresql17-mailarchiver.dump \
|
postgresql17-mailarchiver.dump \
|
||||||
@@ -48,11 +107,24 @@ for dump in \
|
|||||||
age="$(check_file_age_hours "$path")"
|
age="$(check_file_age_hours "$path")"
|
||||||
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
|
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
|
||||||
critical+=("DUMP_STALE $dump age=${age}h")
|
critical+=("DUMP_STALE $dump age=${age}h")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if is_pg_custom_dump "$dump"; then
|
||||||
|
check_pg_header "$dump" "$path" "$age"
|
||||||
else
|
else
|
||||||
info+=("DUMP_OK $dump age=${age}h")
|
info+=("DUMP_OK $dump age=${age}h")
|
||||||
fi
|
fi
|
||||||
done
|
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
|
for service in vaultwarden gitea paperless; do
|
||||||
if [ ! -d "$REPORT_ROOT" ]; then
|
if [ ! -d "$REPORT_ROOT" ]; then
|
||||||
warnings+=("REPORT_ROOT_MISSING $REPORT_ROOT")
|
warnings+=("REPORT_ROOT_MISSING $REPORT_ROOT")
|
||||||
|
|||||||
@@ -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() {
|
latest_archive_name() {
|
||||||
|
require_borg_container
|
||||||
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
||||||
import sqlite3
|
import sqlite3
|
||||||
conn = sqlite3.connect('/data/borg.db')
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
@@ -34,6 +55,7 @@ PY
|
|||||||
}
|
}
|
||||||
|
|
||||||
borg_repo_url() {
|
borg_repo_url() {
|
||||||
|
require_borg_container
|
||||||
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
||||||
import sqlite3
|
import sqlite3
|
||||||
conn = sqlite3.connect('/data/borg.db')
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
@@ -50,6 +72,7 @@ borg_extract() {
|
|||||||
local extract_dir="$1"
|
local extract_dir="$1"
|
||||||
shift
|
shift
|
||||||
local paths=("$@")
|
local paths=("$@")
|
||||||
|
require_borg_container
|
||||||
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
|
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
|
||||||
import os, sys, subprocess
|
import os, sys, subprocess
|
||||||
extract_dir = sys.argv[1]
|
extract_dir = sys.argv[1]
|
||||||
@@ -88,3 +111,22 @@ cleanup_compose() {
|
|||||||
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
|
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
|
||||||
fi
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,8 +37,14 @@ require_cmd curl
|
|||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
cleanup() {
|
cleanup() {
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
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
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$DATA_DIR"
|
rm -rf "$DATA_DIR"
|
||||||
fi
|
fi
|
||||||
@@ -61,9 +67,9 @@ sleep 8
|
|||||||
status="$(curl -s -o /tmp/gitea-body.html -w '%{http_code}' http://127.0.0.1:13000)"
|
status="$(curl -s -o /tmp/gitea-body.html -w '%{http_code}' http://127.0.0.1:13000)"
|
||||||
grep -qi "Gitea" /tmp/gitea-body.html
|
grep -qi "Gitea" /tmp/gitea-body.html
|
||||||
if timeout 5 bash -lc '</dev/tcp/127.0.0.1/12222' >/dev/null 2>&1; then
|
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
|
else
|
||||||
echo "Gitea SSH port not reachable" >&2
|
echo "Gitea SSH port not reachable (TCP connect failed)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -85,7 +91,7 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Borg extract into isolated restore-lab: \`ok\`
|
- Borg extract into isolated restore-lab: \`ok\`
|
||||||
- HTTP status: \`$status\`
|
- HTTP status: \`$status\`
|
||||||
- HTML content: \`Gitea\`
|
- HTML content: \`Gitea\`
|
||||||
- SSH port: \`$ssh_state\`
|
- SSH TCP port: \`$ssh_state\` (TCP connect only, not a full SSH handshake)
|
||||||
- Repository sample: \`$repo_sample\`
|
- Repository sample: \`$repo_sample\`
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
@@ -94,4 +100,5 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
echo "Gitea restore test ok -> $REPORT_FILE"
|
echo "Gitea restore test ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Wenn das Archiv den Pfad anders ablegt, zuerst mit `borg list "$BORG_REPO" "::AR
|
|||||||
3. Testcontainer starten
|
3. Testcontainer starten
|
||||||
|
|
||||||
```bash
|
```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
|
4. Smoke-Test
|
||||||
@@ -83,7 +83,7 @@ Minimal erfolgreich:
|
|||||||
5. Testcontainer wieder stoppen
|
5. Testcontainer wieder stoppen
|
||||||
|
|
||||||
```bash
|
```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
|
6. Report schreiben
|
||||||
|
|||||||
@@ -64,8 +64,14 @@ require_cmd curl
|
|||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
cleanup() {
|
cleanup() {
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
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
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$RESTORE_ROOT"
|
rm -rf "$RESTORE_ROOT"
|
||||||
fi
|
fi
|
||||||
@@ -244,4 +250,5 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Restore-Quelle Dump: \`local/borg-dumps/latest/immich.dump\` aus aktuellem Borg-Archiv.
|
- Restore-Quelle Dump: \`local/borg-dumps/latest/immich.dump\` aus aktuellem Borg-Archiv.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
echo "Immich restore test ok -> $REPORT_FILE"
|
echo "Immich restore test ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -53,8 +53,13 @@ fi
|
|||||||
require_cmd docker
|
require_cmd docker
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
cleanup() {
|
cleanup() {
|
||||||
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down -v >/dev/null 2>&1 || true
|
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
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$RESTORE_ROOT"
|
rm -rf "$RESTORE_ROOT"
|
||||||
fi
|
fi
|
||||||
@@ -132,4 +137,5 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
|
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
echo "Komodo bootstrap trockenlauf ok -> $REPORT_FILE"
|
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"
|
||||||
@@ -3,7 +3,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
TOPIC="${TOPIC:-homelab-info}"
|
TOPIC="${TOPIC:-homelab-info}"
|
||||||
TESTS="${TESTS:-vaultwarden gitea paperless}"
|
TESTS="${TESTS:-vaultwarden gitea paperless authelia}"
|
||||||
|
|
||||||
pick_random() {
|
pick_random() {
|
||||||
printf '%s\n' $TESTS | awk 'BEGIN { srand() } { items[++count] = $0 } END { print items[int(rand() * count) + 1] }'
|
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.
|
||||||
@@ -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"
|
||||||
@@ -41,8 +41,14 @@ require_cmd curl
|
|||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
cleanup() {
|
cleanup() {
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
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
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$RESTORE_ROOT"
|
rm -rf "$RESTORE_ROOT"
|
||||||
fi
|
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
|
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
|
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
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless >/dev/null
|
||||||
sleep 12
|
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)\`
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
echo "Paperless restore test ok -> $REPORT_FILE"
|
echo "Paperless restore test ok -> $REPORT_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
|
3. Test-Postgres und Test-Redis starten
|
||||||
|
|
||||||
```bash
|
```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
|
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
|
5. Testinstanz starten
|
||||||
|
|
||||||
```bash
|
```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
|
6. Smoke-Test
|
||||||
@@ -98,7 +98,7 @@ Minimal erfolgreich:
|
|||||||
7. Testcontainer wieder stoppen
|
7. Testcontainer wieder stoppen
|
||||||
|
|
||||||
```bash
|
```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
|
8. Testdaten nach erfolgreichem Lauf bereinigen
|
||||||
|
|||||||
@@ -34,8 +34,32 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
exec "$SCRIPT_DIR/immich-restore-test.sh"
|
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
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -7,24 +7,29 @@ SUCCESS_TOPIC="${2:-${RESTORE_SUCCESS_TOPIC:-homelab-info}}"
|
|||||||
FAILURE_TOPIC="${RESTORE_FAILURE_TOPIC:-homelab-alerts}"
|
FAILURE_TOPIC="${RESTORE_FAILURE_TOPIC:-homelab-alerts}"
|
||||||
|
|
||||||
if [ -z "$MODE" ]; then
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
REPORT_FILE="$REPORT_ROOT/${MODE}-$(date +%F).md"
|
REPORT_FILE="$REPORT_ROOT/${MODE}-$(date +%F).md"
|
||||||
|
WRAPPER_LOG="$REPORT_ROOT/_wrapper-${MODE}-$(date +%F).log"
|
||||||
|
|
||||||
mkdir -p "$REPORT_ROOT"
|
mkdir -p "$REPORT_ROOT"
|
||||||
|
|
||||||
echo "Running restore job: $MODE"
|
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..."
|
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
|
"$SCRIPT_DIR/send-ntfy.sh" "$SUCCESS_TOPIC" "Restore job ok: $MODE" "Restore job succeeded. Report: $REPORT_FILE" default || true
|
||||||
echo "Done"
|
echo "Done"
|
||||||
else
|
else
|
||||||
echo "Restore job failed, sending ntfy..."
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ Quartals-Belegung:
|
|||||||
| Q4 | `vaultwarden` oder `gitea` | Externe Abhaengigkeiten, Hetzner, GitHub-Mirror |
|
| Q4 | `vaultwarden` oder `gitea` | Externe Abhaengigkeiten, Hetzner, GitHub-Mirror |
|
||||||
|
|
||||||
Bestaetigte Mini-Restores: Vaultwarden, Gitea und Paperless am 2026-05-07;
|
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
|
## Konkreter Kalender
|
||||||
|
|
||||||
@@ -56,6 +57,8 @@ Immich am 2026-05-27; Paperless erneut am 2026-05-31.
|
|||||||
- `gitea`
|
- `gitea`
|
||||||
- Jeden 2. Samstag in ungeraden Monaten, 08:00:
|
- Jeden 2. Samstag in ungeraden Monaten, 08:00:
|
||||||
- `paperless`
|
- `paperless`
|
||||||
|
- Jeden 2. Samstag in geraden Monaten, 07:30:
|
||||||
|
- `authelia`
|
||||||
- Jeden 1. des Monats, 09:00:
|
- Jeden 1. des Monats, 09:00:
|
||||||
- `monthly-random-restore.sh`
|
- `monthly-random-restore.sh`
|
||||||
- Quartalsweise am 1. Werktag des Quartals:
|
- 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
|
## Unraid User Scripts Cron
|
||||||
|
|
||||||
| Script | Cron | Bedeutung |
|
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.
|
||||||
|---|---|---|
|
|
||||||
| `restore-freshness-weekly` | `30 6 * * 1` | jeden Montag 06:30 |
|
| Script | Cron | Shell-Guard (zusaetzlich) | Bedeutung |
|
||||||
| `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-freshness-weekly` | `30 6 * * 1` | - | jeden Montag 06:30 |
|
||||||
| `restore-paperless-bimonthly` | `0 8 8-14 1,3,5,7,9,11 *` | zweiter Samstag in ungeraden Monaten 08:00 |
|
| `restore-vaultwarden-monthly` | `0 7 * * 6` | `[ "$(date +%-d)" -le 7 ]` | erster Samstag im Monat 07:00 |
|
||||||
| `restore-immich-quarterly` | `30 8 8-14 2,5,8,11 0` | zweiter Sonntag in Feb/Mai/Aug/Nov 08:30 |
|
| `restore-gitea-monthly` | `15 7 * * 6` | `d=$(date +%-d); [ "$d" -ge 15 ] && [ "$d" -le 21 ]` | dritter Samstag im Monat 07:15 |
|
||||||
| `monthly-random-restore` | `0 9 1 * *` | erster Kalendertag im Monat 09:00 |
|
| `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
|
## Betriebsmodus
|
||||||
|
|
||||||
- V1:
|
- V1:
|
||||||
- Bash-Jobs laufen hostseitig manuell oder per User Script
|
- Bash-Jobs laufen hostseitig manuell oder per User Script
|
||||||
- `ntfy` ist optional und folgt nach stabiler Basis
|
- `ntfy`-Wrapper ist vorhanden; Erfolg geht nach `homelab-info`, Fehler nach `homelab-alerts`
|
||||||
- Hermes wertet spaeter nur Reports aus
|
- Hermes wertet spaeter optional Reports aus
|
||||||
- V2:
|
- V2:
|
||||||
- fester Host-Schedule
|
- fester Host-Schedule
|
||||||
- `ntfy` bei Erfolg/Fehler
|
- `ntfy` bei Erfolg/Fehler ueber `run-restore-job-with-ntfy.sh`
|
||||||
- Hermes erzeugt Zusammenfassungen und Overviews
|
- Hermes erzeugt Zusammenfassungen und Overviews
|
||||||
|
|
||||||
## Automatisierung
|
## Automatisierung
|
||||||
|
|||||||
@@ -10,18 +10,22 @@ Host-Repo-Pfad:
|
|||||||
/mnt/user/services/homelab-infra
|
/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`
|
## Script 1 - `restore-freshness-weekly`
|
||||||
|
|
||||||
Zeit:
|
Cron:
|
||||||
|
|
||||||
- Montag, 06:30
|
- `30 6 * * 1` (Montag 06:30)
|
||||||
|
|
||||||
Inhalt:
|
Inhalt:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness \
|
exec /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh \
|
||||||
> /mnt/user/backups/restore-reports/freshness-$(date +%F).md
|
freshness homelab-info
|
||||||
```
|
```
|
||||||
|
|
||||||
Erwartung:
|
Erwartung:
|
||||||
@@ -32,77 +36,133 @@ Erwartung:
|
|||||||
|
|
||||||
## Script 2 - `restore-vaultwarden-monthly`
|
## 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
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh vaultwarden \
|
# Guard: nur 1.-7. Tag im Monat, damit "1. Samstag" eindeutig getroffen wird.
|
||||||
> /mnt/user/backups/restore-reports/vaultwarden-$(date +%F).md
|
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`
|
## 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
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh gitea \
|
day=$(date +%-d)
|
||||||
> /mnt/user/backups/restore-reports/gitea-$(date +%F).md
|
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`
|
## 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
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh paperless \
|
month=$(date +%-m)
|
||||||
> /mnt/user/backups/restore-reports/paperless-$(date +%F).md
|
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
|
## Stand
|
||||||
|
|
||||||
- die Bash-Jobs wurden am 2026-05-07 hostseitig erfolgreich verifiziert
|
- die ersten Bash-Jobs wurden am 2026-05-07 hostseitig erfolgreich verifiziert
|
||||||
- `freshness`, `vaultwarden`, `gitea` und `paperless` laufen damit prinzipiell automatisch
|
- `freshness`, `vaultwarden`, `gitea`, `paperless`, `immich` und `authelia` sind als Host-Jobs verfuegbar
|
||||||
- `ntfy` kann jetzt optional per Wrapper-Skript ergaenzt werden
|
- 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
|
## Verwendete Hilfsskripte
|
||||||
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:
|
|
||||||
|
|
||||||
- `ops/restore-tests/send-ntfy.sh`
|
- `ops/restore-tests/send-ntfy.sh`
|
||||||
- `ops/restore-tests/run-restore-job-with-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"
|
WEBSOCKET_ENABLED: "true"
|
||||||
SIGNUPS_ALLOWED: "false"
|
SIGNUPS_ALLOWED: "false"
|
||||||
INVITATIONS_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_PORT: 80
|
||||||
ROCKET_ADDRESS: 0.0.0.0
|
ROCKET_ADDRESS: 0.0.0.0
|
||||||
|
|
||||||
@@ -19,7 +24,6 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/user/backups/restore-lab/vaultwarden/data:/data
|
- /mnt/user/backups/restore-lab/vaultwarden/data:/data
|
||||||
- /mnt/user/appdata/secrets/vaultwarden_admin_token.txt:/run/secrets/admin_token:ro
|
|
||||||
|
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ Nachweisen, dass ein Vaultwarden-Backup in einer isolierten Testumgebung wieder
|
|||||||
|
|
||||||
- Backup-Quelle: Borg / Share-Backup
|
- Backup-Quelle: Borg / Share-Backup
|
||||||
- fachlich relevanter Datenpfad: `/mnt/user/appdata/vaultwarden`
|
- 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
|
## Test-Ziel
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ Minimal erfolgreich:
|
|||||||
|
|
||||||
Optional spaeter:
|
Optional spaeter:
|
||||||
|
|
||||||
- Admin-Endpunkt pruefen
|
- Admin-Endpunkt nur mit separatem Wegwerf-Token pruefen
|
||||||
- Websocket-Endpunkt pruefen
|
- Websocket-Endpunkt pruefen
|
||||||
- Anzahl/Vorhandensein zentraler Daten artefaktisch verifizieren
|
- Anzahl/Vorhandensein zentraler Daten artefaktisch verifizieren
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,14 @@ require_cmd curl
|
|||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
cleanup() {
|
cleanup() {
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
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
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$DATA_DIR"
|
rm -rf "$DATA_DIR"
|
||||||
fi
|
fi
|
||||||
@@ -82,4 +88,5 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
echo "Vaultwarden restore test ok -> $REPORT_FILE"
|
echo "Vaultwarden restore test ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
## Vorbedingungen
|
## Vorbedingungen
|
||||||
|
|
||||||
- Borg-Quelle ist verfuegbar
|
- 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`
|
- 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
|
- 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
|
## Bestaetigter Host-Stand
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ Zielpfad nach dem Restore:
|
|||||||
3. Testcontainer starten
|
3. Testcontainer starten
|
||||||
|
|
||||||
```bash
|
```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
|
4. Smoke-Test
|
||||||
@@ -95,7 +95,7 @@ Minimal erfolgreich:
|
|||||||
5. Testcontainer wieder stoppen
|
5. Testcontainer wieder stoppen
|
||||||
|
|
||||||
```bash
|
```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
|
6. Report schreiben
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
scrutiny:
|
scrutiny:
|
||||||
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:41c5faefb96766d27d58a829fa19b3f4f27da4160926de3255cf142a85a90c12
|
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:d8be7c11950cfdd2ec8d85f8d76cc67fedfa2825eead6cc60d0ed753492fb5f5
|
||||||
container_name: scrutiny
|
container_name: scrutiny
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
privileged: true
|
privileged: true
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ services:
|
|||||||
- /mnt/user/appdata/vaultwarden:/data
|
- /mnt/user/appdata/vaultwarden:/data
|
||||||
- /mnt/user/appdata/secrets/vaultwarden_admin_token.txt:/run/secrets/admin_token:ro
|
- /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
|
- /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:
|
networks:
|
||||||
- frontend_net
|
- frontend_net
|
||||||
|
|||||||
Reference in New Issue
Block a user