E1 Backup & Restore: Umsetzungsplan¶
Für ausführende Agenten: Pflicht-Skill:
superpowers:subagent-driven-development(empfohlen) odersuperpowers:executing-plans. Aufgaben Schritt für Schritt abarbeiten, Fortschritt über die Checkboxen (- [ ]) festhalten.
Ziel: Jeder Host sichert verschlüsselt auf die UNAS und in einen eigenen Sub-Account der Storage Box. Snapshots auf beiden Seiten schützen gegen Löschen. Der VPS ist gesichert, jeden Morgen kommt ein Status. Robin hat drei Restore-Rezepte geprobt, und Lars hat ein Störungs-Handbuch.
Architektur: - Borg 1.4 + borgmatic auf NUC, aiserver und VPS. - Lokal schreibt jeder Host per NFS in ein eigenes Repo auf einer eigenen UNAS-Freigabe mit Btrfs-Snapshots. - Außer Haus schreibt jeder Host über Port 23 in einen eigenen Sub-Account der Storage Box. Die Box hat einen täglichen Snapshot-Plan. - Die Originale (Fotos, Paperless, Archiv) liest der NUC per NFS und schickt sie nur in sein Box-Repo. - Alarmiert wird über das selbst gehostete Healthchecks (Totmann) und ntfy, dazu kommt ein Morgenstatus-Skript auf dem NUC.
Technik: borg 1.4.5 (Standalone-Binary), borgmatic 2.1.5 (pipx), Hetzner Storage Box BX11 mit Console/hcloud ≥ 1.61, UniFi Drive (UNAS Pro), NFS v3, systemd-Timer, Python 3 (nur Standardbibliothek) für den Morgenstatus.
Spec: backup-design.md (homelab-documentation#85). Plan und Spec gehören zusammen, ausführende Agenten lesen beide. Abschnittsnummern wie „Spec 8“ beziehen sich darauf.
Globale Vorgaben¶
- Robins Hand. Claude schreibt das Runbook und führt nichts davon aus. Robin führt aus, Claude prüft danach lesend:
- Schreibzugriffe als root auf NUC, aiserver und VPS (Skill
homelab-infra: „Produktions-Writes macht Robin selbst“) - alles in der Hetzner Console bzw. mit einem
hcloud-Token mit Schreibrecht - alles in der UniFi-Oberfläche
sudoauf dem aiserver- Runbook: Jede Aufgabe mit Robins Hand bekommt vor der Ausführung einen Abschnitt in
homelab-documentation/docs/runbooks/backup-e1-umbau.md(Nav: „Runbooks“). Jeder Schritt dort hat einen Befehl, eine Erwartung und ein Abbruch-Kriterium (Muster: Runbook 2 in homeserver#216). - Zeitfenster: Kein Eingriff zwischen 01:45 und 03:15 (NUC 02:00, VPS 02:30) und zwischen 19:45 und 20:45 (aiserver 20:00). Vor jedem Eingriff muss
systemctl is-active borgmatic.service→inactiveliefern. - Secrets:
- nie im Klartext ausgeben, nie in Git
- Passphrasen als
/etc/borgmatic.d/credentials/<name>.pass(root 0600, Verzeichnis 0700), Quelle der Wahrheit Infisical/borgmatic(EU-Region), Notfallkopie Proton Pass - neue Infisical-Namen:
NUC_BORG_PASSPHRASE_UNAS,AISERVER_BORG_PASSPHRASE_UNAS,AISERVER_BORG_PASSPHRASE_BOX,VPS_BORG_PASSPHRASE_BOX,STORAGEBOX_MAIN_PASSWORD,STORAGEBOX_RESTORE_PASSWORD,HETZNER_API_TOKEN_RO,HEALTHCHECKS_API_KEY_RO, dazu*_BORG_KEY_*für die Paper-Keys - Werte prüfen nur per Hash-Präfix (Muster Runbook 2, Schritt 6)
- Secrets in Configs über
{credential file /etc/borgmatic.d/credentials/<name>}. Unterstützt borgmatic 2.1.5 das für eine Option nicht, anhalten und melden. Kein Klartext als Ausweg. - Storage Box:
remote_path: borg-1.4in jeder Config mit Box-Ziel- Port 23, Schlüssel im OpenSSH-Format
- Box-Zeiten in UTC
- Sub-Accounts und Layout:
| Sub-Account | Home | Repo-Pfad in der Config | Rechte |
|---|---|---|---|
nuc |
backups/nuc |
ssh://<sub>@<sub>.your-storagebox.de:23/./repo |
SSH, schreiben |
aiserver |
backups/aiserver |
…:23/./repo |
SSH, schreiben |
vps |
backups/vps |
…:23/./repo |
SSH, schreiben |
restore |
backups |
…:23/./<host>/repo |
SSH, read-only |
Die bestehenden Repos ziehen per mv (Box-Shell, Hauptaccount) von backups/<host> nach backups/<host>/repo um. Grund: Das Home eines Sub-Accounts enthält dessen .ssh/authorized_keys und darf deshalb nicht selbst das Repo sein. Weil sich der Ort des Repos ändert, braucht jede betroffene Config relocated_repo_access_is_ok: true.
- Box-Snapshots: Plan täglich um 03:30 UTC, --max-snapshots 10. Vor jeder Umstellung ein manueller Snapshot vor-e1-<schritt>.
- UNAS-Freigaben:
| Freigabe | NFS-Export für | Snapshots | Inhalt |
|---|---|---|---|
backup-nuc |
10.10.10.3 | täglich, 14 behalten | Borg-Repo NUC lokal, Appliance-Exporte unter exports/ |
backup-aiserver |
10.10.10.7 | täglich, 14 behalten | Borg-Repo aiserver lokal |
archiv |
10.10.10.3 | täglich, 14 behalten | Archivordner (Spec 7) |
pictures, paperless (bestehend) |
wie heute | täglich, 14 behalten | Originale |
Mountpunkte: NUC /mnt/backup-unas (für backup-nuc) und /mnt/archiv, aiserver /mnt/backup-unas. Repo-Pfad lokal: /mnt/backup-unas/repo.
- Mount-Units für Backup-Ziele: nofail, nicht in docker.service.d/10-wait-for-mounts.conf (#278).
- Verschlüsselung: jedes neue Repo mit repokey-blake2 und eigener Passphrase aus 32 zufälligen Zeichen.
- Alarmwege:
- Fehler über ntfy-Topic homelab-critical, Morgenstatus über homelab-alerts
- ntfy immer per JSON-Body veröffentlichen, nicht über X-Title-Header: Header sind latin-1-kodiert, Umlaute brechen sonst still
- Totmann über das selbst gehostete Healthchecks, ein Check je Host und Config
- Uptime-Kuma-Push fliegt aus allen Configs (Robin, 30.10.)
- Schwellen: Box-Alarm ab 80 % belegt · UNAS-Alarm unter 15 % frei · Snapshot gilt als veraltet ab 26 h · Morgenstatus um 07:00.
- Kein Admin auf Hosts: kein UniFi-Admin, kein UNAS-Root-SSH, kein Box-Hauptpasswort, kein Hetzner-Token mit Schreibrecht. Lese-Token und „View Only“ sind erlaubt.
- Doku: Deutsch mit echten Umlauten. Code, Commits und Variablennamen auf Englisch, Conventional Commits, PRs gegen main, nie direkt auf main.
- Log: Am Ende jeder Aufgabe ein Eintrag in docs/planung/2026-10-e1/umsetzung-log.md, neueste oben. Am Sitzungsende ein Eintrag im Roadmap-Log docs/planung/2026-08/umsetzungs-log.md.
Review-Fokus¶
Fünf Fälle, die die Spec nahelegt, die aber keine Aufgabe von selbst abdeckt. Zu jedem steht der Test in der zuständigen Aufgabe.
- Der Ziel-Mount fehlt, das leere Verzeichnis
/mnt/backup-unasaber existiert: Der Lauf muss scheitern und darf kein neues Repo anlegen. Test in Task 4, Schritt 7. - Ein Host greift mit seinem Key auf das Repo eines anderen zu:
aiserver→backups/nucmuss scheitern, ebensorestorebeim Schreiben. Test in Task 2, Schritt 9. - Der Morgenstatus bekommt keine Antwort von Healthchecks oder der Hetzner-API, zum Beispiel weil der VPS aus ist: Die Nachricht kommt trotzdem und meldet „Quelle nicht erreichbar“ als rot, statt still auszufallen oder grün zu zeigen. Test in Task 7, Schritt 2.
- Ein einzelner DB-Dump scheitert: Das Borg-Backup läuft weiter, der Dump-Check wird rot, und im Morgenstatus steht der Name des Dumps. Test in Task 6, Schritt 4.
- Erster Lauf nach dem Repo-Umzug auf der Box: Läuft er ohne Rückfrage zum Ortswechsel durch? Und bricht keine gleichzeitige Prune-Aktion etwas ab? Test in Task 2, Schritt 8.
Task 0: Issues schneiden, Ausgangslage festhalten¶
Dateien:
- Erstellen: docs/planung/2026-10-e1/umsetzung-log.md
- Erstellen: docs/runbooks/backup-e1-umbau.md (Kopf und Vorgaben, die Abschnitte kommen je Aufgabe)
- Ändern: mkdocs.yml (Nav: Log unter „2026-10 E1“, Runbook unter „Runbooks“)
Schnittstellen:
- Erzeugt: die Teil-Issues T1–T7 aus Spec 15 in homeserver, mit Label des Epics, auf dem Board (Projects #4), jedes mit Verweis auf diesen Plan und die Task-Nummern:
- T1 = Task 1, 2, 3
- T2 = Task 4
- T3 = Task 5
- T4 = Task 6
- T5 = Task 7
- T6 = Task 8
- T7 = Task 9
- Schritt 1: Issues anlegen per
gh issue createundgh project item-add 4 --owner SWATPeaceKeeper. Erwartet: 7 Issues auf dem Board. In #216 und #179 kommt ein Kommentar mit den Links. Die Übergaben aus Spec 18 werden als Kommentare festgehalten: - in #278 (E3): Der Kaltstart-Drill gehört jetzt dorthin.
- in #188 (E5): Lars kommt ins Tailnet.
- in #182 (E2b): Vor dem VPS-Rebuild ein Hetzner-Backup in einen geschützten Snapshot umwandeln (Spec 8).
- Schritt 2: Ausgangslage lesend erfassen und als Abschnitt „Ausgangslage“ ins Log schreiben:
borgmatic --version,borg --versionauf NUC und aiserverrepo-info --json→unique_csizebeider NUC-Reposdf -B1 /mnt/storageboxls -l /etc/borgmatic.d /etc/borgmatic.d/credentials- die Nachtläufe der letzten 7 Tage (
Successfully-Zählung) - Schritt 3: Runbook-Kopf schreiben: Zeitfenster, Vorbedingungen, Abbruch-Muster, Secrets-Regeln (aus den globalen Vorgaben).
- Schritt 4: Commit und PR (
docs(e1): add rollout log and runbook skeleton).
Task 1: NUC-Configs ins Git, ohne Verhaltensänderung¶
Dateien:
- Erstellen: homeserver/backup/borgmatic/local.yaml, homeserver/backup/borgmatic/hetzner.yaml. Inhalt wie live, aber jedes Secret als Credential-Datei.
- Erstellen: homeserver/backup/borgmatic/README.md mit Deploy-Befehl, Credential-Dateien (nur Namen) und Verweis auf die Spec.
- Ändern: homeserver/.gitignore, falls nötig (keine *.pass und keine credentials/ im Repo-Pfad).
- Runbook: Abschnitt „Task 1“.
Schnittstellen:
- Erzeugt:
- Credential-Dateien auf dem NUC: /etc/borgmatic.d/credentials/ntfy.token und healthchecks-nuc-local.url, healthchecks-nuc-hetzner.url
- der Deploy-Weg, den alle späteren Tasks nutzen: install -o root -g root -m 0600 /opt/rpi-docker-compose/backup/borgmatic/<datei>.yaml /etc/borgmatic.d/ && borgmatic config validate
- Schritt 1: Live-Configs redigiert lesen. Muster:
sedmaskiert URLs, Token und Benutzer. Die Struktur ins Repo übernehmen, Secret-Werte durch{credential file …}ersetzen.uptime_kuma:streichen. - Schritt 2: Prüfen, ob borgmatic 2.1.5
{credential file …}fürntfy.access_tokenundhealthchecks.ping_urlunterstützt. Doku perctx7, dann ein Probelauf im Wegwerf-Container mit borgmatic 2.1.5 undconfig validategegen eine Datei mit Dummy-Credentials. Erwartet:All configuration files are valid. Scheitert es, anhalten (globale Vorgaben). - Schritt 3: Runbook-Abschnitt schreiben:
- Credential-Dateien aus den Live-Werten ziehen, per ruamel wie in Runbook 2, ohne Anzeige
git pullin/opt/rpi-docker-compose- Ist-Stand sichern:
cp -anach*.bak-<datum> install, dannvalidatediffder aufgelösten Werte, per Hash-Vergleich je Credential gegen die alte Config- Gegenprobe:
borgmatic --config /etc/borgmatic.d/local.yaml repo-list | tail -n 2 - Schritt 4: Robin führt aus.
- Schritt 5: Nachweis am nächsten Morgen (Claude, lesend):
- beide Configs
Successfully ran - im Journal kein
Pushing Uptime Kuma - Healthchecks zeigt beide Checks
up grep -c "http" /etc/borgmatic.d/*.yaml→ 0 Treffer außer Kommentaren- Schritt 6:
*.bak-<datum>löschen lassen. Commitfeat(backup): track borgmatic configs without secrets, PR inhomeserver, Log-Eintrag.
Task 2: Storage Box: Sub-Accounts, Snapshots, Zugänge¶
Dateien:
- Ändern: homeserver/backup/borgmatic/hetzner.yaml:
- repositories[0].path → Sub-Account nuc
- ssh_command: ssh -i /root/.ssh/storagebox-nuc -p23
- remote_path: borg-1.4
- relocated_repo_access_is_ok: true
- Ändern: aiserver-Config (Live-Datei /etc/borgmatic.d/aiserver.yaml, ins Git erst in Task 4c), Repo storagebox → Sub-Account aiserver, gleiche drei Optionen.
- Runbook: Abschnitt „Task 2“.
Schnittstellen:
- Erzeugt: die Sub-Accounts und Keys aus den globalen Vorgaben, den Snapshot-Plan, das neue Hauptaccount-Passwort und HETZNER_API_TOKEN_RO. Später nutzen sie:
- der Box-Zugang vps (Task 5)
- der Box-Zugang restore (Task 8)
- das Lese-Token (Task 7)
- Erzeugt: Admin-Key ~/.ssh/storagebox-admin nur auf Robins Laptop, hinterlegt im Hauptaccount. Restore-Key ~/.ssh/storagebox-restore, privat in Proton Pass.
- Schritt 1: Runbook schreiben. Reihenfolge: manueller Snapshot
vor-e1-subaccounts→ Snapshot-Plan → Sub-Accounts anlegen → Keys erzeugen und eintragen →mvder Repos → Configs umstellen → alte Keys aus dem Hauptaccount entfernen → Hauptpasswort neu → Lese-Token. Befehle alshcloud storage-box …laut Recherche Abschnitt 3, alternativ der jeweilige Klickpfad in der Console. Für denmv:ssh -p23 -i ~/.ssh/storagebox-admin <main>@<host> "mv backups/nuc backups/nuc-repo; mkdir backups/nuc; mv backups/nuc-repo backups/nuc/repo", füraiservergenauso. - Schritt 2: Key-Format klären, bevor Robin anfängt: Wo liegt das
authorized_keyseines Sub-Accounts, und wie trägt man es ein (install-ssh-keyüber Port 23 gegen den Sub-Account, ja oder nein)? Quelle: Hetzner-Doku per Exa. Das Ergebnis kommt ins Runbook. - Schritt 3: Robin führt aus bis einschließlich „Configs umstellen“. Den aiserver stellt Robin per sudo um. Danach
borgmatic config validateauf beiden Hosts. - Schritt 4: borg 1.4.5 auf dem NUC. Standalone-Binary aus dem GitHub-Release
borg-linux-glibc231-x86_64oder dem passenden glibc-Build, SHA256 gegen die Release-Datei prüfen, nach/usr/local/bin/borg. Erwartet:borg --version→borg 1.4.5. Der aiserver behält die apt-Version (Spec 4: Update, wo das Paket es hergibt). - Schritt 5: Erster Lauf von Hand, außerhalb des Fensters:
borgmatic --config /etc/borgmatic.d/hetzner.yaml create --statsauf dem NUC. Erwartet: ein Archiv, keine Rückfrage zum Ortswechsel,Remote:-Zeilen ohne Fehler. - Schritt 6: Alte Keys entfernen und Hauptpasswort neu setzen (Robin). Erwartet: Im Hauptaccount steht nur noch
storagebox-admin. - Schritt 7: Lese-Token in der Hetzner Console, Berechtigung „Read“, nach
/etc/borgmatic.d/credentials/hetzner-api-ro.tokenauf dem NUC (root 0600). Probe:curl -sf -H "Authorization: Bearer $(cat …)" https://api.hetzner.com/v1/storage_boxes | jq '.storage_boxes | length'→ 1. - Schritt 8: Nachtlauf prüfen (Review-Fokus 5). Am nächsten Morgen beide Hosts
Successfully, keinrelocated-Prompt, keinLock-Fehler. Der Box-Snapshot03:30 UTCexistiert:hcloud storage-box snapshot list(Robin) oder API mit dem Lese-Token. - Schritt 9: Abgrenzung testen (Review-Fokus 2):
- vom aiserver:
ssh -p23 -i /root/.ssh/storagebox-aiserver <sub-nuc>@<host> ls→ abgelehnt - vom Laptop mit
restore:touch probe→ abgelehnt - vom NUC mit dem alten Key auf den Hauptaccount → abgelehnt
Ergebnisse ins Log.
- [ ] Schritt 10: Commit feat(backup): move nuc offsite repo to storage box sub-account, PR, Log.
Task 3: rclone-Mount entfernen¶
Dateien:
- NUC: /etc/systemd/system/mnt-storagebox.mount (deaktivieren, löschen), /etc/systemd/system/docker.service.d/10-wait-for-mounts.conf (nur /mnt/storagebox aus RequiresMountsFor streichen, sonst nichts)
- Ändern: homeserver/infrastructure/… bzw. die Doku-Stelle, an der die Mount-Unit beschrieben ist (per git grep -n storagebox finden)
- Runbook: Abschnitt „Task 3“
Schnittstellen:
- Erzeugt: Auf dem NUC gibt es keinen Schreibweg mehr für robin auf die Box. Den Füllstand liefert ab Task 7 das Lese-Token.
- Schritt 1: Nachsehen, wer den Mount noch nutzt (lesend):
fuser -vm /mnt/storagebox,git grep -n "/mnt/storagebox"inhomeserverundmonitoring-stack. Erwartet: nur der Füllstand. Taucht ein anderer Nutzer auf, anhalten und melden. - Schritt 2: Runbook schreiben:
systemctl disable --now mnt-storagebox.mount, Drop-in anpassen,systemctl daemon-reload,systemctl show docker -p RequiresMountsForohne/mnt/storagebox,rclone.conf-Eintragstorage-boxlöschen. Kein Docker-Neustart nötig. - Schritt 3: Robin führt aus. Nachweis:
findmnt /mnt/storageboxleersystemctl show docker -p RequiresMountsForohne den Pfaddocker psunverändert- Schritt 4: Die generische Regel
DiskSpaceLowbraucht keine Änderung, denn ihr fehlt danach nur die Zeitreihe. Den Satz zur Füllstand-Quelle inmonitoring-stack/docs-alerting.mdanpassen. Commit, PR, Log.
Task 4: UNAS als lokales Ziel¶
Drei Teile, jeder mit eigenem Runbook-Abschnitt und eigener Abnahme: 4a Freigaben, 4b NUC, 4c aiserver.
Dateien:
- Ändern: homeserver/backup/borgmatic/local.yaml:
- Repo → /mnt/backup-unas/repo, Label unas
- Passphrase-Datei unas.pass
- Quellen ohne /mnt/pictures, /mnt/paperless, /mnt/scanner, /mnt/archiv
- Mount-Guard um /mnt/backup-unas erweitert
- Ändern: homeserver/backup/borgmatic/hetzner.yaml: Quelle /mnt/archiv dazu, Mount-Guard um /mnt/archiv erweitert.
- Erstellen: homeserver/infrastructure/mounts/mnt-backup\x2dunas.mount und mnt-archiv.mount, nach dem Muster der vorhandenen NFS-Units. Ist infrastructure/ nicht der Ort der bestehenden Units, den tatsächlichen Ort per git grep -n "mnt-pictures" nehmen.
- Erstellen: aiserver/backup/aiserver.yaml (Live-Config ins Git, Muster Task 1) und aiserver/backup/README.md. Ändern: aiserver/docs/borgmatic.md, die veralteten Aussagen aus #216 korrigieren.
- Runbook: Abschnitte „Task 4a/4b/4c“.
Schnittstellen:
- Konsumiert: den Deploy-Weg aus Task 1, die Sub-Accounts aus Task 2.
- Erzeugt: lokale Repos nuc und aiserver auf der UNAS, Infisical NUC_BORG_PASSPHRASE_UNAS, AISERVER_BORG_PASSPHRASE_UNAS, AISERVER_BORG_PASSPHRASE_BOX und die zugehörigen Paper-Keys, Freigabe archiv mit den Archivordnern.
4a: Freigaben, Exporte, Snapshots (Robin in UniFi Drive)
- Schritt 1: Prüfpunkte aus Spec 17 klären (Claude mit Robin, Robin klickt): Drive- und OS-Version (Scrubbing ab 4.2.2), ob ein NFS-Export je Freigabe ausreicht, ob es eine Squash-Option gibt, ob eine Rolle „View Only“ existiert und was sie darf (Export der Config, Snapshot-Liste). Ergebnisse ins Log. Sie entscheiden, ob Task 6, Schritt 6 automatisch oder von Hand läuft.
- Schritt 2: Runbook 4a schreiben und Robin führt aus:
- Freigaben
backup-nuc,backup-aiserver,archivmit NFS-Exporten laut globalen Vorgaben - Snapshot-Pläne auf allen fünf Freigaben aus der Tabelle
- Scrubbing planen
Erwartet: showmount -e 10.10.10.150 vom NUC zeigt die neuen Exporte mit der richtigen IP.
4b: NUC lokal umziehen, Archivordner
- Schritt 3: Archivordner sichten (Robin). Robin entscheidet je Ordner: behalten oder weg. Danach
rsync -aHAX --info=progress2von/mnt/unas/<ordner>nach/mnt/archiv/<ordner>. Gegenprobe: Anzahl und Bytes perfind -printf %sgleich. Erst danach löscht Robin die Quelle. - Schritt 4: Mount-Units und neues Repo (Runbook 4b, Robin):
- Units installieren und starten
- Passphrase erzeugen → Infisical und Proton Pass →
unas.pass borgmatic --config /etc/borgmatic.d/local.yaml repo-create --encryption repokey-blake2- Key-Export
--paper→ Infisical, danachshred - die neue
local.yamlinstallieren
Das alte /mnt/borg-repo bleibt gemountet und unberührt (TR-004-Rückfall, Spec 7).
- [ ] Schritt 5: Erster Lauf von Hand. Erwartet: ein Archiv auf /mnt/backup-unas/repo mit ~100 GB original_size. Darin borgmatic list --archive latest | grep -c "mnt/pictures" → 0.
- [ ] Schritt 6: Box-Lauf mit Archiv. Am nächsten Morgen enthält das Box-Repo mnt/archiv, und deduplicated_size des Archivs liegt bei ~95 GB. Der Box-Füllstand steht im Log.
- [ ] Schritt 7: Mount-weg-Test (Review-Fokus 1, Abnahme Spec 14):
- /mnt/backup-unas aushängen, local.yaml per create von Hand starten → FEHLER … nicht gemountet, Exit ≠ 0, ls /mnt/backup-unas leer, kein neues Repo
- dasselbe mit /mnt/archiv gegen hetzner.yaml
- danach wieder einhängen
4c: aiserver
- Schritt 8: Live-Config lesen (Robin per sudo, redigiert wie in Task 1) und ins Repo
aiserver/backup/aiserver.yamlübernehmen. - Schritt 9: Repo kopieren (Runbook 4c):
- auf dem NUC
rsync -aH /mnt/borg-repo-aiserver/ /mnt/backup-unas-aiserver-tmp/über einen temporären Mount der Freigabebackup-aiserver(oder direkt vom aiserver aus, Robins Wahl) - auf dem aiserver die Mount-Unit
/mnt/backup-unaseinrichten - Repo-Pfad →
/mnt/backup-unas/repo relocated_repo_access_is_ok: true
Der Weg über ssh://robin@10.10.10.3 und das authorized_keys-command= auf dem NUC fallen weg.
- [ ] Schritt 10: Passphrasen rotieren nach Runbook 2 (homeserver#216), für beide aiserver-Repos. Ablage in Infisical und Proton Pass, Key-Export, Gegenprobe der alten Passphrase → is incorrect.
- [ ] Schritt 11: Nachweis: Nachtlauf Successfully für beide aiserver-Repos. Auf dem NUC authorized_keys von robin ohne borg serve-Zeile. Commits in homeserver und aiserver, PRs, Log.
Task 5: VPS¶
Dateien:
- Erstellen: homelab-external/hetzner/backup/vps.yaml (borgmatic-Config) und homelab-external/hetzner/backup/README.md
- Erstellen: homelab-external/hetzner/backup/borgmatic.service.d/10-schedule.conf bzw. eine Timer-Datei, falls pipx keine Units mitbringt: Timer täglich 02:30 Ortszeit, Persistent=true
- Ändern: homelab-external/CLAUDE.md und README.md, Abschnitt Backup
- Runbook: Abschnitt „Task 5“
Schnittstellen:
- Konsumiert: den Sub-Account vps aus Task 2.
- Erzeugt: das Repo vps auf der Box, Infisical VPS_BORG_PASSPHRASE_BOX, den Healthchecks-Check vps-box.
Config-Werte:
- Quellen /etc, /opt/homelab-repo, /opt/homelab-data (Exclude: Docker-Interna, Logs, Caches wie NUC)
- Postgres von identity-postgres über den nativen borgmatic-Hook postgresql_databases (Spec 13 schließt native Hooks nur für den NUC aus. Hier gibt es eine einzige DB, und ein Fehler soll den Lauf rot machen.)
- Retention vorerst 7/4/6. Die endgültige Aufbewahrung legt Robin in Task 6, Schritt 1 fest.
- Checks repository und archives monatlich, spot wöchentlich, data monatlich (das Repo ist klein)
- ntfy und Healthchecks wie NUC
- Schritt 1: Datenliste prüfen (lesend, per SSH mit
~/.ssh/homelab-external):du -sh /opt/homelab-data/*und welche Dienste eine DB haben. Sind SQLite-Dateien darunter (Healthchecks, Kuma, Headscale)? Dann kommen sie persqlite_databases-Hook in die Config statt als Dateikopie. - Schritt 2: Hetzner Cloud Backup einschalten (Robin, Console). Erwartet: Backups aktiv, Preis +20 %, das erste Backup innerhalb von 24 h sichtbar.
- Schritt 3: Runbook schreiben, Robin führt aus:
- borg 1.4.5 als Binary wie auf dem NUC
- borgmatic 2.1.5 per pipx
- Key
storagebox-vpsin den Sub-Account - Passphrase → Infisical und Proton Pass
repo-create --encryption repokey-blake2, Key-Export- Timer
- Schritt 4: Erster Lauf von Hand und der Nachtlauf →
Successfully, Checkvps-boxup. Mitborgmatic list --archive latest | grep -c identity> 0 ist belegt, dass der Dump drin ist. - Schritt 5: Commit
feat(backup): add borgmatic backup to storage box, PR inhomelab-external, Log.
Task 6: Aufbewahrung, Dumps, Excludes, Appliances¶
Dateien:
- Ändern: homeserver/backup/predeploy-backup.sh: zählt gescheiterte Dumps. Exit 0, wenn alle OK sind. Exit 2, wenn mindestens einer gescheitert ist, mit der Zeile FAILED: <name> je Dump auf stdout. Gilt auch für die manuelle Nutzung vor Deployments (docs-datenbanken.md anpassen).
- Erstellen: homeserver/backup/db-dumps.sh. Ruft predeploy-backup.sh --dest /var/lib/db-dumps auf und pingt den Healthchecks-Check nuc-db-dumps: bei 0 → Erfolg, bei 2 → /fail mit den FAILED:-Zeilen als Body. Eigener Exit ist immer 0, damit das Backup weiterläuft.
- Erstellen: homeserver/backup/borgmatic/systemd/borgmatic.service.d/10-db-dumps.conf mit ExecStartPre=-/usr/local/sbin/db-dumps.sh
- Ändern: beide NUC-Configs. Im before-Hook entfällt der Aufruf von predeploy-backup.sh, die Excludes werden überarbeitet.
- Ändern: homeserver/backup/docs-datenbanken.md, docs.md
- PR #71 wird geschlossen, mit Verweis auf Spec 13.
Schnittstellen:
- Konsumiert: den Deploy-Weg aus Task 1.
- Erzeugt: den Check nuc-db-dumps. Den Hash des Dump-Skripts predeploy-backup.sh (sha256 … | cut -c1-8) hält das Log fest, als Nachweis gegen Drift.
- Schritt 1: Aufbewahrung mit Robin festlegen (Spec 9). Vorlage: heute 7/4/12/2, Kapazitätsrechnung aus Spec 8, Box 10 Snapshots, UNAS 14. Robins Entscheidung kommt ins Log und in alle Configs (NUC lokal, NUC Box, aiserver, VPS).
- Schritt 2: Test für das Dump-Skript schreiben:
homeserver/backup/tests/test_predeploy_exit.sh, ein Bash-Test mit gestubbtemdockerimPATH: - Fall a: alle Dumps OK → Exit 0
- Fall b: der Stub lässt
mysqldumpfürteamspeak_dbscheitern → Exit 2, stdout enthältFAILED: teamspeak_db - Schritt 3: Test laufen lassen. Erwartet: Fall b scheitert vor der Änderung, weil das Skript heute 0 liefert.
- Schritt 4: Exit-Code und
db-dumps.shumsetzen, Test grün (Review-Fokus 4). Dazushellcheckundshfmt -i 2 -dmit den in prek gepinnten Versionen. - Schritt 5: Excludes überarbeiten:
container-data/ollama/außeropenwebui/ausschließen, vor dem 31.12.2026 (Spec 8)**/backup/*und**/backups/*gezielt ersetzen:/etc/lvm/backupund/home/robin/backuprein. Immichs eigene Dumps bleiben draußen, weil der Dump über das Skript läuft.- HA: nur das jüngste Backup unter
homeassistant/backupsbehalten. Wie, entscheidet der Umsetzer, Rezept „mittel“ muss damit funktionieren.
Gegenprobe per create --dry-run --list wie in Runbook 1, Schritt 3.
- [ ] Schritt 6: Appliances.
- Zigbee: Liegt coordinator_backup.json im Z2M-Datenverzeichnis und damit im Archiv? Nachweis per borgmatic list --archive latest | grep coordinator_backup.
- UDM und UNAS: Erlaubt „View Only“ den Export (Task 4, Schritt 1), dann ein Export-Skript nach /mnt/backup-unas/exports/. Sonst ein Runbook für den manuellen Export vierteljährlich. Der Morgenstatus zeigt das Alter der jüngsten Datei unter exports/ (Task 7).
- [ ] Schritt 7: Robin deployt (Skripte nach /usr/local/sbin/, Drop-in, Configs). Nachweis:
- im Nachtlauf genau ein Dump-Durchlauf (grep -c "MariaDB: teamspeak_db" → 1)
- Check nuc-db-dumps up
- [ ] Schritt 8: Dump-Fehler provozieren (Abnahme). teamspeak_db kurz stoppen, db-dumps.sh von Hand laufen lassen → Check rot, ntfy kommt, borgmatic läuft danach trotzdem. Container wieder starten.
- [ ] Schritt 9: Commits, PR, #71 schließen, Log.
Task 7: Alarmierung und Morgenstatus¶
Dateien:
- Erstellen: homeserver/backup/status/backup_status.py. Nur Standardbibliothek: urllib, json, os, dataclasses, datetime.
- Erstellen: homeserver/backup/status/tests/test_backup_status.py (pytest)
- Erstellen: homeserver/backup/status/backup-status.service und backup-status.timer (OnCalendar=*-*-* 07:00:00, Persistent=true)
- Erstellen: homeserver/backup/borgmatic/systemd/borgmatic-verify-box.service und .timer: vierteljährlich am ersten Samstag 09:00, borgmatic --config /etc/borgmatic.d/hetzner.yaml check --only data --force, eigener Check nuc-verify-box
- Ändern: die Configs aller Hosts, checks laut Tabelle unten
- Runbook: Abschnitt „Task 7“
Schnittstellen:
- Konsumiert:
- Healthchecks-API v3 GET /api/v3/checks/, Header X-Api-Key, Lese-Key, Datei /etc/borgmatic.d/credentials/healthchecks-api-ro.key
- Hetzner-API GET /v1/storage_boxes/{id} und /v1/storage_boxes/{id}/snapshots mit dem Lese-Token aus Task 2
- os.statvfs auf /mnt/backup-unas und /mnt/pictures
- Datei-mtime unter /mnt/backup-unas/exports/
- Erzeugt:
- @dataclass CheckStatus(name: str, status: str, last_ping: datetime | None)
- @dataclass BoxStatus(used_bytes: int, total_bytes: int, newest_snapshot: datetime | None)
- @dataclass FsStatus(path: str, free_ratio: float)
- @dataclass Finding(severity: str, text: str), severity ∈ {"red", "green"}
- fetch_checks(base_url: str, api_key: str) -> list[CheckStatus]
- fetch_box(token: str) -> BoxStatus
- read_fs(paths: list[str]) -> list[FsStatus]
- evaluate(checks: list[CheckStatus] | None, box: BoxStatus | None, fs: list[FsStatus], export_age_days: float | None, now: datetime) -> list[Finding]
- render(findings: list[Finding]) -> dict, das ntfy-JSON mit topic, title, message, priority, tags
- main() -> int. Fängt Netzfehler je Quelle ab und gibt None an evaluate weiter.
- Nur wenn Task 4, Schritt 1 einen Leseweg zum Snapshot-Alter der UNAS gefunden hat: evaluate bekommt zusätzlich unas_snapshot: datetime | None, mit derselben 26-h-Regel und dem Test test_unas_snapshot_stale. Sonst entfällt der Parameter, und der Morgenstatus nennt die UNAS-Snapshots nicht.
checks je Config:
| Config | repository |
archives |
spot |
data |
|---|---|---|---|---|
| NUC lokal | 2 Wochen | 1 Monat | 1 Woche | 1 Monat |
| NUC Box | 2 Wochen | 1 Monat | 1 Woche | nur Timer, vierteljährlich |
| aiserver beide | 2 Wochen | 1 Monat | 1 Woche | 1 Monat |
| VPS | 1 Monat | 1 Monat | 1 Woche | 1 Monat |
Die spot-Prüfung von borgmatic setzt den „Probe-Restore jede Woche“ aus Spec 11 um. Sie vergleicht eine Stichprobe der Quelldateien mit dem jüngsten Archiv, eine eigene Testdatei ist damit unnötig. Die Toleranzen legt der Umsetzer nach dem ersten Lauf fest und schreibt sie ins Log. Ob --only data bei borgmatic 2.1.5 auch ohne data in checks greift, prüft der Umsetzer vorab im Wegwerf-Container.
- Schritt 1: Tests schreiben:
test_all_green→ genau ein Findinggreen, TitelBackup OKtest_check_down→ ein Checkdownerzeugtredmit seinem Namentest_box_80_percent→used/total = 0.80erzeugtred,0.79nichttest_unas_below_15_percent→free_ratio = 0.149erzeugtred,0.15nichttest_snapshot_stale→newest_snapshot = now − 26 h 1 minerzeugtred,now − 25 hnichttest_export_older_than_100_days→export_age_days = 101erzeugtredtest_render_red_priority→ bei mindestens einemred:priority == 4,topic == "homelab-critical", Titel ASCII-only. Sonstpriority == 2,topic == "homelab-alerts".- Schritt 2: Test für Review-Fokus 3:
test_source_unreachable,evaluate(None, None, [], None, now)liefertredmit „Healthchecks nicht erreichbar“ und „Hetzner-API nicht erreichbar“, undrenderliefert trotzdem eine Nachricht. - Schritt 3: Tests laufen lassen mit
uv run --with pytest==<aktuelle stabile Version, nachschlagen> pytest homeserver/backup/status -q. Erwartet: FAIL, Modul fehlt. - Schritt 4:
backup_status.pyumsetzen. Tests grün,uv run --with ruff ruff check homeserver/backup/statusohne Befund. Feldnamen der Hetzner-API laut Referenzdocs.hetzner.cloud. Eine redigierte Beispielantwort liegt als Fixture intests/fixtures/. - Schritt 5: Healthchecks-Checks anlegen (Robin oder Claude mit Robins OK in der Healthchecks-Oberfläche):
nuc-local,nuc-hetzner,nuc-db-dumps,nuc-verify-box,aiserver-unas,aiserver-box,vps-box. Zeitplan als Cron je Lauf mit 2 h Gnade,nuc-verify-boxmit 14 Tagen. Benachrichtigung an ntfyhomelab-critical. Lese-Key anlegen → InfisicalHEALTHCHECKS_API_KEY_RO→ Credential-Datei. - Schritt 6: Robin deployt Configs, Timer und Skript. Nachweis: Um 07:00 kommt der Morgenstatus grün, mit allen 7 Checks, Box-Füllstand und Snapshot-Alter.
- Schritt 7: Ausbleibenden Lauf provozieren (Abnahme).
systemctl stop borgmatic.timeram Abend, am Morgen ist der Checknuc-localbzw.nuc-hetznerrot, ntfy aus Healthchecks ist gekommen, der Morgenstatus ist rot. Danachstart. - Schritt 8: Commit
feat(backup): add morning backup status, PR, Log.
Task 8: Restore-Handbuch und Proben¶
Dateien:
- Erstellen: homelab-documentation/docs/runbooks/restore.md mit den drei Rezepten aus Spec 11, je mit Befehl, Erwartung und Zeitbedarf
- Ändern: mkdocs.yml (Runbooks)
Schnittstellen:
- Konsumiert: alle Repos und den Sub-Account restore aus Task 2.
- Schritt 1: Rezepte schreiben:
- Klein:
- Foto über einen UNAS-Snapshot (Download in UniFi Drive)
- Config über
borgmatic --config /etc/borgmatic.d/local.yaml mount --archive latest --mount-point /mnt/restore, Datei kopieren,umount
- Mittel, am Beispiel eines kleinen Dienstes: Ist
sonarr(SQLite) geeignet oder ein Dienst mit MariaDB? Robin wählt einen, der ein paar Minuten offline sein darf. -
Groß:
- auf dem Laptop
- Key
storagebox-restoreaus Proton Pass - Passphrase aus Proton Pass
BORG_PASSPHRASEnur in der Shellborg list --bypass-lock ssh://<restore>@<host>:23/./nuc/repo- Extract eines Verzeichnisses (
/etcund ein Paperless-Unterordner) nach~/restore-probe/ - Gegenprobe per Hash gegen das Original
Lehnt borg den Zugriff ohne Schreibrecht ab (Spec 8), weicht der Umsetzer aus. Im Log steht dann, welcher Weg genommen wurde. - [ ] Schritt 2: Probe Klein (Robin, Claude begleitet), Ergebnis und Dauer ins Log. - [ ] Schritt 3: Probe Mittel, Dienst danach funktionsfähig, Ergebnis ins Log. - [ ] Schritt 4: Probe Groß. Erwartet: Hashes gleich, Dauer und Durchsatz im Log. Der Durchsatz legt fest, ob der vierteljährliche
data-Check aus Task 7 machbar ist. Passt er nicht, wird der Timer angepasst. - [ ] Schritt 5: Ransomware-Probe (Abnahme Spec 14): - UNAS: Datei/mnt/backup-unas/repo/READMEumbenennen, Snapshot von gestern per UniFi-Download zurückholen,borgmatic repo-infoOK - Box: imvps-RepoREADMEper Sub-Accountvpslöschen, dann per Admin-Keycp /.zfs/snapshot/<gestern>/backups/vps/repo/README backups/vps/repo/README,borgmatic --config vps.yaml repo-infoOK - Gegenprobe: Der Sub-Accountvpskann unter/.zfs/snapshotnichts löschen - [ ] Schritt 6: Commit, PR, Log.
Task 9: Störungs-Handbuch, Notfallkonten, Umschlag¶
Dateien:
- Erstellen: homelab-documentation/docs/runbooks/stoerungs-handbuch.md. Inhalt nach Spec 12 und dem Entwurf vom 28.10. Dazu eine Druckfassung als PDF, Ablage im Repo unter docs/runbooks/assets/ ohne Telefonnummern (die trägt Robin von Hand auf dem Papier ein).
- Ändern: mkdocs.yml
- Schritt 1: Fakten prüfen (Robin mit Fotos, Claude lesend):
- Knopf und Lämpchen je Gerät (USV, UDM, UNAS, NUC, aiserver)
- HA-Adresse
http://10.10.10.3:8123 - was das UDM-Display anzeigt
- welche Anschluss-Box (Modem/ONT)
- Bedienung der Thermostate am Gerät
- Zigbee: Stick am NUC oder Netzwerk-Coordinator
Ergebnisse ins Handbuch.
- [ ] Schritt 2: Handbuch schreiben. Abschnitte 1–7 aus Spec 12, Satzbau einfach, mit Fotos und Pfeilen.
- [ ] Schritt 3: Notfallkonten (Robin):
- lokaler HA-Benutzer für Lars ohne Admin
- Zugang zur UniFi-App für Lars nur mit Leserecht, falls es das gibt
- Lars legt beides in seinem Bitwarden ab, Ordner „Zuhause“
- [ ] Schritt 4: Umschlag (Robin): Passphrasen und Paper-Keys aller Repos und das restore-Passwort, ausgedruckt vom Laptop, nicht über einen Netzwerkdrucker mit Speicher. Den Ort entscheidet Robin, er wird nicht ins Repo geschrieben.
- [ ] Schritt 5: Durchgang mit Lars (Robin und Lars): Das Blatt liegt neben dem Rack, das PDF auf Lars' iPhone. Einmal gemeinsam durchgehen, Fragen von Lars kommen ins Handbuch.
- [ ] Schritt 6: Commit, PR, Log.
Task 10: Abschluss¶
- Schritt 1: Abnahme-Liste aus Spec 14 Punkt für Punkt mit Beleg ins Log.
- Schritt 2: Doku nachziehen:
- Backup-Seiten in
homelab-documentation(docs/projects/homeserver/…/backup) - Skill
homelab-infra: Abschnitt Backup und Storage-Layout, USB-RAID nur noch als Rückfall - D1-Design Abschnitt 6: 931 GiB → 1 TiB
- Schritt 3: TR-004-Termin: Abbau = Datum des ersten Archivs im neuen lokalen NUC-Repo + 3 Monate. Als Issue auf dem Board mit diesem Fälligkeitsdatum.
- Schritt 4: Issues schließen. T1–T7, #216 und #179 je mit Abschlusskommentar auf Deutsch: was gebaut wurde, Abweichungen, Zurückgestelltes, Link auf Design und Log. Englische
Closes #Nim PR, sonstgh issue close. - Schritt 5: Einträge im Roadmap-Log, die Memory
homelab-roadmap-2026-08auf E1-Stand, Roadmap-Artifact aktualisieren.