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 <noreply@anthropic.com>
This commit is contained in:
@@ -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/<stack-id>/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/<uuid>" >/dev/null || true
|
||||
```
|
||||
Optional `/start` am Anfang (misst Laufzeit) und `/<uuid>/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).
|
||||
@@ -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
|
||||
@@ -2,6 +2,7 @@
|
||||
"middleware_exempt_identities": [
|
||||
"authelia",
|
||||
"gitea",
|
||||
"healthchecks",
|
||||
"immich-server",
|
||||
"immich_server",
|
||||
"komodo-core",
|
||||
|
||||
Reference in New Issue
Block a user