Zum Inhalt

Umsetzungs-Log

Laufendes Protokoll der Roadmap-Umsetzung — was in welcher Session tatsächlich passiert ist, inklusive der Funde, die vorher niemand auf dem Zettel hatte. Neueste Einträge oben.

Ergänzt die Roadmap, die das Vorhaben beschreibt: hier steht der Verlauf. Das Log läuft bis zum Abschluss aller Epics (Zielmarke 31.12.2026).

Nachträglich rekonstruiert

Die Einträge vor dem 06.09.2026 sind aus der Session-Memory nachgetragen, das Log entstand erst danach. Sie sind knapper als die laufenden Einträge.


2026-09-13 — E0a: Alarmkette ausgewertet, ein Fehlalarm gefunden, 321 GB frei geworden

Issues: homelab-external#23 ausgewertet (bleibt offen) · homeserver#213 und homelab-external#31 und aiserver#9 neu · homeserver#210 und homeserver#194 aktualisiert PR: homeserver#214

Das 7-Tage-Fenster: bestanden, aber nicht ganz

Das Fenster aus #23 endete am 12.09. um 20:07. Ausgewertet wurde es aus drei Quellen: Prometheus-Alarmhistorie, ntfy-Nachrichtenzähler plus Traefik-Access-Log auf dem VPS, und die Journale von NUC und aiserver.

Kriterium 1 trägt. 15 der Meldungen liefen durch Traefik, und 15 von 15 wurden 1–3 Sekunden später mit poll=1&id=<genau diese Nachrichten-ID> abgeholt, jeweils Status 200. Dazu poll=1&since=-Abrufe auf beiden Topics. Das am 05.09. vorgeschlagene Ersatzkriterium für das unerreichbare subscribers >= 1 ist damit bewiesen.

Kriterium 4 nicht ganz. Im Fenster gingen 17 Meldungen raus, nicht 15 — zwei liefen VPS-intern an Traefik vorbei und fielen erst durch den Nachrichtenzähler auf. Es waren Kuma-Erholungsmeldungen der beiden Backup-Monitore, also die bereits behobene Fehlalarm-Klasse aus homelab-external#20, in den ersten 6 Stunden des Fensters. Danach: 6 Tage 18 Stunden ohne einen einzigen Kuma-Zustandswechsel.

Die übrigen 15 sind belegt echt — darunter ein 45-Sekunden-Stromausfall am 09.09. (upsmon 11:04:53 „on battery" → 11:05:38 „on line power", onbatt-shutdown-Timer korrekt abgebrochen). Der NUC hielt durch, der aiserver fiel aus.

Der schönste Einzelbefund ist ein negativer: UpsOnBattery und AiserverBackupMetricMissing blieben beide pending und wurden korrekt nie zugestellt. Die Kette stellt also nicht nur zu, sie hält kurze Ereignisse auch zurück. Das ist der Gegenbeweis zu „meldet einfach alles".

Die Lehre über die Auswertung selbst

Prometheus läuft mit --storage.tsdb.retention.time=7d. Die ersten 16 Stunden des Fensters waren am 13.09. schon gelöscht (ältestes Sample: 06.09. 12:05). Ein 7-Tage-Fenster ist einen Tag zu spät aus Prometheus nicht mehr belegbar.

Gerettet hat die Auswertung der ntfy-Nachrichtenzähler auf dem VPS: lückenlos, und er zählt auch die Meldungen, die nicht durch Traefik laufen. Ohne ihn wären die zwei Kuma-Meldungen unentdeckt geblieben und das Fazit wäre falsch positiv ausgefallen.

Der Fehlalarm von heute

19 Stunden nach Fensterende war die UNAS 4 Sekunden nicht erreichbar (nfs: server 10.10.10.150 not responding 15:10:17 → OK 15:10:21). Ergebnis: drei Critical-Alarme für /mnt/unas, /mnt/paperless und /mnt/monitoring, während alle Mounts durchgehend gesund und rw gemountet waren.

Ursache: bei einem statfs-Timeout wirft node-exporter die Filesystem-Serien des Mounts komplett weg. absent(...) mit for: 2m kann deshalb „NAS war kurz beschäftigt" nicht von „Mount ist weg" unterscheiden — die Serien fehlten rund 3 Minuten. Solche Aussetzer gab es 10× in 8 Tagen.

Fix in homeserver#214: absent_over_time(...[10m]), womit das Fenster selbst die Entprellung ist und die Regeln kein for mehr brauchen. Gegen den echten Vorfall verifiziert — der alte Ausdruck feuert um 15:08, 15:09 und 15:10, der neue bleibt an jedem dieser Zeitpunkte leer. Nebenbei fiel auf, dass /mnt/pictures gar keine Regel hatte, also der Fotobestand, der mit E5 zum Primärsystem wird.

Wäre der Aussetzer einen Tag früher gekommen, hätte er das Fenster gerissen. Deshalb wird #213 vor dem Schließen von #23 ausgerollt.

Storage Box: 89 % → 57 %, ohne dass jemand etwas getan hat

2026-09-12T04:13:00  borgmatic: Remote: compaction freed about 346.03 GB repository space.
2026-09-12T05:05:40  borgmatic: compaction freed about 47.08 GB repository space.

Normal sind rund 0,6 GB pro Nacht. Es war keine Konfigurationsänderung (hetzner.yaml hat mtime 23. Mai) und kein borg-Update (apt-Historie 09.–13.09.: nur python3.10, locales, libc6, perl).

Der Umweg, den diese Session genommen hat, gehört zum Verlauf: Zuerst wurde vermutet, am 12.09. habe ein Massen-Prune stattgefunden, und daraus geschlossen, die 07.09.-Bewertung („Retention kürzen ist wirkungslos", gemessen am Exklusiv-Anteil des ältesten Einzelarchivs) sei widerlegt. Die Archivliste hat das kassiert:

Gruppe Archive
Monatlich 01-04, 01-31, 02-28, 03-31, 04-30, 05-31, 06-30, 07-31, 08-31
Wöchentlich 08-16, 08-23, 08-30
Täglich 09-06 … 09-13

20 Archive, lehrbuchmäßiges Muster, und das älteste ist weiterhin nuc-2026-01-04 — am 07.09. waren es 21 Archive ab dem 04.01., heute 20 ab dem 04.01. Das Aufbewahrungsfenster hat sich nicht verschoben, am 12.09. fiel nur das übliche Tagesarchiv heraus. Die 07.09.-Bewertung war richtig und bleibt stehen.

Tatsächlich waren die 346 GB physisch toter Speicher, den frühere Prunes längst logisch freigegeben hatten und den borg compact nie abgetragen hat — borg schreibt ein Segment erst um, wenn sein Totanteil über eine Schwelle steigt (Standard 10 %). Das ist ein anderer Befund: die Storage Box war zu rund 38 % mit Müll gefüllt. Der Füllstand von 89 % war nie ein Kapazitäts- und nie ein Wachstumsproblem, sondern Hauswirtschaft. Kein Aufstocken und keine Retention-Entscheidung hätte daran etwas geändert.

Das erklärt weiterhin, warum das lokale Repo 43 % kleiner war: den Altbestand gab es lokal nicht in dem Maß (346 GB gegen 47 GB). Heute hält /dev/md0 600 G für beide lokalen Repos zusammen, die Storage Box 592 G für den NUC allein.

Offen bleibt, warum die nächtliche Compaction den Rückstand über Monate nicht abtrug. Die Zeiten passen zu einer lange aufgeschobenen Arbeit, die endlich durchlief (Prune 12 min statt 20–50 s, Compaction 1 h 43 min statt Sekunden). Unbelegte Hypothese: die Storage Box hängt über rclone-fuse, und eine dort abbrechende Compaction lässt Arbeit liegen, die erneut ansteht.

Woran man den Unterschied künftig erkennt: steigt der Füllstand wieder Richtung 89 %, während das Archivfenster gleich lang bleibt, ist es wieder Compaction-Rückstand. Wächst dagegen die Datenmenge bei konstantem Fenster, ist es echtes Wachstum.

Unverändert offen und von freiem Platz nicht berührt: die UNAS-Fotos sind in beiden Configs auskommentiert, also nirgends gesichert. Der Platz kauft Zeit, nicht Abdeckung.

#194: Bestandsaufnahme statt Vermutung

Der Stand vom 07.09. prüfte sieben namentlich bekannte Variablen. Diesmal ein flächiger Scan über alle getrackten Dateien, und für jeden Treffer per SHA-256-Vergleich gegen .Config.Env des laufenden Containers die Frage: lebt der Wert noch?

Fünf Secrets sind aktiv und stehen in getrackten DateienCF_DNS_API_TOKEN (traefik/.env, zonenweite DNS-Kontrolle), SONARR__APIKEY und RADARR__APIKEY (media-stack), TS3SERVER_DB_PASSWORD (ts3), PAPERLESS_SECRET_KEY (paperless-ngx/paperless.env, ebenfalls getrackt).

Neu: zwei tote Stacks tragen Secrets in Git — proxy-manager (3 MySQL-Passwörter) und pzserver (SRCDS_TOKEN). Für beide existiert auf dem NUC kein Container, auch kein gestoppter. Aufräumarbeit für E0b; der Steam-Token muss bei Valve entwertet werden.

Entschärft: die Traefik-Dashboard-Credentials sind tatsächlich admin/admin — die Datei sagt es selbst (# Generiert mit: htpasswd -nb admin admin). Aber von außen nicht erreichbar: traefik.home.robinwerner.net löst öffentlich nicht auf, TCP 443 zur Heim-IP ist zu, und ein Host-Header-Versuch vom VPS kommt nicht durch. Ein LAN-Risiko, kein Internet-Risiko — der Stand nach #195 hält.

Panne des Tages

Bei dieser Bestandsaufnahme sind das TS3-DB-Passwort und zwei der vier Monitoring-Tokens erneut in einen Chat-Verlauf geraten. Die Redigierung zielte auf =-Syntax, die Compose-Datei nutzt YAML, und die borgmatic-Journalzeilen enthielten die vollen Ping-URLs.

Für die Monitoring-Tokens ist es das zweite Mal nach dem 07.09. Die Lehre: solange diese Werte im Klartext in Dateien und Journalen stehen, tauchen sie bei jeder Diagnosearbeit wieder auf. Das ist kein Argument für vorsichtigeres Hinsehen, sondern für Rotation und Credential- Dateien.

Erster Rotationsschritt vorbereitet

Weil das TS3-DB-Passwort durch die Panne oben erneut exponiert war, wurde es vorgezogen: PR homeserver#215 holt es aus der getrackten Compose-Datei nach ts3/.env, mit ${TS3_DB_PASSWORD:?…} — fehlt die Variable, bricht docker compose ab statt mit leerem Passwort zu starten (in beide Richtungen geprüft).

Die eigentliche Rotation steht im PR als Runbook, mit zwei Fußangeln, die man sonst erst merkt, wenn TS3 nicht mehr verbindet: MARIADB_PASSWORD wirkt nur bei der Erstinitialisierung, die Datenbank existiert längst — das Passwort muss per SQL geändert werden. Und MARIADB_RANDOM_ROOT_PASSWORD: yes heißt, das root-Passwort ist mit dem ersten Recreate verloren, weshalb der Benutzer teamspeak sein Passwort selbst ändert.

TS3-Rotation durchgeführt — mit zwei MariaDB-Fallen

Die Rotation lief direkt im Anschluss und ist belegt: teamspeak neu erzeugt, Listener auf 9987/30033/10011, keine DB-Fehlerzeile, teamspeak_db unberührt (die Datenbank lief durchgehend), und das alte Passwort aus der History wird mit ERROR 1045 abgelehnt.

Zwei Fallen unterwegs, beide vorher nicht bedacht:

ALTER USER reicht nicht für das eigene Konto. MariaDB verlangt dafür CREATE USER; der Benutzer hat laut SHOW GRANTS nur USAGE ON *.* plus ALL PRIVILEGES ON teamspeak.*. Der richtige Weg ist SET PASSWORD ohne FOR-Klausel — laut Doku „which any non-anonymous user can perform".

Ein nackter String wird bei SET PASSWORD als fertiger Hash gespeichert, nicht gehasht. SET PASSWORD = '<klartext>' hätte das Konto unbrauchbar gemacht — und weil das root-Passwort durch MARIADB_RANDOM_ROOT_PASSWORD: yes mit dem ersten Recreate verloren ist, wäre das nicht mehr zu reparieren gewesen. Richtig ist SET PASSWORD = PASSWORD('<klartext>'). Nützlicher Nebeneffekt derselben Syntax: der alte Hash aus SHOW GRANTS lässt sich unverändert zurückschreiben — ein Rollback, der das alte Klartextpasswort nicht braucht.

Beim Recreate wurde bewusst nur teamspeak angefasst: MARIADB_PASSWORD wirkt ohnehin nur bei der Erstinitialisierung, und der Stack hat kein depends_on, weshalb ein Recreate beider Dienste TS3 vor die Datenbank hätte starten können.

Rollout — und ein selbst gebauter Fehlschlag

homeserver#214 und #218 gingen zusammen raus. Der erste Reload scheiterte mit HTTP 500:

err="/etc/prometheus/rules/node-alerts.yml: open …: permission denied"
err="error loading rules, previous rule set restored"

Ursache war eine umask 077, die aus dem TS3-Schritt noch in derselben Root-Shell galt — git pull schrieb die geänderten Dateien mit Modus 0600 neu, für den als nobody laufenden Container unlesbar. Betroffen waren genau die zwei angefassten Dateien, die übrigen Regeldateien blieben auf 0644. Das ist eine Nebenwirkung des Verzeichnis-Mounts aus #199: der Container liest die Host-Dateien direkt, also entscheidet deren Modus mit. git überträgt außer dem Executable-Bit keine Rechte, ein Pull schreibt also immer mit der aktuellen umask.

Der unangenehme Teil: prometheus.yml mit 0600 heißt, ein Neustart von Prometheus wäre nicht mehr hochgekommen. Der laufende Prozess merkte nichts, weil er seine Config beim Start gelesen hatte. Lehre für Runbooks: eine restriktive umask gehört in eine Subshell ((umask 077; … > .env)), nie in die Shell, in der danach noch git läuft.

Und wieder war es still: prometheus_config_last_reload_successful stand auf 0, eine Regel darauf gibt es nicht. Ohne den manuellen Reload und den Blick ins Log hätte nichts gemeldet, dass Prometheus mit veralteten Regeln läuft.

Nach chmod 644 war der Endstand verifiziert: 4 × absent_over_time, keine alte absent()-Regel mehr, /mnt/pictures dabei, 0 Ziele down, Reload erfolgreich, keine aktiven Alarme.

Nebenfund: Überwachung, die nicht überwacht — Fall 3 und 4

Beim Lesen der Prometheus-Logs fielen zwei dauerhaft ausgefallene Scrape-Ziele auf, im gesamten Retention-Fenster nie up, obwohl beide Container laufen: qdrant:6333/metrics gibt 401 (Prometheus schickt keinen API-Key), open-webui:8080/metrics liefert <!doctype html> (Metrik-Endpunkt nie aktiviert). Letzteres scheiterte alle 15 Sekunden und machte den Log unlesbar.

Der eigentliche Fund ist aber: von 18 Scrape-Jobs hat genau einer eine Regel auf up (AiserverDown). Robins Entscheid dazu war klar — alles KI-bezogene wandert auf den aiserver (E4), also wurden beide Jobs in #218 entfernt statt repariert. Dabei kam heraus, dass die Scrape-Target-Tabelle in docs-metriken.md nur 10 von 18 Jobs listete, und genau in dieser Lücke lebten die toten Ziele; die Tabelle ist jetzt vollständig.

prometheus hängt nur wegen dieser zwei Jobs im Netz ollama_internal. Das ist jetzt toter Ballast, braucht aber einen Container-Recreate — als Kommentar in der Config festgehalten und E4 zugeordnet.

Damit stehen vier Fälle desselben Musters in homeserver#217:

Regel Was still nicht funktionierte
ContainerRestarting lief auf container_restart_count, das cAdvisor nie exportiert (#197)
NfsMountLost feuerte bei 4-Sekunden-Aussetzern statt bei verlorenen Mounts (#213)
fehlende TargetDown zwei Scrape-Ziele dauerhaft down, keine Meldung
fehlende Reload-Regel fehlgeschlagener Config-Reload, keine Meldung

Für E3 ist das weniger eine Liste einzelner Regeln als ein Prinzip: jede neue Regel einmal gegen /api/v1/query prüfen, ob die Metrik überhaupt Serien hat — und für jede Überwachung die Gegenfrage stellen, woran man merken würde, dass sie selbst ausgefallen ist.

Stand E0a danach

homelab-external#23 ist geschlossen, E0a ist abgenommen. Kriterium 2 entfiel auf Robins Entscheid („Lass es einfach mit dem Screenshot, wir haben es schon ausreichend bewiesen") — der Logbeleg ist ohnehin stärker als ein Foto.

Offen bleiben homeserver#194 (Rotation; TS3 ✓, nächster Posten CF-Token), homeserver#81 (Mosquitto, braucht Wartungsfenster) und die provozierten Fehler nach homelab-external#31.

Der Cloudflare-Tunnel ist laut Robin im Zero-Trust-Dashboard gelöscht — damit ist das TUNNEL_TOKEN aus der History entwertet und fällt endgültig von der Rotationsliste.

Neu angelegt: homeserver#216 (Backup-Konzept belastbar machen, inklusive der Frage, was die UNAS Pro inzwischen selbst kann — Robin: „Das muss dann wirklich RockSolid sein und nicht auf Vermutungen basieren") und homeserver#217 (TargetDown- und Reload-Regel).


2026-09-11 — E0a: #200 abgeschlossen, der Reboot war die Ursache

Issue: homeserver#200 geschlossen (beide Teile) PR: homeserver#212

Die Auswertung, auf die vier Tage gewartet wurde

Am 07.09. stand die Frage offen, ob die Stale-Ereignisse wirklich weg sind oder ob 19 Stunden ohne Ereignis nur Zufall waren (p ≈ 7 %). Jetzt, nach 5 Tagen und 4 Stunden, ist es eindeutig:

Nachweis Wert
RestartCount des Containers 0
„stale" im Container-Log 0 Treffer
min_over_time(nuc_ups_scrape_success[4d]) 1 — 23.040 Messungen, kein Fehlschlag
changes(nuc_ups_scrape_success[4d]) 0

Bei der vorherigen Rate von 3,3 Ereignissen pro Tag wären rund 17 fällig gewesen; die Wahrscheinlichkeit für null liegt bei etwa 4·10⁻⁸.

Der tragende Beleg ist RestartCount: 0, nicht die Metrik. Der Collector tastet alle 30 s ab und kann ein 20-Sekunden-Ereignis verpassen — der Restart-Zähler nicht, weil jedes Ereignis über den Healthcheck einen Neustart ausgelöst hätte. Dass zwei unabhängige Quellen dasselbe sagen, macht die Aussage belastbar.

Was offen bleibt, und warum das in Ordnung ist

Ob das Kernel-Update (5.15.0-179 → 191) oder der Speicherdruck (Swap 3,9 von 4 GiB nach 100 Tagen Uptime, danach 0) das Problem behoben hat, ist rückwirkend nicht trennbar. Weitere Instrumentierung bringt nichts, solange das Problem nicht auftritt — debug_min = 1 hat in fünf Tagen exakt null Ereignisse protokolliert, weil es keine gab.

Der eigentliche Schaden war ohnehin nie das einzelne Ereignis (20 s blinde Überwachung, danach Selbstheilung), sondern dass es vier Monate unbemerkt lief. Das ist behoben: Ein Rückfall wäre jetzt ein Alarm, keine Archäologie.

Randbeobachtung: die eigene Einschränkung live bestätigt

Beim Deploy von #212 war die USV-Überwachung für einige Sekunden unterbrochen — die Metrik hat das nicht erfasst (min_over_time[15m] = 1, keine Flanke). Der 30-Sekunden-Takt fiel zufällig daneben.

Genau diese Einschränkung steht seit dem 07.09. in nut/docs.md, hier unbeabsichtigt verifiziert: Der Collector misst die Rate über Stunden, nicht das Einzelereignis. Ein schöner Beleg dafür, dass es richtig war, den Beweis für Teil 1 auf RestartCount zu stützen statt auf die Metrik.

Verifikation nach dem Deploy

Status: running   RestartCount: 0   Health: healthy   FailingStreak: 0
driver.debug: 0          [D1]-Zeilen: 0   (Log 236 → 14 Zeilen)
driver.flag.pollonly: enabled     driver.parameter.pollfreq: 30

pollonly und pollfreq blieben erhalten, und der Container lief nicht in die Restart-Schleife aus #206.


2026-09-07 — E0a: USV-Metriken gebaut, und die Stale-Ereignisse sind vorerst weg

Issue: homeserver#200 (Teil 2 erledigt, Teil 1 offen) PRs: homeserver#209, homelab-documentation#43

Teil 1: das Log war leer, weil nichts mehr passiert ist

Die Session sollte mit dem auswerten beginnen, was debug_min = 1 seit dem 06.09. mitschreibt — der libusb-Fehler, der erklärt, warum usbhid-ups 3–5×/Tag verstummt. Der steht nirgends. Es gab schlicht kein Stale-Ereignis mehr, seit der Container beim Reboot neu erzeugt wurde: 19 h 17 min, RestartCount: 0, kein einziger Treffer auf „stale".

Zum Vergleich die Restart-Verteilung aus dem Docker-Journal des alten Boots:

Zeitraum Ereignisse/Tag
01.–09.08. 7–12
10.08.–05.09. 1–5 (Ø 3,3)
seit 06.09. 20:11 0

Bei 3,3/Tag wären in 19 h rund 2,6 zu erwarten gewesen. Null ist auffällig, aber bei p ≈ 7 % noch kein Beweis — das braucht 2–3 Tage.

Ein Ausschluss steht jetzt breiter: „kein USB-Reset auf Kernel-Ebene" war über 7 Tage geprüft. Ich habe den kompletten vorherigen Boot durchgesehen — 29.05. bis 06.09., über drei Monate: null USB-Resets, Disconnects oder xhci-Fehler. Der Kernel hat die USV nie verloren; das Problem lag die ganze Zeit im Userspace.

Neue Hypothese: der Reboot selbst. Kernel 5.15.0-179 → 191, und der Swap war nach 100 Tagen Uptime mit 3,9 von 4 GiB voll. Rückwirkend nicht beweisbar — nur beobachtbar, ob es wegbleibt.

Teil 2: die Messgrundlage, die vorher fehlte

Statt weiter zu warten wurde Teil 2 gebaut, denn der liefert genau die Zeitreihe, mit der sich Teil 1 überhaupt auswerten lässt. nut/scripts/ups-textfile-collector.sh ruft alle 30 s upsc auf und schreibt 15 Metriken über den Textfile-Collector des node-exporter, dazu 7 Alarmregeln.

Die tragende Metrik ist nuc_ups_scrape_success: upsc scheitert mit Error: Data stale genau dann, wenn der Treiber verstummt. Damit wird aus dem Problem eine Zeitreihe statt einer Zahl, die jemand im Journal zusammenzählt:

changes(nuc_ups_scrape_success[24h]) / 2    # Ereignisse pro Tag

Zwei Fehlerfälle sind absichtlich behandelt, beide aus früheren Pannen gelernt:

  • Nicht-numerische Werte werden übersprungen. NUT liefert auf manchen Modellen unknown; eine einzige unparsbare Zeile lässt den node-exporter die ganze Datei verwerfen — das hätte stillschweigend alle USV-Metriken mitgenommen. Genau die Sorte stiller Ausfall, die dieses Epic aufräumt.
  • Der Erfolgs-Zeitstempel wird bei einem Fehlschlag übernommen. Verschwände die Serie während eines Ausfalls, wäre „wie lange schon stale" nicht messbar und UpsMetricMissing würde zusätzlich zum echten Alarm feuern.

Funde nebenbei

  • Die Alerting-Doku war veraltet: docs-alerting.md listete weiterhin ContainerRestarting, obwohl die Regel am 06.09. in #197 als tot entfernt wurde. Wer die Doku las, hielt eine Restart-Erkennung für vorhanden, die es seit vier Monaten nicht gab und seit gestern gar nicht mehr gibt. Korrigiert.
  • Drei Runbook-Kommandos wären falsch gewesen, hätte ich sie aus Annahme geschrieben: Prometheus hat kein Port-Mapping (curl localhost:9090 läuft ins Leere), das Image hat wget, aber weder curl noch kill (Reload also über --web.enable-lifecycle), und rule_files ist ein Glob — die neue Regeldatei wird allein durch git pull gefunden. Alle Kommandos im Runbook sind gegen den laufenden NUC geprüft.
  • Klartext-Secrets in Container-Umgebungen (nut-upsd, navidrome, gluetun). Geprüft und als Kommentar an #194 gehängt: aktuell steht keines davon in einer getrackten Datei. In der Historie zwei echte Literale — API_PASSWORD (ein Tag) und WIREGUARD_PRIVATE_KEY (20.09.2024 → 27.09.2025, gut ein Jahr). Beide sind nachweislich rotiert, gemessen über einen Hash-Vergleich gegen die laufenden Container. Zusammen damit, dass homeserver privat ist: kein Handlungsbedarf, insbesondere kein History-Rewrite.
  • Nur zwei Stacks nutzen den Infisical-Agent (ollama, speedtest-tracker). Alle anderen halten ihre Secrets in gitignorierten .env-Dateien — das ist der Umfang, den E2a vor sich hat.

Nachtrag am selben Abend: eingerichtet und verifiziert

Robin hat die Host-Einrichtung durchgeführt, alle drei PRs sind gemergt. Gegengeprüft:

Prüfung Ergebnis
Timer enabled, active, OnUnitActiveUSec=30s greift
Letzter Lauf Result=success
ups.prom 18 s alt, root:root 644
node-exporter Up, Flag aktiv — kein Restart durch den :ro-Mount
Metriken im Exporter 15 von 15
node_textfile_scrape_error 0
Prometheus Ladung 100 %, Restlaufzeit 2025 s, Last 29 %
Alarmregeln 7/7 geladen, alle health: ok

nut-upsd steht zu diesem Zeitpunkt bei 21 Stunden, RestartCount: 0, null Stale-Treffer.

Dabei gefunden: die Storage Box läuft voll

Issue: homeserver#210 (E1)

Beim Prüfen der aktiven Alarme feuerte einer, der nichts mit dieser Session zu tun hat: DiskSpaceLow für /mnt/storagebox seit dem 05.09. — 913G von 1,0T belegt, 90 %.

Nichts ist kaputt: Der borgmatic-Lauf von 15:36 war erfolgreich, und die Journal-Treffer auf „error/failed" sind ausnahmslos Deprecation-Warnungen (on_error is deprecated). Aber die Box ist das Off-Box-DR-Ziel, und der Puffer schrumpft.

Aufteilung: nuc 913G, aiserver 13M. Erster Verdacht war die Retention (keep_monthly: 12, keep_yearly: 2 halten gelöschte Daten bis zu zwei Jahre fest) — das erwies sich im nächsten Abschnitt als falsch.

Der Punkt, der es dringend macht: In hetzner.yaml stehen die UNAS-Daten auskommentiert (# - /mnt/unas/Fotos). Die Familienfotos sind also derzeit nicht off-box gesichert — und für sie ist auf der Box auch kein Platz. Das kollidiert mit E5 (#188), wo Immich zum Familien-Primärsystem wird.

Die Zahlen dazu lieferte Robin anschließend selbst — die Configs sind 0600 root, an borgmatic rinfo kam nur er heran.

Der Backup-Status, den Robin daraufhin sehen wollte

Aus der Frage „warum ist es so groß und warum liegt lokal weniger?" wurde die inhaltsreichste Untersuchung des Tages. Drei Hypothesen wurden aufgestellt und zwei davon widerlegt — festgehalten, weil das Widerlegen hier mehr wert war als das Vermuten.

lokal Hetzner
Original 11,31 TB 11,67 TB
Dedupliziert 686,67 GB 978,99 GB
Unique Chunks 1.255.267 1.371.847
Archive 21 (ab 04.01.2026) 21 (ab 04.01.2026)

Widerlegt 1 — „das lokale Repo ist jünger": Beide haben 21 Archive mit praktisch identischen Zeitpunkten.

Widerlegt 2 — „das Hetzner-Repo wurde unkomprimiert befüllt": Genau umgekehrt. Das Januar-Archiv spart 46,3 % durch Kompression, das September-Archiv nur 5,7 %. borg recreate --recompress fällt als Option weg.

Stattdessen gefunden — die Dateizahl je Archiv ist um 94,5 % eingebrochen: von 5.595.710 (Januar) auf 305.662 (September), bei gestiegenem Volumen. Ursache ist die Umstellung von expliziten Pfaden auf --patterns-from. Kurz sah das nach einem Backup aus, das seit Monaten die Systemkonfiguration nicht mehr enthält.

Der Test darauf gab Entwarnung — alle sechs Quellverzeichnisse sind im Archiv:

320254 mnt      26824 opt      23955 root
 16547 var       9942 home      1924 etc

Weggefallen sind rund 5,3 Mio Kleindateien, mit hoher Wahrscheinlichkeit der overlay2-Baum unter /var/lib/docker — aus den Images jederzeit rekonstruierbar. Das erklärt zugleich den Kompressionsabfall, und die Laufzeit sank von 14 h 15 min auf 4 min 9 s. Eine gute Optimierung, keine Lücke.

Die teuerste Korrektur betraf die eigene Rechnung. Die Schätzung „24,5 GB je Archiv" stand bereits in #210. borgmatic info je Archiv zeigt: Am ältesten Monatsarchiv hängen 3,44 GB, am neuesten 885 MB. Die 979 GB stecken im gemeinsamen Grundbestand, nicht in den alten Archiven. Damit ist Retention kürzen weitgehend wirkungslos — es bliebe nur Aufstocken oder die Quellen zu reduzieren. Das Issue wurde korrigiert.

Offen bleibt, warum das lokale Repo bei nahezu gleichen Quellen 43 % kleiner ist.

Issue: homelab-documentation#45 (E0c)

mkdocs.yml setzt weder strict: true noch validation:. Ein toter interner Link erzeugt nur eine Warnung, der Build läuft grün durch und deployt. Der grüne Build von #43 beweist also nicht, dass die vier neuen Querverweise auflösen — nur, dass gebaut wurde; geprüft wurden sie von Hand. Dieselbe Fehlerklasse wie die toten Alarmregeln: ein Guardrail, der aussieht, als würde er prüfen. Beim Sichten fiel nebenbei auf, dass Infisical Agent zweimal in der Nav steht.

Zweiter Teil: #195 abgeschlossen — und ein offener Port gefunden

Issue: homeserver#195 geschlossen · homeserver#211 neu (geparkt)

Bei #195 fehlte seit dem 06.09. nur ein Nachweis: ob der UDM-Port-Forward auf 443 noch aktiv ist. Ein LAN-Test ist wegen Hairpin-NAT wertlos — also lief er vom Hetzner-VPS aus, einem echten externen Aussichtspunkt.

Vorher die Heim-IP verifiziert, aus zwei unabhängigen Quellen: Der cloudflare-ddns-Container hatte 217.250.186.222 drei Minuten zuvor bestätigt, ifconfig.me vom NUC lieferte dieselbe. Ohne diesen Schritt hätte der Test womöglich den Anschluss eines fremden Telekom-Kunden getroffen.

Ziel Ergebnis
1.1.1.1:443 (Kontrolle) OFFEN
VPS selbst :443 (Kontrolle) OFFEN
Heim-IP :443 gefiltert (DROP)

Die Kontrollen belegen, dass die Methode trägt. Wäre der Forward wirksam, müsste Traefik den Handshake beantworten. #195 geschlossen.

Der Fund nebenbei: ServerQuery ist von außen erreichbar

Da der externe Aussichtspunkt ohnehin stand, wurden zwölf Ports mitgeprüft. Genau zwei waren offen: 30033 (TS3-Filetransfer, gehört zum öffentlichen Betrieb) und 10011 — die TeamSpeak-ServerQuery-Administrationsschnittstelle. Sie antwortet von außen nicht nur mit einem Banner, sondern führt Kommandos aus (versionerror id=0 msg=ok).

Die query_ip_allowlist mit 127.0.0.1 im Log sieht nach Zugangsschutz aus, ist aber keiner — sie nimmt IPs vom Flood- und Ban-Schutz aus. Vor einem Login steht nur das Passwort.

IR-Check sauber: 126 Logzeilen seit dem 03.06.2026, kein einziger Login-Versuch, keine Bans. Exposition ja, Ausnutzung nein.

Robins Entscheidung: Der Port bleibt vorerst offen, ServerQuery wird gebraucht. In #211 dokumentiert und geparkt, damit der Befund bei einem künftigen Review nicht erneut als neue Entdeckung auftaucht. Als Mittelweg für später notiert: Zugriff über Tailscale statt über den offenen Port, thematisch bei E8.

Methodisch schlecht gelaufen: Die Bewertung dieses Fundes wechselte zweimal — erst Alarm, dann Entwarnung, dann wieder Alarm. Ursache waren ein falscher Container-Name (ts3 statt teamspeak) und ein fehlerhafter Banner-Test (head -c 200 blockiert, wenn die Antwort kürzer ist), vor allem aber das Bewerten zwischen den Messungen statt danach. Der Endstand ist belegt, der Weg dahin war unnötig unruhig.

Offen

In 2–3 Tagen Teil 1 auswerten. Ab jetzt geht das über die Metrik statt über das Journal:

changes(nuc_ups_scrape_success[24h]) / 2

Bleibt es bei 0 Ereignissen, war der Reboot die Ursache und debug_min kann raus. Kommt eines, steht der libusb-Fehler jetzt im Log.


2026-09-06 (4) — E0a: Reboots durchgeführt, plus eine selbstverursachte Panne

Issue: homeserver#196 erledigt PR: homeserver#206 (Regression aus #204)

Zuerst: eine Regression aus dem eigenen PR

Der Verzeichnis-Mount für nut aus #204 trug :ro. Das Entrypoint des Images führt aber ein chgrp auf /etc/nut/local aus — auf einem Read-only-Mount scheitert das, das Entrypoint bricht mit Exit 1 ab, restart: unless-stopped startet neu. nut-upsd lief rund zwei Stunden in einer Restart-Schleife, 198 Restarts, die USV-Überwachung war in der Zeit tot.

Der Einzeldatei-Mount davor hatte das Problem nicht: Docker legte /etc/nut/local als beschreibbares Verzeichnis an, nur die Datei darin war read-only.

Der Beleg lag sogar schon vor: Bei der Analyse zu #197 war das Verzeichnis mit drw-r----- root nut aufgelistet worden — die Gruppe nut ist genau das Ergebnis dieses chgrp. Angesehen, nicht gelesen.

Die Lücke in der Prüfung: #204 hatte mit docker compose config verifiziert, dass die Mounts auflösen — nie, ob ein Container mit ihnen startet. Das ist kein Ersatz. In der mitgegebenen Verify-Liste standen Traefik und unbound, ausgerechnet nut aber nicht. Beim Fix wurden beide Varianten lokal gegen das echte Image gestartet, bevor irgendetwas behauptet wurde.

Koordinationspanne beim Hetzner

Der Hetzner wurde zweimal neu gestartet — einmal von Claude um 19:11, einmal von Robin um 19:16. Ursache: Die Aufgabe war Robin zugewiesen worden („das machst du"), er schickte daraufhin den SSH-Key, und Claude leitete daraus ab, sie selbst ausführen zu sollen, ohne das deutlich anzukündigen. In der Folge wurde Robins „Reboot ist durch" fälschlich auf den NUC bezogen und dessen fehlender Neustart als Fehlschlag gedeutet.

Ohne Schaden, aber die Lehre steht: Wer eine Aufgabe abgibt, führt sie nicht ungefragt selbst aus, nur weil die Mittel dazu bereitgestellt werden.

Die Reboots

Hetzner NUC
Kernel 6.8.0-1246.8.0-139 5.15.0-1795.15.0-191
Uptime vorher 75 Tage 100 Tage
Ausfallzeit ~30 s ~90 s
Container 12/12, Liste identisch 65/65, Liste identisch

Auf dem Hetzner lief anschließend ein dist-upgrade mit Docker 29.6.1 → 29.8.0 und containerd 2.2.5 → 2.3.4. Bemerkenswert: der Austausch des Daemons hat die laufenden Container nicht neu gestartet.

Das Drop-in hat messbar gewirkt

Der eigentliche Ertrag des Runbooks, im Bootlog des NUC nachweisbar:

mnt-unas / pictures / monitoring / paperless   20:11:31
mnt-storagebox                                 20:11:32
docker.service                                 20:11:40   ← 8 Sekunden später

Ohne RequiresMountsFor wären beide parallel gestartet. Diese acht Sekunden sind der Unterschied zwischen „Container sieht seine Daten" und „Container arbeitet dauerhaft auf einem leeren Verzeichnis". Gegengeprüft: Immich, Paperless, VictoriaLogs und VictoriaMetrics sehen alle ihre Daten.

Swap war vorher zu 3,9 von 4,0 GiB belegt und steht jetzt bei 0.


2026-09-06 (3) — E0a: Einzeldatei-Mounts beseitigt, Reboot-Runbook

Issues: homeserver#199 geschlossen · #196 vorbereitet PRs: homeserver#204, homelab-documentation#40

Einzeldatei-Mounts (#199)

Die letzten vier Stacks umgestellt — traefik, pihole, nut, influxdb. Jeder legt seine Configs jetzt in ein configs/-Unterverzeichnis und mountet das Verzeichnis. Damit hat das Repo keinen Einzeldatei-Bind-Mount mehr.

configs/ mit „s" ist kein Zufall. Der erste Versuch mit config/ scheiterte an .gitignore, das ein globales **/config/ enthält. Bereits getrackte Dateien überleben das zwar, aber jede später hinzugefügte Datei wäre stillschweigend nicht eingecheckt worden — eine Falle derselben Bauart wie die, die hier beseitigt wird. monitoring-stack benutzte configs/ ohnehin schon.

Vor jeder Umstellung geprüft, was das Image am Zielpfad mitbringt, weil ein Verzeichnis-Mount das verdeckt: bei traefik, unbound und nut existiert das Verzeichnis im Image gar nicht, bei telegraf liegen dort telegraf.conf und ein telegraf.d/ — letzteres wird deshalb als leeres Verzeichnis mitgeführt.

Traefiks Dateien behalten ihre .yml-Namen, damit sämtliche Container-Pfade unverändert bleiben und der File-Provider-Eintrag weiter stimmt. Am kritischsten Bauteil ändert sich so nur der Mount und sonst nichts.

Fünfter toter Guardrail: der unbound-Healthcheck

Beim Prüfen fiel auf, dass unbound seit Containerstart als unhealthy markiert ist — FailingStreak: 158070, also rund 55 Tage ununterbrochen. Der Check ruft drill-hc auf, ein Binary, das im Image nicht existiert. DNS funktionierte die ganze Zeit einwandfrei; der Check hat nie etwas ausgesagt, weder im Guten noch im Schlechten.

Das Image bringt auch keine Shell mit, CMD-SHELL ist dort also unmöglich. Von den vorhandenen Binaries unterscheidet nur dig sauber: im Container verifiziert, 0 wenn unbound antwortet, 9 wenn nichts da ist. delv wäre inhaltlich näher an der DNSSEC-Absicht gewesen, liefert aber auch gegen einen toten Server 0 — also ebenso nutzlos. Der neue Check erkennt einen toten Resolver, keinen SERVFAIL.

Reboot-Runbook (#196)

Als Runbook angelegt. Der Kern ist ein struktureller Fund: docker.service hat keine Abhängigkeit zu den Mount-Units, es ist nur nach network-online.target geordnet. Die fünf Netzwerk-Mounts sind Before=multi-user.target, Docker aber auch — zueinander sind sie damit nicht geordnet.

Docker kann Container also starten, bevor NFS und Storage Box bereit sind. Bind-Mounts werden beim Start aufgelöst; zeigt /mnt/unas in dem Moment noch auf das leere lokale Verzeichnis, arbeitet der Container dauerhaft darauf weiter. Betroffen wären Immich, Paperless, der Media-Stack und die 5-Jahres-Daten von VictoriaMetrics und VictoriaLogs. Das Runbook enthält das systemd-Drop-in mit RequiresMountsFor als einmalige Vorbedingung.

Zweiter Punkt, der leicht übersehen wird: Pi-hole auf dem NUC ist der DNS-Server des ganzen LANs. Während des Reboots löst im Haus nichts auf — daher die Ansage an Lars und die Prüfung, ob auf der UDM ein Zweit-Resolver hinterlegt ist.

Alle Verify-Kommandos wurden gegen die laufende Maschine getestet; zwei Container-Namen und ein Datenpfad im Entwurf waren falsch und sind korrigiert. Die Reboots selbst stehen noch aus, sie brauchen ein Wartungsfenster.

Aus der Verifikation heraus: zwei Leichen entfernt

PRs: homeserver#205, bonus-tracker#42

Beim Gegenprüfen des Deployments fielen zwei tote Routen auf. Zwei eigene Prüffehler übrigens auch: die Traefik-Metrikzahl fiel von 3262 auf 66, was nach Verlust aussah, aber Normalverhalten ist — Metriken entstehen pro Service erst mit dem ersten Request (nach neun Testanfragen: 282). Und der erste Router-Test lieferte sechsmal 000, weil -H "Host: …" keine SNI setzt und Traefik den Handshake ohne SNI ablehnt. Mit --resolve antworteten alle sechs.

usv.home.robinwerner.net zeigte auf PowerPanel Business, Port 3052. Die Software ist vom NUC vollständig verschwunden — kein Listener, keine Unit, keine Binaries, kein Paket; NUT hat sie abgelöst. Der Router lieferte seitdem 502, und Traefik hielt ein Zertifikat für einen Hostnamen aktuell, hinter dem nichts steht.

Nebenwirkung, die zu #200 gehört: Damit gibt es keine Weboberfläche für die USV mehr — PowerPanel weg, NUT hat keine. Das macht den Vorschlag, die USV-Werte nach Prometheus zu bringen, konkreter als zuvor.

bonus.local war doppelt tot: der Name löst nirgends auf, und beide Container hängen nur im Netz internal, während Traefiks Docker-Provider auf proxy_network beschränkt ist. Nicht tot war die Nebenwirkung — der websecure-Entrypoint forderte für jeden Router ein Zertifikat an, und Let's Encrypt kann für .local grundsätzlich keines ausstellen. Traefik versuchte es in Dauerschleife; gescheiterte Orders zählen auf das Rate-Limit des Kontos ein und können die Ausstellung für Namen blockieren, auf die es ankommt. Die App selbst bleibt unberührt, sie läuft über Host-Port 8888.


2026-09-06 (2) — E0a: Cloudflare-Tunnel stillgelegt

Issue: homeserver#195 — Schritte 1 und 2 erledigt, 3 und 4 offen PR: homeserver#203 gemergt

Der Ingress ließ sich ohne Dashboard-Zugriff ermitteln: cloudflared protokolliert beim Start die empfangene Config. Der Tunnel bediente ha.robinwerner.net (Home Assistant) und vs.robinwerner.net (VS Code Server), dazu aktives WARP-Routing.

Genutzt wurde nichts davon. cloudflared_tunnel_total_requests stand über das gesamte 30-Tage-Fenster auf 0, bei sieben Wochen Container-Uptime ohne Restart — der Zähler wurde also nie zurückgesetzt. Eine eingerichtete HA-Companion-App würde dauerhaft Traffic erzeugen. Robin bestätigte, dass beide Hostnames entbehrlich sind und WARP nie eingerichtet war.

Entfernt: der cloudflared-Stack, sein Prometheus-Scrape-Job und alle Referenzen in beiden Repos inklusive LikeC4-Modell.

Sicherheitsrelevant

Das TUNNEL_TOKEN stand im Klartext in der getrackten docker-compose.yaml — nicht in einer .env. Die Stack-docs.md führte das bereits als dringendes ToDo mit dem Vorschlag, es zu rotieren. Die Stilllegung ist die bessere Antwort: den Tunnel zu löschen entwertet das Token vollständig, statt ein Geheimnis zu rotieren, das gleich darauf wegfliegt. Damit erledigt sich ein Posten aus #194. Die git-History behält den Token, deshalb muss der Tunnel im Cloudflare-Dashboard gelöscht werden.

Nebenbefund zur externen Erreichbarkeit

Die Netzwerk-Doku behauptete, die externe Exposition liefe über den UDM-Port-Forward 443 auf den Traefik des NUC. Das löst kein Router ein: alle 21 Traefik-Router lauten *.home.robinwerner.net, von außen matcht kein Host-Header. cloudflare-ddns pflegt genau zwei Records auf die Heim-IP, ddns.robinwerner.net und udm.robinwerner.net — keiner davon ein Router-Name. Der Forward ist damit Angriffsfläche ohne Funktion.

Ob er überhaupt noch aktiv ist, ließ sich nicht klären: die UDM-Konfiguration ist von hier nicht lesbar, und ein Test aus dem LAN ist wegen Hairpin-NAT wertlos. Bleibt als Schritt 3 in #195 offen.

Beim TS3-Stack fiel zusätzlich auf, dass 10011 (ServerQuery, Admin-Schnittstelle) neben den für Freunde nötigen Ports 9987/udp und 30033 steht, und dass dort ein DB-Passwort im Klartext in der getrackten Compose-Datei liegt — an #194 gemeldet.


2026-09-06 (1) — E0a: USV-Kette, Alarmregeln, Config-Mounts

Issues: homeserver#197 geschlossen · #199 zum Großteil erledigt · #200 neu PRs: homeserver#201, #202 — beide gemergt und auf dem NUC deployt

NUT-Restart-Loop (#197)

Der nut-upsd-Container hatte 606 Restarts mit sauberem SIGTERM. Die Vermutung war Fremdeinwirken — Watchtower-Reste, ein Cron, eine NUT-Doppelinstanz. Tatsächlich stammt der Kill aus dem Image selbst: dessen Healthcheck führt bei „stale" ein killall -TERM upsmon aus, und upsmon ist im Container PID 1. Der Kill beendet damit den ganzen Container, restart: unless-stopped bringt ihn 0,3 s später mit frischer USB-Verbindung zurück. Das ist die Selbstheilung des Images; der Restart-Zähler ist in Wahrheit ein Zähler für Stale-Ereignisse (3–5/Tag, je rund 20 s blinde USV-Überwachung).

Einzeln ausgeschlossen: Watchtower (existiert nicht), Cron/systemd-Timer, NUT-Doppelinstanz (nut-server ist not-found), OOM, USB-Reset auf Kernel-Ebene (7 Tage keine Meldung in journalctl -k), USB-Autosuspend, wirkungslose ups.conf.

Warum der Treiber verstummt, war aus den vorhandenen Daten nicht bestimmbar — usbhid-ups loggt beim Standard-Level nichts. Statt zu raten wurde debug_min = 1 gesetzt.

Drei tote Guardrails

Der eigentliche Fund der Session: alle drei sahen dauerhaft grün aus.

Guardrail Defekt
ContainerRestarting lief auf container_restart_count — cAdvisor exportiert die Metrik nicht. Konnte nie feuern; deshalb blieben 606 Restarts vier Monate unbemerkt
ContainerDown absent() über einen Regex-Selektor feuert nur, wenn alle Container gleichzeitig fehlen — ein einzelner toter Dienst blieb still
prometheus.yml als Einzeldatei gemountet: der Container lief vier Wochen mit einer Config von vor dem Merge, der aiserver wurde nie gescrapt

Regel 1 entfernt (kein Ersatz möglich — container_start_time_seconds bewegt sich bei Docker-Restarts nicht), Regel 2 in Gleichheits-Matcher aufgeteilt und um nut-upsd erweitert, beides live gegen Prometheus gegengeprüft.

Config-Mounts (#199)

prometheus, alertmanager und alloy mounten jetzt ihr Config-Verzeichnis statt der Einzeldatei. Ein Einzeldatei-Bind-Mount hängt am Inode: git pull ersetzt die Datei, der Container behält die alte, und nichts meldet einen Fehler. Der Reload-Pfad ist geklärt (alle drei haben /-/reload), --force-recreate braucht es künftig nur bei Änderungen am Service selbst.

Ein vollständiger Audit fand 8 Einzeldatei-Mounts. Drei behoben; fünf offen (traefik 2×, pihole, nut, influxdb) — die brauchen je ein config/-Unterverzeichnis, weil dort die Config neben .env und Zustandsdateien liegt.

Nebenbei repariert

Die ONBATT- und LOWBATT-Meldungen der Shutdown-Kette trugen einen Gedankenstrich im Titel. Der geht als Title:-HTTP-Header raus, HTTP-Header sind latin-1 — ntfy stellt alles jenseits ASCII verstümmelt dar, ohne einen Fehler zu melden. Ausgerechnet die beiden Meldungen, die bei Stromausfall zählen.

Offen daraus

  • #200: Wurzel der Stale-Ereignisse. In 1–2 Tagen docker logs nut-upsd um ein „Signal 15"-Ereignis auswerten; bleibt es stumm, debug_min = 2
  • #200: echte USV-Metriken (Status, Ladung, Restlaufzeit, Staleness) — heute ist nur bekannt, ob der Container existiert. Vorschlag: Textfile-Collector wie auf dem aiserver
  • #199: die fünf verbleibenden Einzeldatei-Mounts

2026-09-05 — E0a: aiserver-Pakete und externe Alarmkette

Erledigt: aiserver#5, #6 · homelab-external#20, #21, #27 · #22 geparkt Neu entdeckt: homeserver#199

  • aiserver#5 — borgmatic-Fehlalarme seit dem 14.08. Drei Ursachen: Textfile-Rechte, ein systemd-Drop-in ohne [Timer]-Kopf (wird zeilenweise still verworfen) und ein ntfy-Titel mit Geviertstrich, der einen UnicodeEncodeError warf — die Anfrage ging wortlos nie raus. Doku-PR #7
  • aiserver#6 — Open WebUI: Signup war real längst zu; ENABLE_SIGNUP ist eine ConfigVar, die DB schlägt die ENV. Ein Konto (Robin/admin), .env nachgezogen. Doku-PR #8. Damit ist der aiserver E0a-frei
  • homelab-external#20 — Uptime-Kuma-Backup-Monitore standen auf einem 60-s-Fenster statt auf täglichem Rhythmus; Monitor „Backup Local" hatte gar keine Benachrichtigung
  • homelab-external#21 — Healthchecks-SMTP-Username im falschen Format; die alte Adresse gab 535, Healthchecks konnte also nie mailen
  • homelab-external#23 — Alarmkette positiv bewiesen: injizierter Critical-Alarm 20:07, iPhone holt die Nachricht 2 s später ab. Bleibt bis 12.09. offen (7-Tage-Fenster), dann Auswertung und E0a-Abnahme
  • homelab-external#22 geparkt — Port-Mapping 3478 korrigiert (PR #30), STUN antwortet trotzdem nicht. Nächster Schritt: headscale log.level=trace bzw. tailscale netcheck
  • homelab-external#27 — verwaister Deploy-Key gelöscht (1× am 30.01. benutzt, danach nie)

2026-08-26 — E0a: Forward-Auth für die externen Werkzeuge

Erledigt: homelab-external#19 · Neu entdeckt: #25, #26, #27

PRs #24 und #18 deployt: Forward-Auth inklusive lldap live, IR-Check negativ, Admin-Passwort rotiert, Doku-PR #28.

Drei Funde nebenbei aufs Board: der Auto-Deploy-Cron existiert auf dem VPS gar nicht (Renovate-Merges erreichten den Server also nie), das Traefik-Access-Log war ungebremst auf 415 MB gewachsen, und ein verwaister Deploy-Key lag herum.


2026-08-23/24 — Planungssession und Roadmap v3

Bestandsaufnahme über 6 Repos und 3 Server in 12 Frage-Runden, kritischer Seitenreview durch zwei unabhängige Review-Agents plus Web-Faktencheck, danach Robins Gegenlesen-Runde. Ergebnis: Roadmap v3, freigegeben am 24.08. mit der Zielmarke 31.12.2026.

Board und Issues angelegt: Epics als homeserver#176#193, granulare Pakete je Repo. homelab-external wurde auf privat gestellt. Die Planungsdokumente kamen als PR #35 in diese Doku-Drehscheibe, die damit die kanonische Quelle ist.

Details in Bestandsaufnahme, Zielbild & Roadmap und Seitenreview.