From cbfbb8ca4f2c2882768830463409198c51722eb7 Mon Sep 17 00:00:00 2001 From: Micha Date: Tue, 23 Jun 2026 20:09:56 +0200 Subject: [PATCH] Add self-hosted Healthchecks stack for internal job monitoring (hybrid) Self-hosted Healthchecks (ops/healthchecks/) as the hub for internal cron/job heartbeats. The three host-down/backup watchdogs (Borg pre-hook, baerchen nearline pull, monitoring watchdog #8) deliberately stay on healthchecks.io cloud, since an on-host watcher cannot report a host outage. - frontend_net + dedicated PostgreSQL 18 in healthchecks_internal - native Healthchecks auth; ping/API exempt from Authelia (n8n/Komodo pattern) - registered as middleware_exempt in ops/policy-checks/exceptions.json - docs: DECISIONS, ARCHITECTURE (3.1/4.2/7.6/10), SERVICE_CATALOG, SECRETS_MAP, MASTER_TODO, README index docker compose config validated (exit 0). Not yet deployed: host secret file, appdata dir, Komodo stack + ENV and Gitea webhook remain operator steps. Co-Authored-By: Claude Opus 4.8 --- HOMELAB_ARCHITECTURE_MASTER_V2.md | 5 ++ docs/DECISIONS.md | 40 +++++++++ docs/MASTER_TODO.md | 1 + docs/README.md | 1 + docs/SECRETS_MAP.md | 6 ++ docs/SERVICE_CATALOG.md | 2 + ops/healthchecks/README.md | 111 +++++++++++++++++++++++++ ops/healthchecks/docker-compose.yml | 124 ++++++++++++++++++++++++++++ ops/policy-checks/exceptions.json | 1 + 9 files changed, 291 insertions(+) create mode 100644 ops/healthchecks/README.md create mode 100644 ops/healthchecks/docker-compose.yml diff --git a/HOMELAB_ARCHITECTURE_MASTER_V2.md b/HOMELAB_ARCHITECTURE_MASTER_V2.md index a60812f..5c62099 100644 --- a/HOMELAB_ARCHITECTURE_MASTER_V2.md +++ b/HOMELAB_ARCHITECTURE_MASTER_V2.md @@ -95,6 +95,7 @@ Jeder produktive Container nutzt `restart: unless-stopped`, außer eine Ausnahme | `monitoring_influx_lan` | Compose-intern, bridge | nicht-oeffentliches Zusatznetz nur fuer Docker Host-Port-Publishing von InfluxDB 8181 | Zielzustand | | `glance_socket_net` | Compose-intern, `internal: true` | interner Zugriff von Glance auf den Docker-Socket-Proxy | umgesetzt | | `smarthome_net` | bridge, `internal: true` | interne Smart-Home-Kommunikation zwischen Home Assistant, Mosquitto, spaeter Zigbee2MQTT/ESPHome | vorbereitet | +| `healthchecks_internal` | bridge, `internal: true` | internes Netz nur fuer `healthchecks` + `healthchecks-postgres` | vorbereitet | | `host` | host | nur für echte Sonderfälle | begründet | ### 3.2 Finales Diagramm (vereinfacht) @@ -169,6 +170,7 @@ Diese Dienste sind **keine Public Apps**: - `hermes-dashboard` — hermes.kaleschke.info (Middleware) - `super-productivity` — sp.kaleschke.info (Middleware) - `n8n` — n8n.kaleschke.info (Traefik ohne pauschale Middleware, native Auth + Webhook-Ausnahme analog Komodo) +- `healthchecks` — hc.kaleschke.info (Traefik, native Healthchecks-Auth; Ping-/API-Endpunkte ohne ForwardAuth analog n8n) - `Traefik-Dashboard` - `AdGuard Home` — Admin-UI auf Port 8082 (`80` im Container), kein Traefik, nur Tailscale-IP `100.80.98.33`; 2026-05-26 bewusst keine 2FA-/Traefik-Umstellung @@ -309,6 +311,8 @@ Legende Status: | `monitoring-influxdb3-core` | ✅ | `monitoring_net`, `monitoring_influx_lan` + LAN-Bind | LAN-Port nur fuer interne Writer | InfluxDB 3 Core fuer Home-Assistant-/Ecowitt-Langzeitdaten; keine Traefik-/Public-Freigabe; Port 8181 nur via `INFLUXDB_BIND_IP` | HA-Write-Token und Sensor-Export finalisieren | | `monitoring-loki` | ✅ | `monitoring_net` | intern | interner Container-Logspeicher ohne Public Route; Monitoring-Grafana greift ueber Loki-Datasource zu | Retention/Storage beobachten | | `monitoring-promtail` | ✅ | `monitoring_net` | intern | Docker-Log-Collector mit read-only Docker-Socket-Ausnahme; schreibt nach Loki | Socket-Ausnahme regelmaessig pruefen | +| `healthchecks` | ✅ vorbereitet | `frontend_net`, `healthchecks_internal` | Traefik, native Auth | self-hosted Heartbeat-Monitor fuer interne Jobs; Ping-/API ohne ForwardAuth (analog n8n); externe Host-down-/Backup-Waechter bleiben auf healthchecks.io-Cloud | Deploy + Gitea-Webhook offen | +| `healthchecks-postgres` | ✅ vorbereitet | `healthchecks_internal` | intern | dedizierte PostgreSQL 18, nie `frontend_net` | — | | `grafana` / `influxdb3-core` / `loki` / `alloy` | entfernt | - | abgeloest | alte Docker-Runtime frei von Altcontainern; Compose-Pfade am 2026-05-26 aus aktivem Repo entfernt | Rollback nur ueber Git-Historie | ### 7.7 Noch offene Sonderfälle @@ -398,6 +402,7 @@ Die Blockmigration aus der Portainer-/Dockerman-Phase ist abgeschlossen: Traefik | `monitoring-influxdb3-core` | Host-Port 8181 auf LAN-IP; `user: "0"` | Home Assistant schreibt spaeter Langzeitdaten. Nach der HA-Container-Entscheidung muss der Writer-Pfad in der Influx-Phase explizit gewaehlt werden: entweder LAN-Bind via `INFLUXDB_BIND_IP` oder gezieltes gemeinsames internes Netz. Keine Traefik-Route, Zugriff nur ueber Token; InfluxDB 3 Core benoetigt im aktuellen Container-Setup Root-Rechte fuer den lokalen Object-Store-Pfad im named volume | | `monitoring-promtail` | Docker-Socket read-only | Docker-Log-Discovery fuer Loki; keine Schreibrechte, keine Appdaten-Persistenz ueber den Socket | | `n8n` | keine pauschale Authelia-Middleware | Webhook-Endpunkte (`/webhook/*`, `/webhook-test/*`) muessen ohne ForwardAuth erreichbar bleiben; n8n bringt eigene Owner-/Login-Auth mit (analog Komodo/Nextcloud) | +| `healthchecks` | keine pauschale Authelia-Middleware | Ping-Endpunkte (`/ping/*`) und API muessen ohne ForwardAuth erreichbar sein, sonst koennen Cron-Jobs nicht melden; Healthchecks bringt eigene Login-Auth fuers Dashboard mit (analog n8n/Komodo). Der self-hosted Dienst deckt bewusst nur INTERNE Job-Checks ab; Host-down-/Backup-Waechter bleiben extern auf healthchecks.io-Cloud | | `plex` | Traefik ohne Authelia, File-Provider-Ausnahme trotz Host-Netz | Plex bringt native Konto-/Client-Auth mit; vorgeschaltete ForwardAuth wuerde Plex Web, Apps und Client-Flows stoeren. Docker-Labels sind fuer diesen Host-Netz-Container ungeeignet, weil Traefik sonst `127.0.0.1:32400` nutzt; daher `traefik/dynamic/plex.yml` mit Ziel `192.168.178.58:32400`. Route nur ueber Traefik/443 (`plex.kaleschke.info`), direkter Plex-WAN-Port 32400 und Plex Remote Access bleiben deaktiviert. | | `homeassistant` | Traefik ohne Authelia, Fach-YAML aus separatem Repo | Home Assistant bringt eigene Auth, mobile Apps, Webhooks und Integrationsfluesse mit. Der Container haengt in `frontend_net` fuer Traefik und in `smarthome_net` fuer MQTT/Zigbee2MQTT/ESPHome. `.storage` und Secrets bleiben in Appdata und werden per Borg gesichert, nicht versioniert. | | `homeassistant` (Ecowitt) | LAN-only Host-Port `8123` auf `192.168.178.58` | Ecowitt-GW3000 kann kein HTTPS und pusht per HTTP an den HA-Webhook. HA bekommt einen Host-Bind nur auf der LAN-IP (`192.168.178.58:8123:8123`, nicht `0.0.0.0`/WAN), analog InfluxDB 8181. Kein Traefik-Umbau des globalen HTTP-Redirects noetig, da Ecowitt rein im LAN pusht. Webhook nicht `local_only`, geschuetzt durch 128-bit-Zufalls-ID. Siehe `docs/DECISIONS.md` (2026-06-13). | diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 067ed3f..9f84f0e 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -11,6 +11,46 @@ in `HOMELAB_ARCHITECTURE_MASTER_V2.md` §13, `docs/MASTER_TODO.md` (Geparkt), --- +## 2026-06-23 - Healthchecks Hybrid: self-hosted fuer interne Jobs, Cloud fuer Host-down-Waechter + +**Entscheidung:** Job-/Cron-Monitoring per Healthchecks (Dead-Man's-Switch) wird +hybrid betrieben. Ein **self-gehosteter** Healthchecks-Stack (`ops/healthchecks/`, +`hc.kaleschke.info`) ist der Hub fuer die vielen **internen** Checks auf einem +laufenden Host ("lief Job X heute?": posture-check, restore-tests, Dump-Erzeugung, +gitea-bundle-mirror). Die drei host-down-/backup-still-Waechter +(Borg-Pre-Hook `ops/borg-ui/scripts/pre-borg.sh`, baerchen-Nearline-Pull +`ops/h-drive-nearline/pull-critical-backups.ps1`, geplanter Monitoring-Watchdog +Empf. #8) bleiben bewusst **extern** auf healthchecks.io-Cloud (Free-Tier). + +**Kontext:** Die zwei bestehenden Borg-/Nearline-Pings waren bereits +endpoint-agnostisch verdrahtet (`docs/SECRETS_MAP.md`), nur die Spielart +(Cloud vs. self-host) war offen (`docs/homelab-optimierung.md` Offene Frage #4). +Ein Waechter, der auf demselben Unraid-Host laeuft, den er ueberwacht, kann einen +Host-Ausfall nicht melden — er ist dann selbst tot, Stille ist nicht von "alles +gut" unterscheidbar. Genau dafuer existieren diese drei Checks, daher muessen sie +extern bleiben. Fuer "lief Job X auf einem lebenden Host?" ist Self-Hosting +dagegen unbedenklich (Host-down ist separat ueber die externen Pings abgedeckt) +und bringt Datenhoheit + unbegrenzte Checks (Cloud-Free = 20). + +**Umsetzung / Ausnahme:** `healthchecks` haengt in `frontend_net` (Traefik, native +Healthchecks-Auth, **ohne** pauschale `authelia@file`: Ping-`/ping/*`- und +API-Endpunkte muessen ohne ForwardAuth erreichbar sein, analog n8n/Komodo — in +`ops/policy-checks/exceptions.json` als `middleware_exempt_identities` registriert). +Dedizierte PostgreSQL 18 nur in `healthchecks_internal` (`internal: true`). +SMTP bewusst aus (Login via Superuser, Alerts via ntfy-Integration nach +`homelab-alerts`). Secrets: `SECRET_KEY`/`DB_PASSWORD`/Superuser als +Komodo-Stack-ENV, Postgres zusaetzlich als Datei-Secret. + +**Alternativen:** (a) Nur Cloud fuer alles — schlanker (kein Stack, kein Postgres), +aber 20-Check-Limit und Metadaten-Abfluss; (b) alles self-host inkl. Backup-Waechter +— verworfen wegen des Host-down-Blind-Spots; (c) ntfy-Heartbeat von einem zweiten +Geraet statt Cloud (Optionsnotiz in Empf. #4) — bleibt moeglich, falls die +Cloud-Abhaengigkeit spaeter unerwuenscht ist. **Review-Trigger:** mehr als ~20 +externe Checks noetig, Wunsch null Cloud-Abhaengigkeit, oder zweite Hardware (dann +koennte der interne Hub dorthin und auch Host-down abdecken). + +--- + ## 2026-06-23 - Komodo nur aus vertrauenswuerdigen Netzen (IP-Allowlist statt public) **Entscheidung:** Der Komodo-Router (`komodo.kaleschke.info`) bekommt eine diff --git a/docs/MASTER_TODO.md b/docs/MASTER_TODO.md index 2aa9108..3dfdcc8 100644 --- a/docs/MASTER_TODO.md +++ b/docs/MASTER_TODO.md @@ -27,6 +27,7 @@ Host-Reports (`/mnt/user/backups/restore-reports/`) und in der Git-Historie. | Home Assistant Tibber | Operator/Codex | Tibber per HA-UI-Config-Flow verbinden. Danach Energy-Dashboard um echte Kosten/Preisquelle ergaenzen; SolarEdge-PV, Netz und Speicher sind bereits konfiguriert und validiert | `docs/runbooks/smart-home-bootstrap.md`, `docs/DECISIONS.md` | | Nearline-Pull Dead-Man's-Switch | Operator | **S4U-Root-Cause 2026-06-21 behoben + verifiziert:** Task `KalliLab H Drive Nearline Pull` von S4U auf LogonType `Interactive` ("Nur wenn Benutzer angemeldet") umgestellt (kein Passwort noetig, da `michi` Dauer-Konsolen-User) -> per Planer mit `0x0` bestaetigt. Spiegel frisch, Exit-Code-Leak gefixt, Heartbeat-Pings gepusht. **Verbleibt (optional, niedrige Dringlichkeit):** je einen Healthchecks-Check anlegen + Capability-URL hinterlegen (baerchen ENV `HEALTHCHECKS_NEARLINE_URL`/Datei; Unraid `/mnt/user/appdata/secrets/healthchecks_borg_url`) | `ops/h-drive-nearline/README.md` | | Monitoring Single-File-Bind-Mount Hardening | Operator/Claude | alertmanager/blackbox/loki/promtail + alertmanager-ntfy-bridge lokal auf Directory-Mounts umgestellt (grafana-provisioning war bereits Directory-Mount); `docker compose config` gruen. **Verbleibt:** Push + Komodo-Redeploy des monitoring-Stacks mit `--force-recreate` (Mount-Pfade aendern sich), danach Reload-/Alert-Smoke | `monitoring/docker-compose.yml` | +| Healthchecks self-hosted (interne Jobs) | Operator | Stack vorbereitet (`ops/healthchecks/`). Pre-Deploy: Appdata `/mnt/user/appdata/healthchecks/postgres18/` + Datei-Secret `healthchecks_postgres_password.txt` + 4 Komodo-Stack-ENV. Dann Komodo-Stack aus Gitea + Pflicht-Gitea-Webhook anlegen, danach interne Jobs (posture-check, restore-tests, Dumps) als Checks verdrahten. Externe Backup-/Host-down-Waechter bleiben auf healthchecks.io-Cloud | `ops/healthchecks/README.md` | --- diff --git a/docs/README.md b/docs/README.md index 805b4c9..72d4116 100644 --- a/docs/README.md +++ b/docs/README.md @@ -57,6 +57,7 @@ geloescht (Git-Historie ist das Archiv). Verbindliche Doku-Regeln: | `RENOVATE.md` | Self-hosted Renovate gegen Gitea | | `runbooks/komodo-bulk-deploy-dns.md` | Bulk-Deploy-Pulls scheitern an DNS bei AdGuard-Recreate | | `../ops/h-drive-nearline/README.md` | Windows-H:/ Nearline-Pull fuer kritische Restore-Artefakte | +| `../ops/healthchecks/README.md` | Self-hosted Healthchecks (interne Job-Heartbeats); externe Host-down-/Backup-Waechter bleiben Cloud | ## Nutzer- und Statusdoku diff --git a/docs/SECRETS_MAP.md b/docs/SECRETS_MAP.md index 56a27ba..f252762 100644 --- a/docs/SECRETS_MAP.md +++ b/docs/SECRETS_MAP.md @@ -50,6 +50,10 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb | Borg Repo | Borg-Passphrase fuer Restore-Tests und Notfallzugriff | `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` -> Host-Secret-Datei, nicht im Repo | aktiv | | Healthchecks Dead-Man's-Switch (Borg Pre-Hook) | Ping-/Capability-URL | `/mnt/user/appdata/secrets/healthchecks_borg_url` (chmod 600) **oder** ENV `HEALTHCHECKS_BORG_URL`/`HEALTHCHECKS_URL`, gelesen von `ops/borg-ui/scripts/pre-borg.sh`; URL ist eine Capability-URL -> wie Secret behandeln, nie ins Repo | aktiv nach Operator-Setup | | Healthchecks Dead-Man's-Switch (Nearline-Pull) | Ping-/Capability-URL | baerchen: ENV `HEALTHCHECKS_NEARLINE_URL` **oder** `%USERPROFILE%\.kallilab\healthchecks-nearline-url.txt`, gelesen von `ops/h-drive-nearline/pull-critical-backups.ps1`; URL ist eine Capability-URL -> wie Secret behandeln, nie ins Repo | aktiv nach Operator-Setup | +| Healthchecks self-hosted (`ops/healthchecks/`) | Django `SECRET_KEY` | Komodo Stack-ENV `${HEALTHCHECKS_SECRET_KEY}` (Image hat keinen `_FILE`-Support) | vorbereitet | +| Healthchecks self-hosted | DB Password | Komodo Stack-ENV `${HEALTHCHECKS_DB_PASSWORD}` (= Wert von `healthchecks_postgres_password.txt`) | vorbereitet | +| Healthchecks self-hosted | Superuser Login | Komodo Stack-ENV `${HEALTHCHECKS_SUPERUSER_EMAIL}`, `${HEALTHCHECKS_SUPERUSER_PASSWORD}` | vorbereitet | +| healthchecks-postgres | DB Password | `/mnt/user/appdata/secrets/healthchecks_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | vorbereitet | | Unraid Flash Backup | Boot-/Array-/Share-/Plugin-Konfiguration, ggf. Hashes/Keys/Templates | `/mnt/user/backups/borg/dumps/latest/unraid-flash-config.tar.gz`, via Borg/Hetzner gesichert | aktiv; wie Secret-Material behandeln | | Hermes Agent | Provider-Keys, Bot-Tokens, API-Server-Key | `/mnt/user/appdata/hermes-agent/data/.env` | VM-seitig offen | | Hermes Agent | SSH-Runner Private Key | `/mnt/user/appdata/secrets/hermes_runner_id_ed25519` -> `/root/.ssh/id_ed25519` | VM-seitig offen | @@ -118,6 +122,7 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb |-- dawarich_secret_key_base.txt |-- dawarich_metrics_password.txt |-- dawarich_grafana_ro_password.txt +|-- healthchecks_postgres_password.txt `-- vaultwarden_admin_token.txt ``` @@ -162,6 +167,7 @@ Einige Secrets liegen bewusst nur als Komodo Stack Environment Variables vor, we | `hermes-agent` | `HERMES_DASHBOARD_HOST` plus Provider-/API-/Home-Assistant-Tokens in Host-`.env` | Vaultwarden -> externe Notiz | Stack ist aktuell geparkt (Review 2026-07-25); ohne Werte bleibt der Stack deaktiviert, kein Schaden am Rest | | `glance` | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY`, `GLANCE_KOMODO_API_KEY`, `GLANCE_KOMODO_API_SECRET`, `GLANCE_GITEA_TOKEN`, `GLANCE_PAPERLESS_TOKEN`, `GLANCE_MEALIE_TOKEN`, `GLANCE_HA_TOKEN` | Provider-UIs (Immich, AdGuard, Speedtest-Tracker, Komodo, Gitea, Paperless, Mealie, Home Assistant) neu erzeugen | rebuildbar; Widgets bleiben leer bis Tokens neu erzeugt sind, kein kritischer Datentopf; `GLANCE_HA_TOKEN` muss zusaetzlich in `ops/glance/docker-compose.yml` durchgereicht werden | | `n8n` | `N8N_ENCRYPTION_KEY` | Host-Secret-Datei `/mnt/user/appdata/secrets/n8n_encryption_key.txt` -> Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | Bei Verlust aller Quellen: n8n startet, aber **alle gespeicherten Credentials sind unbrauchbar** (Re-Eingabe noetig: GMX IMAP, OpenAI, Gitea PAT). Workflows bleiben strukturell erhalten. | +| `healthchecks` | `HEALTHCHECKS_SECRET_KEY`, `HEALTHCHECKS_DB_PASSWORD`, `HEALTHCHECKS_SUPERUSER_EMAIL`, `HEALTHCHECKS_SUPERUSER_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | `SECRET_KEY`-Verlust invalidiert Sessions/Signaturen (neu setzen, alle Logins neu); DB-Passwort gemeinsam in Postgres + Stack-ENV zuruecksetzen; Superuser-Account ist via SUPERUSER-ENV reproduzierbar. Check-Metadaten gehen nur verloren, wenn auch das DB-Volume weg ist; die Pings selbst leben in den Jobs | ### Komodo-Sonderfall diff --git a/docs/SERVICE_CATALOG.md b/docs/SERVICE_CATALOG.md index e5ec1d6..97ddcd6 100644 --- a/docs/SERVICE_CATALOG.md +++ b/docs/SERVICE_CATALOG.md @@ -82,6 +82,8 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und | `hermes-gateway` | Hermes Agent Gateway/API intern | `ops/hermes-agent/docker-compose.yml` | intern `8642` auf `hermes_net` | SSH Runner (VM 192.168.178.143), LLM Provider, optional Home Assistant | `/mnt/user/appdata/hermes-agent/data`, SSH key path | Tier 3, Borg/Share | nein | NAS-Stack bleibt deaktiviert, solange die separate Hermes-VM/Runner-Seite nicht wiederhergestellt ist; kein Docker-Socket | | `hermes-dashboard` | Hermes Dashboard | `ops/hermes-agent/docker-compose.yml` | `https://hermes.kaleschke.info` via `${HERMES_DASHBOARD_HOST}` | `hermes-gateway`, Traefik + Authelia | shared read-only data mount | Tier 3, Borg/Share | ja + Authelia | Compose-Profil `dashboard`; aktuell VM-seitig offen, nicht Teil des NAS-Finalstarts | | `n8n` | Workflow-Automation; aktuell genutzt fuer Mail->LLM->Gitea-Issue (Inbox `Micha/mails`) | `apps/n8n/docker-compose.yml`, `apps/n8n/workflows/*.json` | `https://n8n.kaleschke.info` | Traefik (ohne pauschale Authelia, analog Komodo/Nextcloud), GMX IMAP, OpenAI API, Gitea API | `/mnt/user/appdata/n8n/data` (SQLite, Credentials, Workflows) | Tier 2, Borg + `n8n-data` (Credentials sind nur mit `N8N_ENCRYPTION_KEY` entschluesselbar) | ja, native Auth | Wegen Webhook-Endpunkten (`/webhook/*`) bewusst ohne `authelia@file`; eigene Login-/Owner-Auth bleibt Pflicht; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret, Verlust macht Credentials unbrauchbar. | +| `healthchecks` | Self-hosted Cron-/Job-Heartbeat-Monitor (Dead-Man's-Switch) fuer interne Jobs/Scripte | `ops/healthchecks/docker-compose.yml`, `ops/healthchecks/README.md` | `https://hc.kaleschke.info` | Traefik (native Auth, ohne pauschale Authelia), `healthchecks-postgres`, ntfy | keine kritische App-Persistenz (Check-Metadaten in der DB) | Tier 3, rebuildbar | ja, native Auth | Hub fuer INTERNE Checks; die externen Host-down-/Backup-Waechter (Borg-Pre-Hook, Nearline-Pull, Monitoring-Watchdog #8) bleiben bewusst auf healthchecks.io-Cloud (ein On-Host-Waechter kann Host-Down nicht melden). Ping-/API-Endpunkte ohne ForwardAuth (analog n8n). Stack-ENV: `HEALTHCHECKS_SECRET_KEY`, `HEALTHCHECKS_DB_PASSWORD`, `HEALTHCHECKS_SUPERUSER_EMAIL/PASSWORD`. Vorbereitet, noch nicht deployed | +| `healthchecks-postgres` | Healthchecks-Datenbank | `ops/healthchecks/docker-compose.yml` | intern | `healthchecks_internal` | `/mnt/user/appdata/healthchecks/postgres18`, `healthchecks_postgres_password.txt` | Check-Metadaten, rebuildbar | nein | interne DB; PostgreSQL 18; nie `frontend_net` | ## Smart Home diff --git a/ops/healthchecks/README.md b/ops/healthchecks/README.md new file mode 100644 index 0000000..684dd48 --- /dev/null +++ b/ops/healthchecks/README.md @@ -0,0 +1,111 @@ +Typ: Runbook · Stand: 2026-06-23 · Status: vorbereitet (noch nicht deployed) + +# Healthchecks (self-hosted) — Cron-/Job-Heartbeat-Monitor + +Self-gehostete Instanz von [Healthchecks](https://github.com/healthchecks/healthchecks) +als zentraler **Dead-Man's-Switch** fuer die internen Jobs und Scripte des +Homelabs: ein Job pingt beim erfolgreichen Lauf eine URL; bleibt der Ping aus, +alarmiert Healthchecks. Damit werden stille Job-Ausfaelle sichtbar, die Docker +("Up"), Prometheus-Blackbox (nur HTTP-Routen) und der Critical-Events-Watcher +(nur `die`/`oom`) nicht sehen. + +## Scope-Entscheidung (wichtig) + +Dieser Stack ist bewusst der Hub fuer **interne** Checks auf einem **laufenden** +Host — Frage: "Lief Job X heute?". Beispiele: + +- `services/posture-check/posture-check.sh` (stuendlich / pre-borg) +- `ops/restore-tests/run-restore-checks.sh` (Kadenz aus `schedule.md`) +- `ops/borg-ui/scripts/pre-backup-dumps.sh` (Dump-Erzeugung) +- `ops/borg-ui/scripts/gitea-bundle-mirror.sh` + +**Nicht hier:** die host-down-/backup-still-Waechter bleiben **extern** auf +healthchecks.io-Cloud (Free-Tier): + +| Check | Quelle | Endpoint | +|---|---|---| +| Borg-Pre-Hook | `ops/borg-ui/scripts/pre-borg.sh` | healthchecks.io-Cloud | +| baerchen Nearline-Pull | `ops/h-drive-nearline/pull-critical-backups.ps1` | healthchecks.io-Cloud | +| Monitoring-Watchdog (#8) | `monitoring/prometheus/alerts.yml` (geplant) | healthchecks.io-Cloud | + +**Begruendung:** Ein Waechter, der auf demselben Unraid-Host laeuft, den er +ueberwacht, kann einen Host-Ausfall nicht melden — er ist dann selbst tot, und +Stille ist nicht von "alles gut" unterscheidbar. Genau diese drei Checks +existieren fuer den Host-/Backup-Stillstand, deshalb muessen sie extern bleiben. +Die Skripte sind endpoint-agnostisch (siehe `docs/SECRETS_MAP.md`), eine +spaetere Umstellung waere reine URL-Frage — die Architektur-Empfehlung bleibt +aber extern. + +## Architektur + +- `healthchecks` — Web-UI + Ping-Listener, `frontend_net`, Traefik via + `https://hc.kaleschke.info`, **native Healthchecks-Auth ohne pauschale + Authelia** (analog n8n/Komodo): die Ping-Endpunkte `/ping/*` und die API + muessen ohne ForwardAuth erreichbar sein, sonst koennen Jobs nicht melden. +- `healthchecks-postgres` — dedizierte PostgreSQL 18, nur `healthchecks_internal` + (`internal: true`), nie `frontend_net`. +- SMTP ist bewusst **nicht** konfiguriert: Login laeuft ueber das + Superuser-Passwort, Benachrichtigung ueber die ntfy-Integration. SMTP (GMX) + kann spaeter additiv ergaenzt werden, falls E-Mail-Alerts gewuenscht sind. + +## Secrets (siehe `docs/SECRETS_MAP.md`) + +| Secret | Mechanik | +|---|---| +| `HEALTHCHECKS_SECRET_KEY` | Komodo Stack-ENV (Django Secret Key) | +| `HEALTHCHECKS_DB_PASSWORD` | Komodo Stack-ENV (gleicher Wert wie Datei-Secret) | +| `HEALTHCHECKS_SUPERUSER_EMAIL` | Komodo Stack-ENV (Login-Mail des Erst-Admins) | +| `HEALTHCHECKS_SUPERUSER_PASSWORD` | Komodo Stack-ENV (Login-Passwort des Erst-Admins) | +| `healthchecks_postgres_password.txt` | Datei-Secret `/mnt/user/appdata/secrets/` → `POSTGRES_PASSWORD_FILE` | + +`SECRET_KEY` und `DB_PASSWORD` unterstuetzt das Image nicht als `_FILE` → Stack-ENV +(Regel aus `docs/SECRETS_MAP.md`). Das Postgres-Passwort liegt zusaetzlich als +Datei-Secret vor; beide Werte muessen identisch sein. + +## Pre-Deploy (einmalig, Operator) + +1. Appdata anlegen: `/mnt/user/appdata/healthchecks/postgres18/`. +2. Datei-Secret erzeugen: `/mnt/user/appdata/secrets/healthchecks_postgres_password.txt` + (`chmod 600`), Wert = `HEALTHCHECKS_DB_PASSWORD`. +3. In Komodo die vier Stack-ENV-Variablen setzen (`SECRET_KEY` z. B. via + `python -c "import secrets;print(secrets.token_urlsafe(64))"`). + +## Deploy + Pflicht-Webhook + +1. Stack in Komodo aus Gitea `Micha/homelab-infra` anlegen, `webhook_enabled` an. +2. Gitea-Webhook auf die neue Stack-ID anlegen + (`http://komodo-core:9120/listener/github/stack//deploy`), + Branch-Filter `master`. **Pflicht fuer jeden neuen produktiven Stack** + (`docs/WORKFLOW.md`). +3. Test-Delivery ausloesen, `last_status`/Komodo-Deploy pruefen. + +## Post-Deploy + +1. Login auf `https://hc.kaleschke.info` mit Superuser-Mail/-Passwort. +2. Pro Job einen Check anlegen (Period + Grace passend zur Job-Kadenz, gern als + Cron-Ausdruck). Ping-URL kopieren. +3. **ntfy-Integration**: im Check unter Integrations ntfy hinzufuegen, + Server `https://ntfy.kaleschke.info`, Topic `homelab-alerts` (Problem-Alerts) + — konsistent mit der bestehenden Alert-Schiene. +4. Im Job am Ende des erfolgreichen Laufs pingen, z. B.: + ```bash + curl -fsS -m 10 --retry 3 "https://hc.kaleschke.info/ping/" >/dev/null || true + ``` + Optional `/start` am Anfang (misst Laufzeit) und `//fail` im Trap. + +## Rollback + +- Letzter stabiler Git-Stand: Stack existiert noch nicht — Rollback = Stack in + Komodo stoppen/destroyen, Repo-Pfad `ops/healthchecks/` per `git rm` + zuruecknehmen, Gitea-Webhook deaktivieren. +- Datenpfad `/mnt/user/appdata/healthchecks/postgres18` bleibt unberuehrt und ist + jederzeit loeschbar (reine Check-Metadaten, kein kritischer Datentopf — die + Pings selbst sind in den Jobs definiert). +- Secrets/ENV: bei Abbau die vier Stack-ENV + die Datei-Secret entfernen. + +## Image-Pinning + +`healthchecks/healthchecks:v4.2@sha256:6b5f59…` ist auf den am 2026-06-23 ueber +die Docker-Hub-API ermittelten Manifest-Digest gepinnt. Beim ersten Pull den +real laufenden Digest gegenpruefen und bei Abweichung im Repo nachziehen +(`docs/WORKFLOW.md` Abschnitt Image-Versionierung). diff --git a/ops/healthchecks/docker-compose.yml b/ops/healthchecks/docker-compose.yml new file mode 100644 index 0000000..cfb9186 --- /dev/null +++ b/ops/healthchecks/docker-compose.yml @@ -0,0 +1,124 @@ +name: healthchecks + +# Self-gehostetes Healthchecks (Dead-Man's-Switch / Cron-Heartbeat-Monitor). +# +# SCOPE (bewusst): Hub fuer die vielen INTERNEN Jobs/Scripte, die auf einem +# laufenden Host melden sollen "lief Job X heute?" (posture-check, +# restore-tests, pre-backup-dumps, gitea-bundle-mirror, ...). +# +# NICHT hier: die host-down-/backup-still-Waechter (Borg-Pre-Hook, +# baerchen-Nearline-Pull, Monitoring-Watchdog #8) bleiben bewusst EXTERN auf +# healthchecks.io-Cloud. Ein Waechter auf demselben Host kann einen +# Host-Ausfall nicht melden (er ist dann selbst tot). Siehe ops/healthchecks/README.md. + +services: + healthchecks: + image: healthchecks/healthchecks:v4.2@sha256:6b5f593d40994345053f05f86decfa9e17ab1e4422df2ae58abd032a7b14d8f6 + container_name: healthchecks + restart: unless-stopped + + # ntfy-Integration nutzt die oeffentliche Traefik-URL; Container-DNS loest + # ntfy.kaleschke.info sonst nicht (gleiches Muster wie mealie/komodo). + extra_hosts: + - "ntfy.kaleschke.info:192.168.178.58" + + environment: + TZ: Europe/Berlin + DEBUG: "False" + SITE_ROOT: https://hc.kaleschke.info + SITE_NAME: KalliLab Healthchecks + ALLOWED_HOSTS: hc.kaleschke.info,localhost + REGISTRATION_OPEN: "False" + + DB: postgres + DB_HOST: healthchecks-postgres + DB_PORT: "5432" + DB_NAME: healthchecks + DB_USER: healthchecks + DB_PASSWORD: ${HEALTHCHECKS_DB_PASSWORD} + + SECRET_KEY: ${HEALTHCHECKS_SECRET_KEY} + + # Erst-Admin wird beim Start angelegt/aktualisiert. Werte nur als + # Komodo-Stack-ENV, niemals im Repo. SMTP ist bewusst nicht konfiguriert + # (Login via Superuser-Passwort, Benachrichtigung via ntfy-Integration). + SUPERUSER_EMAIL: ${HEALTHCHECKS_SUPERUSER_EMAIL} + SUPERUSER_PASSWORD: ${HEALTHCHECKS_SUPERUSER_PASSWORD} + + networks: + - frontend_net + - healthchecks_internal + + depends_on: + healthchecks-postgres: + condition: service_healthy + + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/', timeout=5).status==200 else 1)\""] + interval: 60s + timeout: 10s + retries: 5 + start_period: 60s + + security_opt: + - no-new-privileges:true + + labels: + # Traefik mit nativer Healthchecks-Auth, bewusst OHNE pauschale + # authelia@file: die Ping-Endpunkte (/ping/*) und die API muessen ohne + # ForwardAuth erreichbar sein, sonst koennen Cron-Jobs nicht melden + # (gleiche Ausnahme-Logik wie n8n/Komodo). Dashboard ist durch den + # Healthchecks-eigenen Login geschuetzt. + - traefik.enable=true + - traefik.docker.network=frontend_net + - traefik.http.routers.healthchecks.rule=Host(`hc.kaleschke.info`) + - traefik.http.routers.healthchecks.entrypoints=websecure + - traefik.http.routers.healthchecks.tls=true + - traefik.http.routers.healthchecks.tls.certresolver=le + - traefik.http.routers.healthchecks.middlewares=secure-headers@file + - traefik.http.services.healthchecks.loadbalancer.server.port=8000 + + healthchecks-postgres: + image: postgres:18.4@sha256:29ee7bb30d804447dc9a91fd0d74322ae1dc3a4072cc6346f70a5ed6e783b565 + container_name: healthchecks-postgres + restart: unless-stopped + + environment: + TZ: Europe/Berlin + POSTGRES_USER: healthchecks + POSTGRES_DB: healthchecks + POSTGRES_PASSWORD_FILE: /run/secrets/healthchecks_postgres_password + PGDATA: /var/lib/postgresql/18/docker + + volumes: + - /mnt/user/appdata/healthchecks/postgres18:/var/lib/postgresql + + networks: + - healthchecks_internal + + secrets: + - healthchecks_postgres_password + + expose: + - "5432" + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] + interval: 10s + timeout: 10s + retries: 5 + start_period: 30s + + security_opt: + - no-new-privileges:true + +networks: + frontend_net: + external: true + healthchecks_internal: + driver: bridge + internal: true + +secrets: + healthchecks_postgres_password: + file: /mnt/user/appdata/secrets/healthchecks_postgres_password.txt diff --git a/ops/policy-checks/exceptions.json b/ops/policy-checks/exceptions.json index a73000b..7098c6d 100644 --- a/ops/policy-checks/exceptions.json +++ b/ops/policy-checks/exceptions.json @@ -2,6 +2,7 @@ "middleware_exempt_identities": [ "authelia", "gitea", + "healthchecks", "immich-server", "immich_server", "komodo-core",