8 Commits

29 changed files with 1721 additions and 14 deletions
+3 -1
View File
@@ -8,6 +8,7 @@ Verwandte Dokumente:
- `docs/ROLLBACK.md` - Rueckweg bei Fehlern im laufenden GitOps-Betrieb - `docs/ROLLBACK.md` - Rueckweg bei Fehlern im laufenden GitOps-Betrieb
- `docs/RESTORE_MATRIX.md` - Restore-Quellen und Verifikationsregeln pro Dienst - `docs/RESTORE_MATRIX.md` - Restore-Quellen und Verifikationsregeln pro Dienst
- `docs/RESTORE_HANDBOOK.md` - praktische Restore-Betriebsanleitung
- `ops/borg-ui/BACKUP_SCOPE.md` - Zielbild des Borg-Scopes - `ops/borg-ui/BACKUP_SCOPE.md` - Zielbild des Borg-Scopes
--- ---
@@ -139,6 +140,7 @@ Erwartete Basis unter `/mnt/user/appdata/secrets/`:
- `nextcloud_postgres_password.txt` - `nextcloud_postgres_password.txt`
- `postgres_password.txt` - `postgres_password.txt`
- `redis_password.txt` - `redis_password.txt`
- `borg_repo_passphrase.txt`
- `vaultwarden_admin_token.txt` - `vaultwarden_admin_token.txt`
- `hermes_runner_id_ed25519` - `hermes_runner_id_ed25519`
@@ -392,7 +394,7 @@ Wenn weder externer Mirror noch lokaler Clone verfuegbar sind, ist `services/git
- Unraid-USB-/Flash-Backup pruefen - Unraid-USB-/Flash-Backup pruefen
- Borg-Passphrase extern sicher hinterlegen - Borg-Passphrase extern sicher hinterlegen
- Komodo Stack-ENV-Werte zentral ausserhalb von Komodo dokumentieren - Komodo Stack-ENV-Werte zentral ausserhalb von Komodo dokumentieren
- Restore-Smoke-Test fuer mindestens einen weiteren kritischen Dienst dokumentieren - regelmaessige automatisierte Restore-Smoke-Tests fuer Vaultwarden, Gitea und Paperless etablieren
- `komodo-mongo`-Dump nach Major-Upgrades gezielt kontrollieren - `komodo-mongo`-Dump nach Major-Upgrades gezielt kontrollieren
--- ---
+30
View File
@@ -16,6 +16,36 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab
## Historische Meilensteine ## Historische Meilensteine
### 2026-05-07 - Vaultwarden Restore-Test praktisch verifiziert
- Erster echter Vaultwarden-Mini-Restore gegen das produktive Borg-Repo `hetzner_borg_appdata_critical` erfolgreich durchgefuehrt.
- Restore lief isoliert nach `/mnt/user/backups/restore-lab/vaultwarden`, nicht gegen produktive Pfade.
- Testinstanz `restoretest-vaultwarden` wurde lokal auf `127.0.0.1:18080` gestartet; HTTP 200 und Login-Seite wurden erfolgreich bestaetigt.
- Report wurde unter `/mnt/user/backups/restore-reports/vaultwarden-2026-05-07.md` geschrieben.
- Fuer den praktischen Restore-Pfad wurden zwei hostseitige Voraussetzungen sichtbar und umgesetzt:
- `known_hosts` fuer das Hetzner-Ziel im `borg-ui`-Container
- Host-Secret-Datei `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` fuer kuenftige Restore-Tests
- Testdaten unter `/mnt/user/backups/restore-lab/vaultwarden/data` wurden nach erfolgreichem Lauf wieder bereinigt.
### 2026-05-07 - Gitea Restore-Test praktisch verifiziert
- Erster echter Gitea-Mini-Restore gegen das produktive Borg-Repo `hetzner_borg_appdata_critical` erfolgreich durchgefuehrt.
- Restore lief isoliert nach `/mnt/user/backups/restore-lab/gitea`, nicht gegen produktive Pfade.
- Testinstanz `restoretest-gitea` wurde lokal auf `127.0.0.1:13000` und `127.0.0.1:12222` gestartet.
- HTTP 200, HTML-Titel und lokaler SSH-Port wurden erfolgreich bestaetigt.
- Report wurde unter `/mnt/user/backups/restore-reports/gitea-2026-05-07.md` geschrieben.
- Testdaten unter `/mnt/user/backups/restore-lab/gitea/data` wurden nach erfolgreichem Lauf wieder bereinigt.
### 2026-05-07 - Paperless Restore-Test praktisch verifiziert
- Erster echter Paperless-Mini-Restore gegen das produktive Borg-Repo `hetzner_borg_appdata_critical` erfolgreich durchgefuehrt.
- Restore umfasste sowohl die Dateipfade als auch `postgresql17-paperless.dump` aus dem Borg-Archiv.
- Testinstanzen `restoretest-paperless`, `restoretest-paperless-postgres` und `restoretest-paperless-redis` liefen isoliert ohne Traefik.
- Login-Seite war lokal auf `127.0.0.1:18120` erreichbar.
- Der Dump-Import in Test-Postgres war erfolgreich; die Test-Datenbank enthielt `25` Dokumente.
- Report wurde unter `/mnt/user/backups/restore-reports/paperless-2026-05-07.md` geschrieben.
- Testdaten unter `/mnt/user/backups/restore-lab/paperless` wurden nach erfolgreichem Lauf wieder bereinigt.
### 2026-05-06 - Komodo Webhook Secret getrennt ### 2026-05-06 - Komodo Webhook Secret getrennt
- `KOMODO_WEBHOOK_SECRET` von `KOMODO_SECRET_KEY` getrennt und als eigene Stack-ENV-Variable dokumentiert. - `KOMODO_WEBHOOK_SECRET` von `KOMODO_SECRET_KEY` getrennt und als eigene Stack-ENV-Variable dokumentiert.
+206
View File
@@ -0,0 +1,206 @@
# Restore Handbook - KalliLab CORE
Stand: 2026-05-07
Dieses Handbuch ist die praktische Betriebsanleitung fuer Restore-Checks und Restore-Lab in KalliLab CORE.
Es ergaenzt:
- `docs/RESTORE_MATRIX.md`
- `docs/DISASTER_RECOVERY.md`
- `ops/restore-tests/*`
---
## 1. Ziel
Dieses Handbuch beantwortet vier Fragen:
1. Was ist die Restore-Quelle?
2. Wo wird getestet?
3. Wie pruefen wir Erfolg?
4. Wie machen wir das regelmaessig mit wenig Handarbeit?
---
## 2. Grundmuster
Alle validierten Restore-Tests folgen demselben Muster:
- Quelle bleibt das produktive Borg-Repo bei Hetzner
- Borg-Zugriff laeuft ueber den vorhandenen `borg-ui`-Container
- Passphrase kommt aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
- Testdaten landen unter `/mnt/user/backups/restore-lab/<dienst>`
- Reports landen unter `/mnt/user/backups/restore-reports`
- Testinstanzen laufen lokal ohne Traefik und ohne produktive Domain
- nach Erfolg werden Testcontainer und Testdaten wieder entfernt
---
## 3. Bereits praktisch verifiziert
### Vaultwarden
- Report: `/mnt/user/backups/restore-reports/vaultwarden-2026-05-07.md`
- Nachweis:
- Borg-Restore erfolgreich
- Testcontainer startete
- Login-Seite war erreichbar
### Gitea
- Report: `/mnt/user/backups/restore-reports/gitea-2026-05-07.md`
- Nachweis:
- Borg-Restore erfolgreich
- Web-UI antwortete
- SSH-Port reagierte
### Paperless
- Report: `/mnt/user/backups/restore-reports/paperless-2026-05-07.md`
- Nachweis:
- Borg-Datei-Restore erfolgreich
- Paperless-Dump aus Borg importiert
- Login-Seite war erreichbar
- Test-DB enthielt `25` Dokumente
---
## 4. Verzeichnisstruktur
### Produktiv
- `/mnt/user/appdata`
- `/mnt/user/services`
- `/mnt/user/documents`
- `/mnt/user/backups/borg/dumps/latest`
### Restore-Lab
- `/mnt/user/backups/restore-lab/vaultwarden`
- `/mnt/user/backups/restore-lab/gitea`
- `/mnt/user/backups/restore-lab/paperless`
### Reports
- `/mnt/user/backups/restore-reports`
---
## 5. Restore-Frequenz
- jeden Montag, 06:30:
- Frische-Check fuer Dumps und Reports
- 1. Samstag im Monat, 07:00:
- Vaultwarden
- 3. Samstag im Monat, 07:00:
- Gitea
- jeder 2. Monat, 2. Samstag, 08:00:
- Paperless
---
## 6. Betriebsmodi
### V1
- validierte Bash-Host-Jobs
- Host-Job-Definitionen liegen im Repo
- Scheduler kann bereits echte Frische- und Restore-Checks fahren
- `ntfy` und Hermes-Auswertung folgen danach
### V2
- `ntfy` bei Erfolg/Fehler
- Hermes liest Reports und baut Uebersichten
- zusaetzliche Rotation, Sammelreports und weitere Dienste
---
## 7. User Script Jobs auf Unraid
Die Vorlagen stehen in:
- `ops/restore-tests/unraid-user-scripts.md`
Host-Repo-Pfad:
```text
/mnt/user/services/homelab
```
V1-Jobs:
1. `restore-freshness-weekly`
2. `restore-vaultwarden-monthly`
3. `restore-gitea-monthly`
4. `restore-paperless-bimonthly`
---
## 8. Erfolgskriterien
Ein Restore-Test gilt nur dann als erfolgreich, wenn:
- Restore-Quelle lesbar war
- Daten im Restore-Lab ankamen
- Testcontainer startete
- Smoke-Test erfolgreich war
- Report geschrieben wurde
Nur `Container laeuft` reicht nicht.
---
## 9. Sicherheitsregeln
- keine produktiven Pfade beschreiben
- keine produktiven Container fuer Restore-Tests verwenden
- keine produktiven Domains fuer Testinstanzen verwenden
- keine Secrets im Repo
- keine Restore-Automatik fuer neue Dienste ohne bewusste Freigabe
---
## 10. Schnellstart
### Frische-Check
Auf dem Unraid-Host:
```bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh freshness
```
### Vaultwarden Restore-Check
```bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh vaultwarden
```
### Gitea Restore-Check
```bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh gitea
```
### Paperless Restore-Check
```bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh paperless
```
### Optional mit `ntfy`
```bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-restore
```
---
## 11. Naechste Ausbaustufen
1. Vollautomatik fuer Vaultwarden, Gitea und Paperless
2. `ntfy`-Meldungen fuer Erfolg/Fehler
3. Hermes-Zusammenfassung ueber vorhandene Reports
4. naechster Referenz-Restore fuer `mail-archiver` oder `mealie`
+15 -5
View File
@@ -32,9 +32,9 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
| PostgreSQL 17 | Share + Dumps | `/mnt/user/appdata/postgresql17` | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt` | `backend_net` | DB startet, Ziel-Datenbanken vorhanden | | PostgreSQL 17 | Share + Dumps | `/mnt/user/appdata/postgresql17` | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt` | `backend_net` | DB startet, Ziel-Datenbanken vorhanden |
| Redis | Share / Host | `/mnt/user/appdata/redis` | keine | `redis_password.txt` | `backend_net` | Redis startet, Apps verbinden sich | | Redis | Share / Host | `/mnt/user/appdata/redis` | keine | `redis_password.txt` | `backend_net` | Redis startet, Apps verbinden sich |
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 17, 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, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 17, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut |
| Gitea | Borg / Share | `/mnt/user/services/gitea/data` | SQLite in `/data` | keine separaten Secret-Dateien dokumentiert | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert | | Gitea | Borg / Share | `/mnt/user/services/gitea/data` | SQLite in `/data` | `borg_repo_passphrase.txt` fuer Restore-Tests | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert; 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` | `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` | `komodo-mongo.archive.gz` falls verifiziert | `komodo_mongo_password.txt`, `KOMODO_*` Stack ENV | Traefik, Mongo, Gitea | UI erreichbar, Periphery verbunden |
| Vaultwarden | Borg / Share | `/mnt/user/appdata/vaultwarden` | dateibasiert | `vaultwarden_admin_token.txt` | Traefik | Login-Seite erreichbar, Tresor-Daten sichtbar | | Vaultwarden | Borg / Share | `/mnt/user/appdata/vaultwarden` | dateibasiert | `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 |
--- ---
@@ -42,7 +42,7 @@ 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` | PostgreSQL 17, Redis, Traefik | Web-UI startet, Dokumente vorhanden | | 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 17, Redis, Traefik | Web-UI startet, Dokumente vorhanden; Mini-Restore nach `/mnt/user/backups/restore-lab/paperless` am 2026-05-07 erfolgreich validiert |
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, optional `/mnt/user/appdata/mealie/postgres` bei lokalem Share-Weiterbetrieb | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden | | Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, optional `/mnt/user/appdata/mealie/postgres` bei lokalem Share-Weiterbetrieb | `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` | `immich.dump` | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt` | `immich_postgres`, `immich_redis`, Traefik | UI startet, Medienbibliothek sichtbar | | Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | `immich.dump` | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt` | `immich_postgres`, `immich_redis`, Traefik | UI startet, Medienbibliothek sichtbar |
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 17, 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 17, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen |
@@ -95,14 +95,24 @@ Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.s
- Bei Unsicherheit zuerst in Testpfade oder Testinstanzen pruefen. - Bei Unsicherheit zuerst in Testpfade oder Testinstanzen pruefen.
- Der minimale Erfolg ist **nicht** "Container startet", sondern "fachlicher Smoke-Test gelingt". - Der minimale Erfolg ist **nicht** "Container startet", sondern "fachlicher Smoke-Test gelingt".
### Validiertes Restore-Lab-Muster
- Restore-Quelle bleibt das produktive Borg-Repo
- Testziel liegt unter `/mnt/user/backups/restore-lab/<dienst>`
- Reports liegen unter `/mnt/user/backups/restore-reports`
- Borg-Zugriff kann ueber den vorhandenen `borg-ui`-Container laufen
- Borg-Passphrasen fuer Restore-Tests sollten aus Host-Secret-Dateien kommen, nicht aus UI-Interna
- Testinstanzen bekommen keine produktive Domain und keine Traefik-Route
--- ---
## Erste sinnvolle Referenz-Restores ## Erste sinnvolle Referenz-Restores
Wenn weitere Restore-Uebungen dokumentiert werden sollen, sind diese Dienste besonders geeignet: Wenn weitere Restore-Uebungen dokumentiert werden sollen, sind diese Dienste besonders geeignet:
1. `gitea` 1. `mail-archiver`
2. `paperless-ngx` 2. `paperless-ngx`
3. `vaultwarden` 3. `gitea`
4. `vaultwarden`
Sie liefern hohen Erkenntnisgewinn ohne den kompletten Homelab-Neuaufbau zu brauchen. Sie liefern hohen Erkenntnisgewinn ohne den kompletten Homelab-Neuaufbau zu brauchen.
+3
View File
@@ -76,6 +76,9 @@ Vor lokaler Arbeit:
Nach lokaler Arbeit: Nach lokaler Arbeit:
1. Aenderungen pruefen 1. Aenderungen pruefen
2. bei Compose-/Backup-/Restore-Aenderungen relevante manuelle Repo-Checks ausfuehren
- `ops/policy-checks/check_repo.ps1`
- `ops/restore-tests/check-restore-freshness.ps1` oder gezielte Restore-Checks
2. Commit mit sauberer Nachricht 2. Commit mit sauberer Nachricht
3. `Push origin` 3. `Push origin`
4. Komodo-Webhook im Hinterkopf behalten 4. Komodo-Webhook im Hinterkopf behalten
+45 -6
View File
@@ -21,9 +21,23 @@ Ziel:
- `schedule.md`: Intervalle und Verantwortlichkeiten - `schedule.md`: Intervalle und Verantwortlichkeiten
- `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-plan.md`: konkreter Vaultwarden-Testplan - `vaultwarden-plan.md`: konkreter Vaultwarden-Testplan
- `vaultwarden-compose.test.yml`: isolierte Testinstanz fuer Vaultwarden - `vaultwarden-compose.test.yml`: isolierte Testinstanz fuer Vaultwarden
- spaeter weitere Tests fuer `gitea` und `paperless` - `gitea-restore-test.ps1`: Gitea-Mini-Restore-Ablauf
- `gitea-restore-test.sh`: hosttauglicher Gitea-Restore-Job
- `gitea-plan.md`: konkreter Gitea-Testplan
- `gitea-compose.test.yml`: isolierte Testinstanz fuer Gitea
- `paperless-restore-test.ps1`: Paperless-Mini-Restore-Ablauf
- `paperless-restore-test.sh`: hosttauglicher Paperless-Restore-Job
- `paperless-plan.md`: konkreter Paperless-Testplan
- `paperless-compose.test.yml`: isolierte Testinstanz fuer Paperless inkl. Test-Postgres und Test-Redis
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
- `run-restore-checks.sh`: hosttauglicher Dispatcher
- `common.sh`: gemeinsame Host-Helferfunktionen
- `automation-plan.md`: Host-Job- und Automatisierungsmodell
## Automatisierungsmodell ## Automatisierungsmodell
@@ -33,13 +47,38 @@ Ziel:
- Meldung: `ntfy` - Meldung: `ntfy`
- Hermes: optional nur fuer Zusammenfassung und Auswertung - Hermes: optional nur fuer Zusammenfassung und Auswertung
Wichtig:
- die Bash-Skripte `*.sh` sind die produktive Host-Variante
- `check-restore-freshness.ps1` und die `*.ps1`-Dateien bleiben als lokale Plan-/Hilfsvariante nutzbar
- im Windows-Clone fehlen die `/mnt/user/...`-Pfade naturgemaess
## Validiertes Grundmuster
Stand nach dem ersten echten Vaultwarden-Test:
- Borg-Quelle bleibt das produktive Remote-Repo bei Hetzner
- Borg-Zugriff laeuft praktisch ueber den vorhandenen `borg-ui`-Container
- SSH-Trust wird ueber `known_hosts` im `borg-ui`-Container hergestellt
- die Borg-Passphrase kommt fuer Restore-Tests aus einer Host-Secret-Datei
- Restore-Ziel liegt immer getrennt unter `/mnt/user/backups/restore-lab`
- Reports liegen unter `/mnt/user/backups/restore-reports`
- Testinstanzen bekommen keine produktive Domain und keine Traefik-Route
Das ist das bevorzugte Muster fuer weitere dateibasierte Restore-Tests wie `gitea`.
Fuer datenbankgestuetzte Dienste wie `paperless` kommt zusaetzlich ein isolierter Dump-Restore in Test-Postgres dazu.
## Status ## Status
Aktuell ist hier nur die erste Repo-Vorbereitung angelegt. Aktuell ist das erste validierte Muster vorhanden.
- noch kein echter Restore-Lauf - echter Vaultwarden-Restore am 2026-05-07 erfolgreich verifiziert
- noch keine Host-Pfade beschrieben - echter Gitea-Restore am 2026-05-07 erfolgreich verifiziert
- noch keine Container gestartet - echter Paperless-Restore am 2026-05-07 erfolgreich verifiziert
- erster V1-Ablauf ohne `ntfy`, mit Bereinigung nach Erfolg - Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
- Restore-Lab und Report-Pfade auf dem Host angelegt
- V1-Ablauf weiter ohne `ntfy`, mit Bereinigung nach Erfolg
- naechster grosser Kandidat ist ein weiterer datenbankgestuetzter Dienst oder die Automatisierung
Vor dem ersten echten Testlauf muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden. Vor dem ersten echten Testlauf muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
+78
View File
@@ -0,0 +1,78 @@
# Restore Automation Plan
## Ziel
Die bereits validierten Restore-Tests fuer `vaultwarden`, `gitea` und `paperless` sollen regelmaessig mit wenig Handarbeit laufen.
## Prinzip
- Ausfuehrung bleibt hostseitig
- Logik bleibt im Repo
- Reports bleiben unter `/mnt/user/backups/restore-reports`
- Restore-Arbeitsdaten bleiben unter `/mnt/user/backups/restore-lab`
- Hermes ist Reporter, nicht Operator
## V1
### Woechentlicher Frische-Check
- Script: `check-restore-freshness.sh`
- Ziel:
- Dump-Dateien vorhanden
- Dump-Dateien nicht zu alt
- letzte Restore-Reports vorhanden
- Wirkung:
- schneller Fruehwarncheck ohne Containerstart
### Monatliche / zweimonatliche Restore-Jobs
- Script-Dispatcher: `run-restore-checks.sh`
- Modi:
- `freshness`
- `vaultwarden`
- `gitea`
- `paperless`
- diese Bash-Jobs sind jetzt hostseitig praktisch verifiziert
- die `*.ps1`-Dateien bleiben als Plan-/Hilfsvariante fuer die Windows-Arbeitskopie erhalten
## V2
- `ntfy` Erfolg/Fehler
- optional Hermes-Zusammenfassung ueber vorhandene Reports
- spaeter Job-Metadaten, Rotation und Sammel-Reports weiter ausbauen
### `ntfy`-Muster
- Script: `run-restore-job-with-ntfy.sh`
- Hilfsskript: `send-ntfy.sh`
- nur kurze Erfolg/Fehler-Meldung
- eigentlicher Detailnachweis bleibt die Markdown-Reportdatei
## Host-Integration
Empfohlen:
- Unraid User Scripts
- je ein geplanter Job pro Laufklasse
- Ausfuehrung auf dem Unraid-Host, nicht im Windows-Clone
Beispiel:
1. `restore-freshness-weekly`
2. `restore-vaultwarden-monthly`
3. `restore-gitea-monthly`
4. `restore-paperless-bimonthly`
## Erfolgskriterium
Ein automatisierter Lauf ist nur dann erfolgreich, wenn:
- Script sauber endet
- Report geschrieben wird
- bei echten Restore-Laeufen der definierte Smoke-Test erfolgreich war
## Nicht automatisieren
- neue Restore-Typen ohne bewusste Freigabe
- invasive Produktiv-Restores
- Komodo-/Auth-/Secret-Umbauten im selben Job
@@ -0,0 +1,88 @@
param(
[string]$DumpRoot = "/mnt/user/backups/borg/dumps/latest",
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
[int]$MaxDumpAgeHours = 36,
[int]$MaxReportAgeDays = 45
)
$checks = @(
@{ Name = "postgresql17-paperless.dump"; Path = Join-Path $DumpRoot "postgresql17-paperless.dump" },
@{ Name = "postgresql17-mailarchiver.dump"; Path = Join-Path $DumpRoot "postgresql17-mailarchiver.dump" },
@{ Name = "mealie.dump"; Path = Join-Path $DumpRoot "mealie.dump" },
@{ Name = "immich.dump"; Path = Join-Path $DumpRoot "immich.dump" }
)
$reportChecks = @(
@{ Name = "vaultwarden"; Path = Join-Path $ReportRoot "vaultwarden-*.md" },
@{ Name = "gitea"; Path = Join-Path $ReportRoot "gitea-*.md" },
@{ Name = "paperless"; Path = Join-Path $ReportRoot "paperless-*.md" }
)
$now = Get-Date
$critical = New-Object System.Collections.Generic.List[string]
$warnings = New-Object System.Collections.Generic.List[string]
$info = New-Object System.Collections.Generic.List[string]
foreach ($check in $checks) {
if (-not (Test-Path $check.Path)) {
$critical.Add("DUMP_MISSING $($check.Name)")
continue
}
$item = Get-Item $check.Path
$ageHours = ($now - $item.LastWriteTime).TotalHours
if ($ageHours -gt $MaxDumpAgeHours) {
$warnings.Add(("DUMP_STALE {0} age={1:N1}h" -f $check.Name, $ageHours))
} else {
$info.Add(("DUMP_OK {0} age={1:N1}h" -f $check.Name, $ageHours))
}
}
foreach ($check in $reportChecks) {
$latest = Get-ChildItem -Path $ReportRoot -Filter ([System.IO.Path]::GetFileName($check.Path)) -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if (-not $latest) {
$warnings.Add("REPORT_MISSING $($check.Name)")
continue
}
$ageDays = ($now - $latest.LastWriteTime).TotalDays
if ($ageDays -gt $MaxReportAgeDays) {
$warnings.Add(("REPORT_STALE {0} age={1:N1}d file={2}" -f $check.Name, $ageDays, $latest.Name))
} else {
$info.Add(("REPORT_OK {0} age={1:N1}d file={2}" -f $check.Name, $ageDays, $latest.Name))
}
}
Write-Output "# Restore Freshness Check"
Write-Output ""
Write-Output ("Timestamp: {0}" -f $now.ToString("yyyy-MM-dd HH:mm:ss"))
Write-Output ("Critical: {0}" -f $critical.Count)
Write-Output ("Warnings: {0}" -f $warnings.Count)
Write-Output ("Info: {0}" -f $info.Count)
Write-Output ""
if ($critical.Count -gt 0) {
Write-Output "## Critical"
$critical | ForEach-Object { Write-Output ("- {0}" -f $_) }
Write-Output ""
}
if ($warnings.Count -gt 0) {
Write-Output "## Warnings"
$warnings | ForEach-Object { Write-Output ("- {0}" -f $_) }
Write-Output ""
}
if ($info.Count -gt 0) {
Write-Output "## Info"
$info | ForEach-Object { Write-Output ("- {0}" -f $_) }
}
if ($critical.Count -gt 0) {
exit 1
}
exit 0
@@ -0,0 +1,81 @@
#!/bin/bash
set -euo pipefail
DUMP_ROOT="${DUMP_ROOT:-/mnt/user/backups/borg/dumps/latest}"
REPORT_ROOT="${REPORT_ROOT:-/mnt/user/backups/restore-reports}"
MAX_DUMP_AGE_HOURS="${MAX_DUMP_AGE_HOURS:-36}"
MAX_REPORT_AGE_DAYS="${MAX_REPORT_AGE_DAYS:-45}"
now_epoch="$(date +%s)"
critical=()
warnings=()
info=()
check_file_age_hours() {
local path="$1"
local mtime
mtime="$(stat -c %Y "$path")"
echo $(( (now_epoch - mtime) / 3600 ))
}
check_file_age_days() {
local path="$1"
local mtime
mtime="$(stat -c %Y "$path")"
echo $(( (now_epoch - mtime) / 86400 ))
}
for dump in postgresql17-paperless.dump postgresql17-mailarchiver.dump mealie.dump immich.dump; do
path="$DUMP_ROOT/$dump"
if [ ! -f "$path" ]; then
critical+=("DUMP_MISSING $dump")
continue
fi
age="$(check_file_age_hours "$path")"
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
warnings+=("DUMP_STALE $dump age=${age}h")
else
info+=("DUMP_OK $dump age=${age}h")
fi
done
for service in vaultwarden gitea paperless; do
latest="$(find "$REPORT_ROOT" -maxdepth 1 -type f -name "$service-*.md" | sort | tail -n 1 || true)"
if [ -z "$latest" ]; then
warnings+=("REPORT_MISSING $service")
continue
fi
age="$(check_file_age_days "$latest")"
if [ "$age" -gt "$MAX_REPORT_AGE_DAYS" ]; then
warnings+=("REPORT_STALE $service age=${age}d file=$(basename "$latest")")
else
info+=("REPORT_OK $service age=${age}d file=$(basename "$latest")")
fi
done
echo "# Restore Freshness Check"
echo
echo "Timestamp: $(date '+%F %T')"
echo "Critical: ${#critical[@]}"
echo "Warnings: ${#warnings[@]}"
echo "Info: ${#info[@]}"
echo
if [ "${#critical[@]}" -gt 0 ]; then
echo "## Critical"
printf -- '- %s\n' "${critical[@]}"
echo
fi
if [ "${#warnings[@]}" -gt 0 ]; then
echo "## Warnings"
printf -- '- %s\n' "${warnings[@]}"
echo
fi
if [ "${#info[@]}" -gt 0 ]; then
echo "## Info"
printf -- '- %s\n' "${info[@]}"
fi
[ "${#critical[@]}" -eq 0 ]
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
set -euo pipefail
RESTORE_TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
BORG_RESTORE_HOST_ROOT="${BORG_RESTORE_HOST_ROOT:-/mnt/user/appdata/borg-ui/restore}"
BORG_PASSPHRASE_FILE_DEFAULT="${BORG_PASSPHRASE_FILE_DEFAULT:-/mnt/user/appdata/secrets/borg_repo_passphrase.txt}"
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing command: $1" >&2
exit 1
}
}
require_path() {
[ -e "$1" ] || {
echo "Missing path: $1" >&2
exit 1
}
}
latest_archive_name() {
docker exec "$BORG_CONTAINER" python3 - <<'PY'
import sqlite3
conn = sqlite3.connect('/data/borg.db')
cur = conn.cursor()
cur.execute("select archive_name from backup_jobs where status='completed' order by created_at desc limit 1")
row = cur.fetchone()
if not row:
raise SystemExit("No completed borg archive found")
print(row[0])
PY
}
borg_repo_url() {
docker exec "$BORG_CONTAINER" python3 - <<'PY'
import sqlite3
conn = sqlite3.connect('/data/borg.db')
cur = conn.cursor()
cur.execute("select path from repositories where path is not null and path != '' order by id asc limit 1")
row = cur.fetchone()
if not row:
raise SystemExit("No borg repository configured")
print(row[0])
PY
}
borg_extract() {
local extract_dir="$1"
shift
local paths=("$@")
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
import os, sys, subprocess
extract_dir = sys.argv[1]
paths = sys.argv[2:]
import sqlite3
conn = sqlite3.connect('/data/borg.db')
cur = conn.cursor()
cur.execute("select path from repositories where path is not null and path != '' order by id asc limit 1")
repo = cur.fetchone()[0]
cur.execute("select archive_name from backup_jobs where status='completed' order by created_at desc limit 1")
archive = cur.fetchone()[0]
with open('/local/secrets/borg_repo_passphrase.txt', 'r', encoding='utf-8') as f:
os.environ['BORG_PASSPHRASE'] = f.read().strip()
os.makedirs(extract_dir, exist_ok=True)
os.chdir(extract_dir)
subprocess.run(['borg', 'extract', f'{repo}::{archive}', *paths], check=True)
PY
}
write_report() {
local report_file="$1"
shift
mkdir -p "$(dirname "$report_file")"
cat > "$report_file"
}
cleanup_compose() {
local compose_file="$1"
if [ -f "$compose_file" ]; then
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
fi
}
+23
View File
@@ -0,0 +1,23 @@
services:
restoretest-gitea:
image: docker.gitea.com/gitea:1.25.4@sha256:17d18218be2dad1f8ed402a4f906989505c90ab8b66ee9befcecfb5d470133e7
container_name: restoretest-gitea
restart: "no"
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__server__DOMAIN: 127.0.0.1
GITEA__server__ROOT_URL: http://127.0.0.1:13000/
GITEA__database__DB_TYPE: sqlite3
GITEA__webhook__ALLOWED_HOST_LIST: "*"
ports:
- "127.0.0.1:13000:3000"
- "127.0.0.1:12222:22"
volumes:
- /mnt/user/backups/restore-lab/gitea/data:/data
security_opt:
- no-new-privileges:true
+59
View File
@@ -0,0 +1,59 @@
# Gitea Restore Test Plan
## Ziel
Nachweisen, dass ein Gitea-Backup in einer isolierten Testumgebung wieder startbar ist und sowohl Web-UI als auch SSH-Port wieder verfuegbar sind.
## Quelle
- Backup-Quelle: Borg / Share-Backup
- fachlich relevanter Datenpfad: `/mnt/user/services/gitea/data`
- keine separaten Secret-Dateien dokumentiert
## Test-Ziel
- Restore-Lab: `/mnt/user/backups/restore-lab/gitea`
- Testdatenpfad: `/mnt/user/backups/restore-lab/gitea/data`
- Testcontainer: `restoretest-gitea`
- Testports:
- Web: `127.0.0.1:13000:3000`
- SSH: `127.0.0.1:12222:22`
- Report-Ziel: `/mnt/user/backups/restore-reports/gitea-YYYY-MM-DD.md`
## Schutzregeln
- produktiven Pfad `/mnt/user/services/gitea/data` nie beschreiben
- produktive Domain `git.kaleschke.info` nicht fuer die Testinstanz uebernehmen
- produktiven SSH-Port `222` nicht fuer die Testinstanz uebernehmen
- keine Traefik-Labels fuer die Testinstanz
- Testcontainer nur gegen Restore-Lab-Daten starten
## Geplanter Ablauf
1. Restore-Ziel unter `/mnt/user/backups/restore-lab/gitea` vorbereiten
2. Gitea-Daten aus Backup in `restore-lab/gitea/data` wiederherstellen
3. Testinstanz mit `ops/restore-tests/gitea-compose.test.yml` starten
4. lokalen Smoke-Test gegen `http://127.0.0.1:13000` und `127.0.0.1:12222` ausfuehren
5. Report unter `/mnt/user/backups/restore-reports/` schreiben
6. Testcontainer stoppen und Testumgebung bereinigen oder bewusst stehen lassen
## Smoke-Test
Minimal erfolgreich:
- Container startet
- Web-UI antwortet
- mindestens ein bestehendes Repository-Verzeichnis ist im Restore-Lab sichtbar
- SSH-Port reagiert auf Verbindungsaufbau
Optional spaeter:
- Login-Seite gezielt pruefen
- SQLite-Datei `gitea.db` oder Nachfolger explizit bestaetigen
- `gitea doctor` oder interner Healthcheck als Zusatz
## Noch offen vor dem ersten echten Lauf
- exakter Borg-Restore-Befehl bzw. Restore-Quelle auf dem Host
- Bereinigungsstrategie fuer alte Restore-Lab-Daten
- ob Reports spaeter zusaetzlich per `ntfy` referenziert werden
+36
View File
@@ -0,0 +1,36 @@
param(
[string]$BackupSource = "/mnt/user/backups/borg",
[string]$RestoreRoot = "/mnt/user/backups/restore-lab/gitea",
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
[string]$BorgPassphraseFile = "/mnt/user/appdata/secrets/borg_repo_passphrase.txt",
[switch]$WhatIf
)
$Mode = if ($WhatIf) { "WhatIf" } else { "PlanOnly" }
Write-Output "Gitea restore test scaffold"
Write-Output "BackupSource: $BackupSource"
Write-Output "RestoreRoot: $RestoreRoot"
Write-Output "ReportRoot: $ReportRoot"
Write-Output "BorgPassphraseFile: $BorgPassphraseFile"
Write-Output "Expected Borg source path inside archive: local/gitea/data"
if ($WhatIf) {
Write-Output "Mode: WhatIf"
} else {
Write-Output "Mode: PlanOnly"
}
Write-Output ""
Write-Output "Planned steps:"
Write-Output "1. Prepare restore-lab target under /mnt/user/backups/restore-lab/gitea"
Write-Output "2. Restore Gitea data into an isolated test path"
Write-Output ' Template: borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/gitea/data'
Write-Output ' Passphrase source: $(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)'
Write-Output "3. Start container restoretest-gitea against test data only"
Write-Output "4. Run smoke checks against the local web and SSH endpoints"
Write-Output "5. Write markdown report under /mnt/user/backups/restore-reports"
Write-Output "6. Stop test container and clean restore data after success"
Write-Output ""
Write-Output "This script is intentionally a scaffold only."
Write-Output "No restore, no container start, no file write is executed yet."
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
set -euo pipefail
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/gitea"
REPORT_ROOT="/mnt/user/backups/restore-reports"
DATA_DIR="$RESTORE_ROOT/data"
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/gitea-extract"
COMPOSE_FILE="$SCRIPT_DIR/gitea-compose.test.yml"
REPORT_FILE="$REPORT_ROOT/gitea-$(date +%F).md"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Gitea restore test
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
ReportRoot: $REPORT_ROOT
Expected Borg source path: local/gitea/data
EOF
exit 0
fi
require_cmd docker
require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$DATA_DIR"
fi
rm -rf "$EXTRACT_DIR"
}
trap cleanup EXIT
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
mkdir -p "$RESTORE_ROOT"
archive="$(latest_archive_name)"
repo="$(borg_repo_url)"
borg_extract "/restore/gitea-extract" "local/gitea/data"
mv "$EXTRACT_DIR/local/gitea/data" "$DATA_DIR"
repo_sample="$(find "$DATA_DIR/git/repositories" -maxdepth 3 -type d | sed -n '2p')"
docker compose -f "$COMPOSE_FILE" up -d >/dev/null
sleep 8
status="$(curl -s -o /tmp/gitea-body.html -w '%{http_code}' http://127.0.0.1:13000)"
grep -qi "Gitea" /tmp/gitea-body.html
if timeout 5 bash -lc '</dev/tcp/127.0.0.1/12222' >/dev/null 2>&1; then
ssh_state="open"
else
echo "Gitea SSH port not reachable" >&2
exit 1
fi
write_report "$REPORT_FILE" <<EOF
# Gitea Restore Test Report - $(date +%F)
- Service: \`gitea\`
- Source repo: \`$repo\`
- Archive: \`$archive\`
- Restore target: \`$DATA_DIR\`
- Test container: \`restoretest-gitea\`
- Test endpoints:
- Web: \`http://127.0.0.1:13000\`
- SSH: \`127.0.0.1:12222\`
- Result: \`SUCCESS\`
## Checks
- Borg extract into isolated restore-lab: \`ok\`
- HTTP status: \`$status\`
- HTML content: \`Gitea\`
- SSH port: \`$ssh_state\`
- Repository sample: \`$repo_sample\`
## Notes
- Test ran without Traefik and without the productive domain.
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
echo "Gitea restore test ok -> $REPORT_FILE"
+107
View File
@@ -0,0 +1,107 @@
# Gitea Restore Runbook
## Vorbedingungen
- Borg-Quelle ist verfuegbar
- 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
## Bestaetigter Host-Stand
- produktive Gitea-Daten liegen unter `/mnt/user/services/gitea/data`
- Borg-Scope fuer Gitea ist `/local/gitea/data`
- `restore-lab` und `restore-reports` sind auf dem Host vorhanden
## Bestaetigter Teststand
- echter Mini-Restore am `2026-05-07` erfolgreich gelaufen
- Restore-Ziel war `/mnt/user/backups/restore-lab/gitea/data`
- Testcontainer `restoretest-gitea` lief lokal auf `127.0.0.1:13000` und `127.0.0.1:12222`
- HTTP `200 OK`, HTML-Titel und SSH-Port wurden erfolgreich verifiziert
- Report liegt unter `/mnt/user/backups/restore-reports/gitea-2026-05-07.md`
## Platzhalter
- `ARCHIVE_NAME`: Borg-Archiv fuer den Restore-Test
- `REPORT_DATE`: z. B. `2026-05-07`
- `BORG_REPO`: Host-Borg-Repo
- `BORG_PASSPHRASE_FILE`: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
## Ablauf
1. Testpfade vorbereiten
```bash
mkdir -p /mnt/user/backups/restore-lab/gitea/data
mkdir -p /mnt/user/backups/restore-reports
rm -rf /mnt/user/backups/restore-lab/gitea/data/*
```
2. Gitea-Daten aus Borg in das Restore-Lab extrahieren
Archiv zuerst pruefen:
```bash
export BORG_REPO='...'
export BORG_PASSPHRASE="$(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)"
borg list "$BORG_REPO"
```
Restore in das Testziel:
```bash
cd /mnt/user/backups/restore-lab/gitea
borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/gitea/data
mv /mnt/user/backups/restore-lab/gitea/local/gitea/data /mnt/user/backups/restore-lab/gitea/data
rmdir /mnt/user/backups/restore-lab/gitea/local/gitea
rmdir /mnt/user/backups/restore-lab/gitea/local
```
Wenn das Archiv den Pfad anders ablegt, zuerst mit `borg list "$BORG_REPO" "::ARCHIVE_NAME"` den exakten Eintrag pruefen.
3. Testcontainer starten
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml up -d
```
4. Smoke-Test
```bash
curl -I http://127.0.0.1:13000
ssh -p 12222 -o BatchMode=yes -o StrictHostKeyChecking=no git@127.0.0.1
docker logs restoretest-gitea --tail 50
```
Minimal erfolgreich:
- HTTP-Antwort kommt
- Login-Seite ist erreichbar
- SSH-Port antwortet
- Restore-Lab enthaelt Gitea-Daten
5. Testcontainer wieder stoppen
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml down
```
6. Report schreiben
Ziel:
```text
/mnt/user/backups/restore-reports/gitea-REPORT_DATE.md
```
7. Testdaten nach erfolgreichem Lauf bereinigen
```bash
rm -rf /mnt/user/backups/restore-lab/gitea/data
```
## Festgelegte Entscheidungen
- Testdaten werden nach erfolgreichem Lauf geloescht.
- `ntfy` wird nicht im ersten echten Lauf eingebunden.
- Die Borg-Passphrase wird fuer Restore-Tests aus einer Host-Secret-Datei gelesen, nicht aus Borg-UI-Interna.
@@ -0,0 +1,61 @@
services:
restoretest-paperless-postgres:
image: postgres:17.9@sha256:5b96f1a16bd9768b060dd2ffe55cb6225c4d9ef4d214a8b21eb08134869a97e4
container_name: restoretest-paperless-postgres
restart: "no"
environment:
TZ: Europe/Berlin
POSTGRES_USER: paperless
POSTGRES_DB: paperless
POSTGRES_PASSWORD: restoretest-paperless-db
PGDATA: /var/lib/postgresql/data
volumes:
- /mnt/user/backups/restore-lab/paperless/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U paperless -d paperless"]
interval: 10s
timeout: 5s
retries: 10
security_opt:
- no-new-privileges:true
restoretest-paperless-redis:
image: redis:7-alpine
container_name: restoretest-paperless-redis
restart: "no"
command:
- sh
- -c
- exec redis-server --appendonly yes --requirepass "restoretest-paperless-redis"
security_opt:
- no-new-privileges:true
restoretest-paperless:
image: ghcr.io/paperless-ngx/paperless-ngx:2.20.10@sha256:07a0b4ba01ce377c82a0636e16c0c3d931fde5b7e9304de6601986cc42d9b6e6
container_name: restoretest-paperless
restart: "no"
depends_on:
restoretest-paperless-postgres:
condition: service_healthy
restoretest-paperless-redis:
condition: service_started
environment:
PAPERLESS_TIKA_ENABLED: "0"
PAPERLESS_DBENGINE: postgresql
PAPERLESS_DBHOST: restoretest-paperless-postgres
PAPERLESS_DBNAME: paperless
PAPERLESS_DBUSER: paperless
PAPERLESS_DBPASS: restoretest-paperless-db
PAPERLESS_REDIS: redis://:restoretest-paperless-redis@restoretest-paperless-redis:6379
PAPERLESS_TIME_ZONE: Europe/Berlin
PAPERLESS_OCR_LANGUAGE: deu+eng
PAPERLESS_URL: http://127.0.0.1:18120
ports:
- "127.0.0.1:18120:8000"
volumes:
- /mnt/user/backups/restore-lab/paperless/consume:/usr/src/paperless/consume
- /mnt/user/backups/restore-lab/paperless/data:/usr/src/paperless/data
- /mnt/user/backups/restore-lab/paperless/export:/usr/src/paperless/export
- /mnt/user/backups/restore-lab/paperless/media:/usr/src/paperless/media
security_opt:
- no-new-privileges:true
+72
View File
@@ -0,0 +1,72 @@
# Paperless Restore Test Plan
## Ziel
Nachweisen, dass ein Paperless-Backup in einer isolierten Testumgebung wieder startbar ist und sowohl Dokumentenpfade als auch PostgreSQL-Dump sauber zusammenlaufen.
## Quelle
- Backup-Quelle: Borg / Share-Backup
- fachlich relevante Dateipfade:
- `/mnt/user/appdata/paperless-ngx/data`
- `/mnt/user/documents/paperless`
- `/mnt/user/documents/paperless/export`
- `/mnt/user/documents/scans_inbox`
- fachlich relevanter Dump:
- `/mnt/user/backups/borg/dumps/latest/postgresql17-paperless.dump`
## Test-Ziel
- Restore-Lab: `/mnt/user/backups/restore-lab/paperless`
- Testdatenpfade:
- `/mnt/user/backups/restore-lab/paperless/data`
- `/mnt/user/backups/restore-lab/paperless/media`
- `/mnt/user/backups/restore-lab/paperless/export`
- `/mnt/user/backups/restore-lab/paperless/consume`
- `/mnt/user/backups/restore-lab/paperless/postgres`
- Testcontainer:
- `restoretest-paperless`
- `restoretest-paperless-postgres`
- `restoretest-paperless-redis`
- Testport Web: `127.0.0.1:18120:8000`
- Report-Ziel: `/mnt/user/backups/restore-reports/paperless-YYYY-MM-DD.md`
## Schutzregeln
- produktive Pfade nie beschreiben
- produktive Domain `paperless.kaleschke.info` nicht fuer die Testinstanz uebernehmen
- keine Traefik-Labels fuer die Testinstanz
- keine produktive PostgreSQL- oder Redis-Instanz fuer den Test verwenden
- Testcontainer nur gegen Restore-Lab-Daten und isolierte Test-Backends starten
## Geplanter Ablauf
1. Restore-Ziel unter `/mnt/user/backups/restore-lab/paperless` vorbereiten
2. Paperless-Dateipfade aus Borg in das Restore-Lab wiederherstellen
3. Test-Postgres und Test-Redis mit `ops/restore-tests/paperless-compose.test.yml` starten
4. `postgresql17-paperless.dump` in Test-Postgres importieren
5. Testinstanz `restoretest-paperless` starten
6. lokalen Smoke-Test gegen `http://127.0.0.1:18120` ausfuehren
7. Report unter `/mnt/user/backups/restore-reports/` schreiben
8. Testcontainer stoppen und Testumgebung bereinigen
## Smoke-Test
Minimal erfolgreich:
- Test-Postgres startet
- Dump-Import gelingt
- Paperless-Web-UI antwortet
- mindestens ein Dokument liegt im Restore-Lab-Medienpfad
Optional spaeter:
- Login-Seite gezielt pruefen
- Dokumentanzahl aus UI oder DB querpruefen
- OCR-/Task-Worker-Status verifizieren
## Noch offen vor dem ersten echten Lauf
- exakter Borg-Restore-Befehl fuer alle vier Dateipfade
- exakter `pg_restore`-Befehl im Test-Postgres
- wie stark wir `consume` im ersten Lauf ueberhaupt brauchen
@@ -0,0 +1,38 @@
param(
[string]$BackupSource = "/mnt/user/backups/borg",
[string]$DumpSource = "/mnt/user/backups/borg/dumps/latest/postgresql17-paperless.dump",
[string]$RestoreRoot = "/mnt/user/backups/restore-lab/paperless",
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
[string]$BorgPassphraseFile = "/mnt/user/appdata/secrets/borg_repo_passphrase.txt",
[switch]$WhatIf
)
$Mode = if ($WhatIf) { "WhatIf" } else { "PlanOnly" }
Write-Output "Paperless restore test scaffold"
Write-Output "BackupSource: $BackupSource"
Write-Output "DumpSource: $DumpSource"
Write-Output "RestoreRoot: $RestoreRoot"
Write-Output "ReportRoot: $ReportRoot"
Write-Output "BorgPassphraseFile: $BorgPassphraseFile"
Write-Output "Expected Borg source paths inside archive:"
Write-Output " - local/appdata/paperless-ngx/data"
Write-Output " - local/paperless/media"
Write-Output " - local/paperless/export"
Write-Output " - local/paperless/consume"
Write-Output "Mode: $Mode"
Write-Output ""
Write-Output "Planned steps:"
Write-Output "1. Prepare restore-lab target under /mnt/user/backups/restore-lab/paperless"
Write-Output "2. Restore Paperless file data into isolated test paths"
Write-Output ' Template: borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/appdata/paperless-ngx/data local/paperless/media local/paperless/export local/paperless/consume'
Write-Output ' Passphrase source: $(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)'
Write-Output "3. Start isolated test Postgres and test Redis"
Write-Output "4. Import /mnt/user/backups/borg/dumps/latest/postgresql17-paperless.dump into test Postgres"
Write-Output "5. Start restoretest-paperless against restored files and isolated DB/Redis"
Write-Output "6. Run smoke checks against the local web endpoint"
Write-Output "7. Write markdown report under /mnt/user/backups/restore-reports"
Write-Output "8. Stop test containers and clean restore data after success"
Write-Output ""
Write-Output "This script is intentionally a scaffold only."
Write-Output "No restore, no dump import, no container start, no file write is executed yet."
+113
View File
@@ -0,0 +1,113 @@
#!/bin/bash
set -euo pipefail
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/paperless"
REPORT_ROOT="/mnt/user/backups/restore-reports"
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/paperless-extract"
COMPOSE_FILE="$SCRIPT_DIR/paperless-compose.test.yml"
REPORT_FILE="$REPORT_ROOT/paperless-$(date +%F).md"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Paperless restore test
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
ReportRoot: $REPORT_ROOT
Expected Borg source paths:
- local/appdata/paperless-ngx/data
- local/paperless/media
- local/paperless/export
- local/paperless/consume
- local/borg-dumps/latest/postgresql17-paperless.dump
EOF
exit 0
fi
require_cmd docker
require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
cleanup() {
cleanup_compose "$COMPOSE_FILE"
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/postgres" "$RESTORE_ROOT/dumps/latest"
archive="$(latest_archive_name)"
repo="$(borg_repo_url)"
borg_extract "/restore/paperless-extract" \
"local/appdata/paperless-ngx/data" \
"local/paperless/media" \
"local/paperless/export" \
"local/paperless/consume" \
"local/borg-dumps/latest/postgresql17-paperless.dump"
mv "$EXTRACT_DIR/local/appdata/paperless-ngx/data" "$RESTORE_ROOT/data"
mv "$EXTRACT_DIR/local/paperless/media" "$RESTORE_ROOT/media"
mv "$EXTRACT_DIR/local/paperless/export" "$RESTORE_ROOT/export"
mv "$EXTRACT_DIR/local/paperless/consume" "$RESTORE_ROOT/consume"
mv "$EXTRACT_DIR/local/borg-dumps/latest/postgresql17-paperless.dump" "$RESTORE_ROOT/dumps/latest/postgresql17-paperless.dump"
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless-postgres restoretest-paperless-redis >/dev/null
until docker exec restoretest-paperless-postgres pg_isready -U paperless -d paperless >/dev/null 2>&1; do sleep 2; done
cat "$RESTORE_ROOT/dumps/latest/postgresql17-paperless.dump" | docker exec -i restoretest-paperless-postgres pg_restore -U paperless -d paperless --clean --if-exists --no-owner --no-privileges
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless >/dev/null
sleep 12
status="$(curl -s -o /tmp/paperless-body.html -w '%{http_code}' -L http://127.0.0.1:18120)"
grep -qi "Paperless-ngx sign in" /tmp/paperless-body.html
doc_count="$(docker exec restoretest-paperless-postgres psql -U paperless -d paperless -tAc "select count(*) from documents_document;" | tr -d '[:space:]')"
doc_sample="$(find "$RESTORE_ROOT/media/documents/originals" -type f | sed -n '1p')"
write_report "$REPORT_FILE" <<EOF
# Paperless Restore Test Report - $(date +%F)
- Service: \`paperless-ngx\`
- Source repo: \`$repo\`
- Archive: \`$archive\`
- Restore root: \`$RESTORE_ROOT\`
- Test containers:
- \`restoretest-paperless\`
- \`restoretest-paperless-postgres\`
- \`restoretest-paperless-redis\`
- Test endpoint: \`http://127.0.0.1:18120\`
- Result: \`SUCCESS\`
## Checks
- Borg extract of file data: \`ok\`
- Borg extract of dump: \`ok\`
- Dump import into isolated Postgres: \`ok\`
- HTTP status after redirect: \`$status\`
- Login page content: \`Paperless-ngx sign in\`
- Document count in test DB: \`$doc_count\`
- Document sample in media path: \`$doc_sample\`
## Notes
- Test ran without Traefik and without the productive domain.
- Test used isolated Postgres and Redis containers.
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
echo "Paperless restore test ok -> $REPORT_FILE"
+115
View File
@@ -0,0 +1,115 @@
# Paperless Restore Runbook
## Vorbedingungen
- Borg-Quelle ist verfuegbar
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
- aktueller Dump vorhanden: `/mnt/user/backups/borg/dumps/latest/postgresql17-paperless.dump`
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
## Bestaetigter Host-Stand
- produktive Paperless-Daten liegen unter `/mnt/user/appdata/paperless-ngx/data`
- produktive Medien liegen unter `/mnt/user/documents/paperless`
- produktiver Exportpfad liegt unter `/mnt/user/documents/paperless/export`
- produktiver Consume-Pfad liegt unter `/mnt/user/documents/scans_inbox`
- aktueller Dump `postgresql17-paperless.dump` ist vorhanden
## Bestaetigter Teststand
- echter Mini-Restore am `2026-05-07` erfolgreich gelaufen
- Datei-Restore und Dump kamen aus dem produktiven Borg-Archiv
- Testcontainer:
- `restoretest-paperless`
- `restoretest-paperless-postgres`
- `restoretest-paperless-redis`
- Login-Seite war lokal auf `127.0.0.1:18120` erreichbar
- Dump-Import in Test-Postgres war erfolgreich
- Test-Datenbank enthielt `25` Dokumente
- Report liegt unter `/mnt/user/backups/restore-reports/paperless-2026-05-07.md`
## Platzhalter
- `ARCHIVE_NAME`: Borg-Archiv fuer den Restore-Test
- `REPORT_DATE`: z. B. `2026-05-07`
- `BORG_REPO`: Host-Borg-Repo
- `BORG_PASSPHRASE_FILE`: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
## Ablauf
1. Testpfade vorbereiten
```bash
mkdir -p /mnt/user/backups/restore-lab/paperless/{data,media,export,consume,postgres}
mkdir -p /mnt/user/backups/restore-reports
rm -rf /mnt/user/backups/restore-lab/paperless/data/*
rm -rf /mnt/user/backups/restore-lab/paperless/media/*
rm -rf /mnt/user/backups/restore-lab/paperless/export/*
rm -rf /mnt/user/backups/restore-lab/paperless/consume/*
rm -rf /mnt/user/backups/restore-lab/paperless/postgres/*
```
2. Paperless-Dateipfade aus Borg in das Restore-Lab extrahieren
```bash
export BORG_REPO='...'
export BORG_PASSPHRASE="$(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)"
borg list "$BORG_REPO"
cd /mnt/user/backups/restore-lab/paperless
borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/appdata/paperless-ngx/data local/paperless/media local/paperless/export local/paperless/consume
mv /mnt/user/backups/restore-lab/paperless/local/appdata/paperless-ngx/data /mnt/user/backups/restore-lab/paperless/data
mv /mnt/user/backups/restore-lab/paperless/local/paperless/media /mnt/user/backups/restore-lab/paperless/media
mv /mnt/user/backups/restore-lab/paperless/local/paperless/export /mnt/user/backups/restore-lab/paperless/export
mv /mnt/user/backups/restore-lab/paperless/local/paperless/consume /mnt/user/backups/restore-lab/paperless/consume
```
3. Test-Postgres und Test-Redis starten
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless-postgres restoretest-paperless-redis
```
4. Dump in Test-Postgres importieren
```bash
docker exec -i restoretest-paperless-postgres pg_restore -U paperless -d paperless < /mnt/user/backups/borg/dumps/latest/postgresql17-paperless.dump
```
5. Testinstanz starten
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless
```
6. Smoke-Test
```bash
curl -I http://127.0.0.1:18120
docker logs restoretest-paperless --tail 50
find /mnt/user/backups/restore-lab/paperless/media -type f | head -n 10
```
Minimal erfolgreich:
- Dump-Import gelingt
- HTTP-Antwort kommt
- Dokumentenpfad ist im Restore-Lab befuellt
7. Testcontainer wieder stoppen
```bash
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml down
```
8. Testdaten nach erfolgreichem Lauf bereinigen
```bash
rm -rf /mnt/user/backups/restore-lab/paperless
```
## Festgelegte Entscheidungen
- Paperless nutzt fuer Restore-Tests immer isoliertes Test-Postgres und Test-Redis.
- Testdaten werden nach erfolgreichem Lauf geloescht.
- `ntfy` wird nicht im ersten echten Lauf eingebunden.
- Die Borg-Passphrase wird fuer Restore-Tests aus einer Host-Secret-Datei gelesen, nicht aus Borg-UI-Interna.
+38
View File
@@ -0,0 +1,38 @@
param(
[ValidateSet("freshness","vaultwarden","gitea","paperless")]
[string]$Mode,
[switch]$WhatIf
)
$base = Split-Path -Parent $MyInvocation.MyCommand.Path
switch ($Mode) {
"freshness" {
& (Join-Path $base "check-restore-freshness.ps1")
exit $LASTEXITCODE
}
"vaultwarden" {
if ($WhatIf) {
& (Join-Path $base "vaultwarden-restore-test.ps1") -WhatIf
} else {
& (Join-Path $base "vaultwarden-restore-test.ps1")
}
exit $LASTEXITCODE
}
"gitea" {
if ($WhatIf) {
& (Join-Path $base "gitea-restore-test.ps1") -WhatIf
} else {
& (Join-Path $base "gitea-restore-test.ps1")
}
exit $LASTEXITCODE
}
"paperless" {
if ($WhatIf) {
& (Join-Path $base "paperless-restore-test.ps1") -WhatIf
} else {
& (Join-Path $base "paperless-restore-test.ps1")
}
exit $LASTEXITCODE
}
}
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
MODE="${1:-}"
WHATIF="${2:-}"
case "$MODE" in
freshness)
exec "$SCRIPT_DIR/check-restore-freshness.sh"
;;
vaultwarden)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/vaultwarden-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/vaultwarden-restore-test.sh"
;;
gitea)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/gitea-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/gitea-restore-test.sh"
;;
paperless)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/paperless-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/paperless-restore-test.sh"
;;
*)
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless} [--what-if]" >&2
exit 1
;;
esac
@@ -0,0 +1,29 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
MODE="${1:-}"
TOPIC="${2:-homelab-restore}"
if [ -z "$MODE" ]; then
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless> [topic]" >&2
exit 1
fi
REPORT_ROOT="/mnt/user/backups/restore-reports"
REPORT_FILE="$REPORT_ROOT/${MODE}-$(date +%F).md"
mkdir -p "$REPORT_ROOT"
echo "Running restore job: $MODE"
echo "Report target: $REPORT_FILE"
if "$SCRIPT_DIR/run-restore-checks.sh" "$MODE" > "$REPORT_FILE"; then
echo "Restore job succeeded, sending ntfy..."
"$SCRIPT_DIR/send-ntfy.sh" "$TOPIC" "Restore job ok: $MODE" "Restore job succeeded. Report: $REPORT_FILE" default || true
echo "Done"
else
echo "Restore job failed, sending ntfy..."
"$SCRIPT_DIR/send-ntfy.sh" "$TOPIC" "Restore job failed: $MODE" "Restore job failed. Report: $REPORT_FILE" high || true
exit 1
fi
+24
View File
@@ -37,6 +37,30 @@ Spaeter:
- `immich` als eigener Sprint - `immich` als eigener Sprint
## Konkreter Kalender
- Jeden Montag, 06:30:
- `check-restore-freshness.sh`
- Jeden 1. Samstag im Monat, 07:00:
- `vaultwarden`
- Jeden 3. Samstag im Monat, 07:00:
- `gitea`
- Jeden 2. Monat am 2. Samstag, 08:00:
- `paperless`
- Quartalsweise am 1. Werktag des Quartals:
- DR-/Restore-Sanity-Check
## Betriebsmodus
- V1:
- Bash-Jobs laufen hostseitig manuell oder per User Script
- `ntfy` ist optional und folgt nach stabiler Basis
- Hermes wertet spaeter nur Reports aus
- V2:
- fester Host-Schedule
- `ntfy` bei Erfolg/Fehler
- Hermes erzeugt Zusammenfassungen und Overviews
## Automatisierung ## Automatisierung
Automatisch: Automatisch:
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
set -euo pipefail
TOPIC="${1:-}"
TITLE="${2:-}"
MESSAGE="${3:-}"
PRIORITY="${4:-default}"
if [ -z "$TOPIC" ] || [ -z "$TITLE" ] || [ -z "$MESSAGE" ]; then
echo "Usage: $0 <topic> <title> <message> [priority]" >&2
exit 1
fi
NTFY_URL="${NTFY_URL:-https://ntfy.kaleschke.info}"
curl -fsS \
--connect-timeout 5 \
--max-time 10 \
-H "Title: $TITLE" \
-H "Priority: $PRIORITY" \
-d "$MESSAGE" \
"$NTFY_URL/$TOPIC" >/dev/null
+106
View File
@@ -0,0 +1,106 @@
# Unraid User Scripts fuer Restore-Checks
## Ziel
Diese Vorlagen binden die validierten Restore-Checks in Unraid User Scripts ein.
Host-Repo-Pfad:
```text
/mnt/user/services/homelab
```
## Script 1 - `restore-freshness-weekly`
Zeit:
- Montag, 06:30
Inhalt:
```bash
#!/bin/bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh freshness \
> /mnt/user/backups/restore-reports/freshness-$(date +%F).md
```
Erwartung:
- prueft Dump-Frische
- prueft Report-Frische
- startet keine Container
## Script 2 - `restore-vaultwarden-monthly`
Zeit:
- 1. Samstag im Monat, 07:00
V1-Inhalt:
```bash
#!/bin/bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh vaultwarden \
> /mnt/user/backups/restore-reports/vaultwarden-$(date +%F).md
```
## Script 3 - `restore-gitea-monthly`
Zeit:
- 3. Samstag im Monat, 07:00
V1-Inhalt:
```bash
#!/bin/bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh gitea \
> /mnt/user/backups/restore-reports/gitea-$(date +%F).md
```
## Script 4 - `restore-paperless-bimonthly`
Zeit:
- jeder 2. Monat, 2. Samstag, 08:00
V1-Inhalt:
```bash
#!/bin/bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh paperless \
> /mnt/user/backups/restore-reports/paperless-$(date +%F).md
```
## Stand
- die Bash-Jobs wurden am 2026-05-07 hostseitig erfolgreich verifiziert
- `freshness`, `vaultwarden`, `gitea` und `paperless` laufen damit prinzipiell automatisch
- `ntfy` kann jetzt optional per Wrapper-Skript ergaenzt werden
## V2 Zielbild
Als naechster Ausbau kommen dazu:
1. Restore aus Borg
2. Testcontainer starten
3. Smoke-Test
4. Report schreiben
5. optional `ntfy`
6. Bereinigung
## Optionales `ntfy` Wrapper-Muster
Wenn `ntfy` genutzt wird, soll der Host-Job nur Erfolg/Fehler referenzieren, nicht den ganzen Report in die Nachricht kippen.
Beispiel:
```bash
#!/bin/bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-restore
```
Verwendete Hilfsskripte:
- `ops/restore-tests/send-ntfy.sh`
- `ops/restore-tests/run-restore-job-with-ntfy.sh`
@@ -2,6 +2,7 @@ param(
[string]$BackupSource = "/mnt/user/backups/borg", [string]$BackupSource = "/mnt/user/backups/borg",
[string]$RestoreRoot = "/mnt/user/backups/restore-lab/vaultwarden", [string]$RestoreRoot = "/mnt/user/backups/restore-lab/vaultwarden",
[string]$ReportRoot = "/mnt/user/backups/restore-reports", [string]$ReportRoot = "/mnt/user/backups/restore-reports",
[string]$BorgPassphraseFile = "/mnt/user/appdata/secrets/borg_repo_passphrase.txt",
[switch]$WhatIf [switch]$WhatIf
) )
@@ -12,6 +13,7 @@ Write-Output "Vaultwarden restore test scaffold"
Write-Output "BackupSource: $BackupSource" Write-Output "BackupSource: $BackupSource"
Write-Output "RestoreRoot: $RestoreRoot" Write-Output "RestoreRoot: $RestoreRoot"
Write-Output "ReportRoot: $ReportRoot" Write-Output "ReportRoot: $ReportRoot"
Write-Output "BorgPassphraseFile: $BorgPassphraseFile"
Write-Output "Expected Borg source path inside archive: local/appdata/vaultwarden" Write-Output "Expected Borg source path inside archive: local/appdata/vaultwarden"
if ($WhatIf) { if ($WhatIf) {
@@ -25,6 +27,7 @@ Write-Output "Planned steps:"
Write-Output "1. Prepare restore-lab target under /mnt/user/backups/restore-lab/vaultwarden" Write-Output "1. Prepare restore-lab target under /mnt/user/backups/restore-lab/vaultwarden"
Write-Output "2. Restore Vaultwarden data into an isolated test path" Write-Output "2. Restore Vaultwarden data into an isolated test path"
Write-Output ' Template: borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/appdata/vaultwarden' Write-Output ' Template: borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/appdata/vaultwarden'
Write-Output ' Passphrase source: $(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)'
Write-Output "3. Start container restoretest-vaultwarden against test data only" Write-Output "3. Start container restoretest-vaultwarden against test data only"
Write-Output "4. Run smoke checks against the test instance" Write-Output "4. Run smoke checks against the test instance"
Write-Output "5. Write markdown report under /mnt/user/backups/restore-reports" Write-Output "5. Write markdown report under /mnt/user/backups/restore-reports"
@@ -0,0 +1,85 @@
#!/bin/bash
set -euo pipefail
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/vaultwarden"
REPORT_ROOT="/mnt/user/backups/restore-reports"
DATA_DIR="$RESTORE_ROOT/data"
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/vaultwarden-extract"
COMPOSE_FILE="$SCRIPT_DIR/vaultwarden-compose.test.yml"
REPORT_FILE="$REPORT_ROOT/vaultwarden-$(date +%F).md"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Vaultwarden restore test
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
ReportRoot: $REPORT_ROOT
Expected Borg source path: local/appdata/vaultwarden
EOF
exit 0
fi
require_cmd docker
require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$DATA_DIR"
fi
rm -rf "$EXTRACT_DIR"
}
trap cleanup EXIT
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
mkdir -p "$RESTORE_ROOT"
archive="$(latest_archive_name)"
repo="$(borg_repo_url)"
borg_extract "/restore/vaultwarden-extract" "local/appdata/vaultwarden"
mv "$EXTRACT_DIR/local/appdata/vaultwarden" "$DATA_DIR"
docker compose -f "$COMPOSE_FILE" up -d >/dev/null
sleep 8
status="$(curl -s -o /tmp/vaultwarden-body.html -w '%{http_code}' http://127.0.0.1:18080)"
grep -qi "vaultwarden" /tmp/vaultwarden-body.html
write_report "$REPORT_FILE" <<EOF
# Vaultwarden Restore Test Report - $(date +%F)
- Service: \`vaultwarden\`
- Source repo: \`$repo\`
- Archive: \`$archive\`
- Restore target: \`$DATA_DIR\`
- Test container: \`restoretest-vaultwarden\`
- Test endpoint: \`http://127.0.0.1:18080\`
- Result: \`SUCCESS\`
## Checks
- Borg extract into isolated restore-lab: \`ok\`
- HTTP status: \`$status\`
- Login page content: \`ok\`
## Notes
- Test ran without Traefik and without the productive domain.
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
echo "Vaultwarden restore test ok -> $REPORT_FILE"
+25 -2
View File
@@ -4,14 +4,36 @@
- Borg-Quelle ist verfuegbar - Borg-Quelle ist verfuegbar
- Secret-Datei vorhanden: `/mnt/user/appdata/secrets/vaultwarden_admin_token.txt` - Secret-Datei vorhanden: `/mnt/user/appdata/secrets/vaultwarden_admin_token.txt`
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben - Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
## Bestaetigter Host-Stand
- `borg` ist im Container `borg-ui` verfuegbar
- `BORG_BACKUP_PATH` ist im `borg-ui`-Container auf `/backups` gesetzt
- produktive Vaultwarden-Daten liegen unter `/mnt/user/appdata/vaultwarden`
- produktives Admin-Token liegt unter `/mnt/user/appdata/secrets/vaultwarden_admin_token.txt`
- `restore-lab` und `restore-reports` sind auf dem Host vorhanden
## Beobachtete Borg-Hinweise
- beobachtetes produktives Repo: `ssh://u565255@u565255.your-storagebox.de:23/./hetzner_borg_appdata_critical`
- beobachtete Archivnamen:
- `Taegliche-Sicherung-2026-04-16T04:30:02.798`
- `Taegliche-Sicherung-2026-04-17T04:30:31.660`
- aeltere manuelle Beispiele:
- `manual-backup-2026-04-12T17:17:30`
- `manual-backup-2026-04-12T17:35:17`
Hinweis:
Vor dem ersten echten Restore-Lauf immer das aktuelle Archiv mit `borg list` erneut pruefen.
## Platzhalter ## Platzhalter
- `ARCHIVE_NAME`: Borg-Archiv fuer den Restore-Test - `ARCHIVE_NAME`: Borg-Archiv fuer den Restore-Test
- `REPORT_DATE`: z. B. `2026-05-06` - `REPORT_DATE`: z. B. `2026-05-06`
- `BORG_REPO`: Host-Borg-Repo, z. B. das produktive `critical_infra` - `BORG_REPO`: Host-Borg-Repo, z. B. das produktive `critical_infra`
- `BORG_PASSPHRASE`: wie im bestehenden Host-Setup - `BORG_PASSPHRASE_FILE`: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
## Ablauf ## Ablauf
@@ -29,7 +51,7 @@ Archiv zuerst pruefen:
```bash ```bash
export BORG_REPO='...' export BORG_REPO='...'
export BORG_PASSPHRASE='...' export BORG_PASSPHRASE="$(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)"
borg list "$BORG_REPO" borg list "$BORG_REPO"
``` ```
@@ -95,3 +117,4 @@ rm -rf /mnt/user/backups/restore-lab/vaultwarden/data
- Testdaten werden nach erfolgreichem Lauf geloescht. - Testdaten werden nach erfolgreichem Lauf geloescht.
- `ntfy` wird nicht im ersten echten Lauf eingebunden. - `ntfy` wird nicht im ersten echten Lauf eingebunden.
- `ntfy` folgt spaeter, wenn der manuelle Basisablauf stabil verifiziert ist. - `ntfy` folgt spaeter, wenn der manuelle Basisablauf stabil verifiziert ist.
- Die Borg-Passphrase wird fuer Restore-Tests aus einer Host-Secret-Datei gelesen, nicht aus Borg-UI-Interna.