Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f77a69a0b2 | |||
| f73cf48e41 | |||
| eea2697ca1 | |||
| a3d77d7529 | |||
| 02a50e1a58 | |||
| 267e76059a | |||
| 9d4fee02ca | |||
| 24ebcaa3c7 | |||
| 45bae13aa0 | |||
| ff5991cec8 | |||
| 5b6e7b8b66 | |||
| 5cb401797d | |||
| 1d0cba92bd | |||
| 9353a9fc44 | |||
| d50b11784d | |||
| 09eeac51e1 | |||
| 565940b9ef | |||
| b6bbca43ad | |||
| 388e57e385 | |||
| 0c2bb8484a | |||
| a7797fd02e | |||
| bac927bbcc | |||
| add8b71ea9 | |||
| e21e89e51b | |||
| 4e4684b616 | |||
| 84030956ac | |||
| 17fe8073bb | |||
| 9f32ba72c1 | |||
| e9a7f79025 | |||
| 43727151df | |||
| 66ee10cb55 | |||
| ab68900216 | |||
| 8f56c6edcd | |||
| 8e400fb3c3 | |||
| cd650b19ac | |||
| af231dd4e8 | |||
| 428223d2e4 | |||
| b6d3ed4832 | |||
| 9e7bebbd3c | |||
| b7cbbe51de | |||
| 71ac18b21c | |||
| 90f270be96 | |||
| e28f8dabec | |||
| edfec5b66d | |||
| 59bec9ac77 | |||
| 9f86da708a | |||
| d6170211c4 | |||
| fb681086f3 | |||
| 5b101f3b3d | |||
| 669efbd57e | |||
| 2dd5590a2a | |||
| 175cd6951f | |||
| aeb7573b03 | |||
| 215f44b962 | |||
| 6ce625f77a | |||
| c3c8060ddf | |||
| 29eaf8001f | |||
| db7dc3f2af | |||
| c748236886 | |||
| 8aa850df40 | |||
| 2c4854f628 | |||
| b7050812d4 | |||
| c95fa601f0 | |||
| 0c308ff352 | |||
| 53216e50c1 | |||
| b7dfdad621 | |||
| 61625a7a1c | |||
| 6e28ea94d2 | |||
| 58eb53a6a8 | |||
| d345d770c2 | |||
| 2e136d9060 | |||
| 6ca829ec45 | |||
| ef3b546d30 | |||
| 6f684fb4e3 | |||
| 0adddb6533 | |||
| 162421e537 | |||
| bf30240217 | |||
| 5f7940aa01 | |||
| a5add937f8 | |||
| 5ada1ad153 | |||
| ead7e1e17d | |||
| 14e9c0963d | |||
| 23262cd7b9 | |||
| 878ad2d5f1 | |||
| 12a87ad342 | |||
| 0e7e639df4 | |||
| 18df2d155d | |||
| fa177155e6 | |||
| 11c863c8aa | |||
| dd9f677779 | |||
| a9e62ee8e5 | |||
| 4e7d313a20 | |||
| 57ea7507a7 | |||
| 1e3e019f28 | |||
| 54fd0c3347 | |||
| d7e1eb33ba | |||
| 008ab9bc4a | |||
| 7ff7284f6b | |||
| d20b687211 | |||
| 16416d964f | |||
| 2cc39c73f6 | |||
| d351b1cac8 | |||
| df4d335907 | |||
| 7161da00b3 | |||
| aded9a9cbc | |||
| 5cc0a4dadb | |||
| 84020346bc | |||
| 1dc1c1ef17 |
@@ -27,3 +27,4 @@
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
*.tmp
|
*.tmp
|
||||||
*.log
|
*.log
|
||||||
|
.serena/
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ Standard-Workflow:
|
|||||||
7. Komodo-Deploy/Runtime pruefen
|
7. Komodo-Deploy/Runtime pruefen
|
||||||
8. Dokumentation nachziehen
|
8. Dokumentation nachziehen
|
||||||
|
|
||||||
|
Neue produktive Komodo-Stacks aus `Micha/homelab-infra` brauchen verpflichtend einen aktiven Gitea->Komodo-Webhook auf die aktuelle Stack-ID. Ausnahmen muessen im selben Aenderungsblock dokumentiert werden.
|
||||||
|
|
||||||
Wenn Drift vermutet wird, nicht raten. Erst die Pflichtmatrix in `docs/GITOPS_DRIFT_RUNBOOK.md` abarbeiten.
|
Wenn Drift vermutet wird, nicht raten. Erst die Pflichtmatrix in `docs/GITOPS_DRIFT_RUNBOOK.md` abarbeiten.
|
||||||
|
|
||||||
## Sicherheitsregeln
|
## Sicherheitsregeln
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
> **Single Source of Truth** für Docker-Netzwerkarchitektur, Sicherheitsregeln, Zielbild und Migration des Kallilabcore-Homelabs.
|
> **Single Source of Truth** für Docker-Netzwerkarchitektur, Sicherheitsregeln, Zielbild und Migration des Kallilabcore-Homelabs.
|
||||||
> **Arbeitsregel für KI-Assistenten:** Dieses Dokument immer zuerst lesen, bevor Fragen zu Containern, Netzwerken, Traefik, Tailscale, Migration oder Security beantwortet werden.
|
> **Arbeitsregel für KI-Assistenten:** Dieses Dokument immer zuerst lesen, bevor Fragen zu Containern, Netzwerken, Traefik, Tailscale, Migration oder Security beantwortet werden.
|
||||||
|
|
||||||
**Stand:** 2026-04-17 | **Aktueller Schwerpunkt:** GitOps / Doku-Synchronisierung / Reproduzierbare Deployments
|
**Stand:** 2026-05-23 | **Aktueller Schwerpunkt:** GitOps / Doku-Synchronisierung / Reproduzierbare Deployments
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -47,20 +47,20 @@
|
|||||||
## 2. Architektur-Prinzipien
|
## 2. Architektur-Prinzipien
|
||||||
|
|
||||||
### P1 — Traefik ist der einzige öffentliche HTTP(S)-Einstiegspunkt
|
### P1 — Traefik ist der einzige öffentliche HTTP(S)-Einstiegspunkt
|
||||||
Kein Webdienst veröffentlicht finale direkte Host-Ports außer `traefik` selbst. Begründete Ausnahmen: `gitea`-SSH (Port 222), `AdGuard Home` (Port 53/DNS + 8082/Admin), `Tailscale`, `Plex-Media-Server` und `influxdb3-core` Port 8181 als LAN-only Writer-Endpunkt fuer Home Assistant.
|
Kein Webdienst veröffentlicht finale direkte Host-Ports außer `traefik` selbst. Begründete Ausnahmen: `gitea`-SSH (Port 222), `AdGuard Home` (Port 53/DNS direkt; Admin 8082 nur auf Tailscale-IP `100.80.98.33`), `Tailscale`, `Plex-Media-Server` und `monitoring-influxdb3-core` Port 8181 als LAN-only Writer-Endpunkt fuer Home Assistant.
|
||||||
|
|
||||||
### P2 — Das Setup bleibt bewusst einfach: `frontend_net` + `backend_net` + app-interne Netze
|
### P2 — Das Setup bleibt bewusst einfach: `frontend_net` + `backend_net` + app-interne Netze
|
||||||
- `frontend_net` = Proxy-/Web-Netz
|
- `frontend_net` = Proxy-/Web-Netz
|
||||||
- `backend_net` = intern für DB/Cache/App-Kommunikation
|
- `backend_net` = intern für DB/Cache/App-Kommunikation
|
||||||
- zusätzliche Netze nur app-intern, wenn technisch nötig (`mealie_mealie_internal`, `immich_default`, `dns_net`)
|
- zusätzliche Netze nur app-intern, wenn technisch nötig (`mealie_internal`, `immich_default`, `dns_net`)
|
||||||
|
|
||||||
Es gibt **keine künstlichen globalen Zusatznetze** wie `admin_net`, `monitoring_net` oder `media_net`.
|
Es gibt **keine künstlichen globalen Zusatznetze** wie `admin_net` oder `media_net`. `monitoring_net` ist die dokumentierte Ausnahme fuer den zentralen Observability-Stack.
|
||||||
|
|
||||||
### P3 — Datenbanken gehören nie ins `frontend_net`
|
### P3 — Datenbanken gehören nie ins `frontend_net`
|
||||||
Postgres, Redis und ähnliche Dienste laufen ausschließlich in `backend_net` oder einem eigenen internen Compose-Netz.
|
Postgres, Redis und ähnliche Dienste laufen ausschließlich in `backend_net` oder einem eigenen internen Compose-Netz.
|
||||||
|
|
||||||
### P4 — Admin-UIs sind nicht öffentlich
|
### P4 — Admin-UIs sind nicht öffentlich
|
||||||
filebrowser, scrutiny, UptimeKuma, code-server, Traefik-Dashboard, backrest und borg-ui sind standardmaessig **Tailscale-only** oder hinter Traefik **mit zentraler Middleware** abgesichert. `Komodo` ist die dokumentierte Ausnahme und bleibt bewusst bei nativer Authentifizierung ohne pauschal vorgeschaltete ForwardAuth-Middleware.
|
filebrowser, scrutiny, code-server, Traefik-Dashboard und borg-ui sind standardmaessig **Tailscale-only** oder hinter Traefik **mit zentraler Middleware** abgesichert. `Komodo` ist die dokumentierte Ausnahme und bleibt bewusst bei nativer Authentifizierung ohne pauschal vorgeschaltete ForwardAuth-Middleware.
|
||||||
|
|
||||||
### P5 — Compose-first
|
### P5 — Compose-first
|
||||||
Alle produktiven Container werden als Compose verwaltet. Bestehende Dockerman-/Ad-hoc-Container werden schrittweise migriert.
|
Alle produktiven Container werden als Compose verwaltet. Bestehende Dockerman-/Ad-hoc-Container werden schrittweise migriert.
|
||||||
@@ -87,11 +87,12 @@ Jeder produktive Container nutzt `restart: unless-stopped`, außer eine Ausnahme
|
|||||||
| `frontend_net` | bridge, external | einziges Traefik-/Web-Netz | Standard |
|
| `frontend_net` | bridge, external | einziges Traefik-/Web-Netz | Standard |
|
||||||
| `backend_net` | bridge, `internal: true` | interne App-/DB-/Cache-Kommunikation | Standard |
|
| `backend_net` | bridge, `internal: true` | interne App-/DB-/Cache-Kommunikation | Standard |
|
||||||
| `dns_net` | bridge | Resolver-Schicht: AdGuard Home + Unbound | bleibt |
|
| `dns_net` | bridge | Resolver-Schicht: AdGuard Home + Unbound | bleibt |
|
||||||
| `mealie_mealie_internal` | bridge, `internal: true` | internes Netz nur für `mealie` + `mealie-postgres` | ✅ umgesetzt |
|
| `mealie_internal` | bridge, `internal: true` | internes Netz nur für `mealie` + `mealie-postgres` | ✅ umgesetzt |
|
||||||
| `immich_default` | Compose-intern, `internal: true` | internes Immich-Netz | ✅ umgesetzt |
|
| `immich_default` | Compose-intern, `internal: true` | internes Immich-Netz | ✅ umgesetzt |
|
||||||
| `nextcloud_internal` | bridge, `internal: true` | internes Netz nur fuer `nextcloud` + `nextcloud-postgres` + `nextcloud-redis` | ✅ vorbereitet |
|
| `nextcloud_internal` | bridge, `internal: true` | internes Netz nur fuer `nextcloud` + `nextcloud-postgres` + `nextcloud-redis` | ✅ vorbereitet |
|
||||||
| `grafana_influx_internal` | Compose-intern, `internal: true` | interne Grafana-zu-InfluxDB-Kommunikation | ✅ umgesetzt |
|
| `monitoring_net` | Compose-intern, bridge | zentraler Observability-Stack fuer Prometheus, Loki, Grafana, Promtail, Exporter und InfluxDB | Zielzustand |
|
||||||
| `grafana_influx_lan` | Compose-intern, bridge | nicht-oeffentliches Zusatznetz nur fuer Docker Host-Port-Publishing von InfluxDB 8181 | ✅ umgesetzt |
|
| `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 |
|
||||||
| `host` | host | nur für echte Sonderfälle | begründet |
|
| `host` | host | nur für echte Sonderfälle | begründet |
|
||||||
|
|
||||||
### 3.2 Finales Diagramm (vereinfacht)
|
### 3.2 Finales Diagramm (vereinfacht)
|
||||||
@@ -103,7 +104,7 @@ traefik (80/443)
|
|||||||
│
|
│
|
||||||
└── frontend_net
|
└── frontend_net
|
||||||
├── öffentliche Apps (vaultwarden, mealie, paperless, immich, gitea, ntfy, mail-archiver, nextcloud)
|
├── öffentliche Apps (vaultwarden, mealie, paperless, immich, gitea, ntfy, mail-archiver, nextcloud)
|
||||||
├── geschützte UIs mit Middleware (homepage, paperless-gpt, uptime-kuma, filebrowser, scrutiny, code-server, backrest, borg-ui, glances, speedtest, bentopdf, grafana)
|
├── geschützte UIs mit Middleware (glance, paperless-gpt, filebrowser, scrutiny, code-server, borg-ui, glances, speedtest, bentopdf, monitoring-grafana)
|
||||||
├── Admin-UI mit nativer Auth (komodo)
|
├── Admin-UI mit nativer Auth (komodo)
|
||||||
└── Dienste mit Internetbedarf ohne öffentliche UI (ddns-updater)
|
└── Dienste mit Internetbedarf ohne öffentliche UI (ddns-updater)
|
||||||
|
|
||||||
@@ -118,11 +119,11 @@ dns_net
|
|||||||
└── unbound
|
└── unbound
|
||||||
|
|
||||||
App-interne Netze
|
App-interne Netze
|
||||||
├── mealie_mealie_internal (internal: true) ✅
|
├── mealie_internal (internal: true) ✅
|
||||||
├── immich_default (internal: true) ✅
|
├── immich_default (internal: true) ✅
|
||||||
├── nextcloud_internal (internal: true) ✅
|
├── nextcloud_internal (internal: true) ✅
|
||||||
├── grafana_influx_internal (internal: true)
|
├── monitoring_net (zentraler Observability-Stack)
|
||||||
└── grafana_influx_lan (Bridge fuer LAN-Port-Publishing, keine Traefik-Route)
|
└── monitoring_influx_lan (Bridge fuer LAN-Port-Publishing, keine Traefik-Route)
|
||||||
|
|
||||||
Host-Sonderfälle
|
Host-Sonderfälle
|
||||||
├── tailscale
|
├── tailscale
|
||||||
@@ -149,22 +150,20 @@ Diese Dienste sind über echte `*.kaleschke.info`-Domains erreichbar:
|
|||||||
Diese Dienste sind **keine Public Apps**:
|
Diese Dienste sind **keine Public Apps**:
|
||||||
|
|
||||||
- `Komodo` — komodo.kaleschke.info (Traefik, aber bewusst ohne zentrale Middleware; native Auth bleibt aktiv)
|
- `Komodo` — komodo.kaleschke.info (Traefik, aber bewusst ohne zentrale Middleware; native Auth bleibt aktiv)
|
||||||
- `UptimeKuma` — uptime.kaleschke.info (Middleware)
|
|
||||||
- `filebrowser` — files.kaleschke.info (Middleware)
|
- `filebrowser` — files.kaleschke.info (Middleware)
|
||||||
- `scrutiny` — scrutiny.kaleschke.info (Middleware)
|
- `scrutiny` — scrutiny.kaleschke.info (Middleware)
|
||||||
- `code-server` — Traefik + Middleware
|
- `code-server` — Traefik + Middleware
|
||||||
- `backrest` — Traefik + Middleware
|
|
||||||
- `borg-ui` — borg.kaleschke.info (Middleware)
|
- `borg-ui` — borg.kaleschke.info (Middleware)
|
||||||
- `homepage` — home.kaleschke.info (Middleware)
|
- `glance` — glance.kaleschke.info (Middleware)
|
||||||
- `paperless-gpt` — paperless-gpt.kaleschke.info (Middleware)
|
- `paperless-gpt` — paperless-gpt.kaleschke.info (Middleware)
|
||||||
- `mail-archiver` — mail.kaleschke.info (Middleware + App-Auth)
|
- `mail-archiver` — mail.kaleschke.info (Middleware + App-Auth)
|
||||||
- `glances` — glances.kaleschke.info (Middleware)
|
- `glances` — glances.kaleschke.info (Middleware)
|
||||||
- `speedtest-tracker` — speedtest.kaleschke.info (Middleware)
|
- `speedtest-tracker` — speedtest.kaleschke.info (Middleware)
|
||||||
- `bentopdf` — pdf.kaleschke.info (Middleware)
|
- `bentopdf` — pdf.kaleschke.info (Middleware)
|
||||||
- `grafana` — grafana.kaleschke.info (Middleware)
|
- `monitoring-grafana` — monitoring.kaleschke.info (Middleware)
|
||||||
- `hermes-dashboard` — hermes.kaleschke.info (Middleware)
|
- `hermes-dashboard` — hermes.kaleschke.info (Middleware)
|
||||||
- `Traefik-Dashboard`
|
- `Traefik-Dashboard`
|
||||||
- `AdGuard Home` — Port 8082 direkt auf die Admin-UI (`80` im Container), kein Traefik, nur LAN-Zugang
|
- `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
|
||||||
|
|
||||||
### 4.3 Regel
|
### 4.3 Regel
|
||||||
Wenn ein Dienst im `frontend_net` hängt, heißt das **nicht automatisch öffentlich**. Admin-Dienste dürfen im `frontend_net` liegen, wenn:
|
Wenn ein Dienst im `frontend_net` hängt, heißt das **nicht automatisch öffentlich**. Admin-Dienste dürfen im `frontend_net` liegen, wenn:
|
||||||
@@ -236,12 +235,10 @@ Legende Status:
|
|||||||
| Container | Status | Soll-Netz(e) | Finaler Zugang | Finaler Sollzustand | Offene Punkte |
|
| Container | Status | Soll-Netz(e) | Finaler Zugang | Finaler Sollzustand | Offene Punkte |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `traefik` | ✅ | `frontend_net`, `backend_net` | öffentlich 80/443 | zentraler Ingress, Service-Routing via Docker-Labels | — |
|
| `traefik` | ✅ | `frontend_net`, `backend_net` | öffentlich 80/443 | zentraler Ingress, Service-Routing via Docker-Labels | — |
|
||||||
| `AdGuard Home` | ✅ | `dns_net` (172.23.0.3), `frontend_net` | Port 53 DNS direkt, Port 8082 Admin (LAN) | DNS-Server + Upstream zu unbound; kein Traefik (DNS-Sonderfall) | Admin-Port per Traefik + Middleware absichern (Block F) |
|
| `AdGuard Home` | ✅ | `dns_net` (172.23.0.3), `frontend_net` | Port 53 DNS direkt, Port 8082 Admin nur auf Tailscale-IP `100.80.98.33` | DNS-Server + Upstream zu unbound; kein Traefik fuer Admin-UI | Admin-Port bleibt bewusst ohne Traefik/2FA, aber nicht mehr auf allen LAN-Interfaces |
|
||||||
| `unbound` | ✅ | `dns_net` | intern | Upstream-Resolver für AdGuard, isoliert | — |
|
| `unbound` | ✅ | `dns_net` | intern | Upstream-Resolver für AdGuard, isoliert | — |
|
||||||
| `ddns-updater` | ✅ | `frontend_net` | intern | Cloudflare DNS API; bleibt in `frontend_net` | Dokumentierte Ausnahme |
|
| `ddns-updater` | ✅ | `frontend_net` | intern | Cloudflare DNS API; bleibt in `frontend_net` | Dokumentierte Ausnahme |
|
||||||
| `tailscale` | ✅ | `host` | VPN-Zugang | Git-Stack (`host-services/tailscale/`) | nutzt `NET_ADMIN`, `NET_RAW` und `/dev/net/tun` als dokumentierte VPN-Ausnahme |
|
| `tailscale` | ✅ | `host` | VPN-Zugang | Git-Stack (`host-services/tailscale/`) | nutzt `NET_ADMIN`, `NET_RAW` und `/dev/net/tun` als dokumentierte VPN-Ausnahme |
|
||||||
| `backrest` | ✅ | `frontend_net`, `backend_net` | Traefik + Middleware | produktiver Backup-Admin-Dienst | breite Mounts bewusst dokumentieren |
|
|
||||||
| `homepage` | ✅ | `frontend_net` | Traefik + Middleware | geschuetztes Start-Dashboard via `home.kaleschke.info` | — |
|
|
||||||
|
|
||||||
### 7.2 Sicherheit / Identity
|
### 7.2 Sicherheit / Identity
|
||||||
|
|
||||||
@@ -256,7 +253,7 @@ Legende Status:
|
|||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `postgresql17` | ✅ | `backend_net` | intern | kein Host-Port, `POSTGRES_PASSWORD_FILE` | — |
|
| `postgresql17` | ✅ | `backend_net` | intern | kein Host-Port, `POSTGRES_PASSWORD_FILE` | — |
|
||||||
| `Redis` | ✅ | `backend_net` | intern | intern-only Cache | optional named volume |
|
| `Redis` | ✅ | `backend_net` | intern | intern-only Cache | optional named volume |
|
||||||
| `mealie-postgres` | ✅ | `mealie_mealie_internal` | intern | isoliert, nie `frontend_net` | — |
|
| `mealie-postgres` | ✅ | `mealie_internal` | intern | isoliert, nie `frontend_net` | — |
|
||||||
| `immich_postgres` | ✅ | `immich_default` | intern | intern-only | — |
|
| `immich_postgres` | ✅ | `immich_default` | intern | intern-only | — |
|
||||||
| `immich_redis` | ⏳ | `immich_default` | intern | intern-only | anonymes Volume → named volume |
|
| `immich_redis` | ⏳ | `immich_default` | intern | intern-only | anonymes Volume → named volume |
|
||||||
| `nextcloud-postgres` | ✅ | `nextcloud_internal` | intern | app-eigene Nextcloud-Datenbank mit `_FILE`-Secret | — |
|
| `nextcloud-postgres` | ✅ | `nextcloud_internal` | intern | app-eigene Nextcloud-Datenbank mit `_FILE`-Secret | — |
|
||||||
@@ -268,12 +265,13 @@ Legende Status:
|
|||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `paperless-ngx` | ✅ | `frontend_net`, `backend_net` | Traefik | aktiv via `paperless.kaleschke.info` | — |
|
| `paperless-ngx` | ✅ | `frontend_net`, `backend_net` | Traefik | aktiv via `paperless.kaleschke.info` | — |
|
||||||
| `mail-archiver` | ✅ | `frontend_net`, `backend_net` | Traefik + Middleware | aktiv via `mail.kaleschke.info`; IMAP-Abruf + DB-Zugang; App-eigene Auth bleibt zusaetzliche Schutzschicht | — |
|
| `mail-archiver` | ✅ | `frontend_net`, `backend_net` | Traefik + Middleware | aktiv via `mail.kaleschke.info`; IMAP-Abruf + DB-Zugang; App-eigene Auth bleibt zusaetzliche Schutzschicht | — |
|
||||||
| `mealie` | ✅ | `frontend_net`, `mealie_mealie_internal` | Traefik | sauber getrennte App/DB-Struktur | — |
|
| `mealie` | ✅ | `frontend_net`, `mealie_internal` | Traefik | sauber getrennte App/DB-Struktur | — |
|
||||||
| `ntfy` | ✅ | `frontend_net` | Traefik | aktiv via `ntfy.kaleschke.info`, Git-Stack | — |
|
| `ntfy` | ✅ | `frontend_net` | Traefik | aktiv via `ntfy.kaleschke.info`, Git-Stack | — |
|
||||||
| `gitea` | ✅ | `frontend_net` | Traefik + SSH-Port 222 | Web via Traefik, SSH direkt gebunden | — |
|
| `gitea` | ✅ | `frontend_net` | Traefik + SSH-Port 222 | Web via Traefik, SSH direkt gebunden | — |
|
||||||
| `immich_server` | ✅ | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | — |
|
| `immich_server` | ✅ | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | — |
|
||||||
| `immich_machine_learning` | ✅ | `immich_default` | intern | bleibt intern | — |
|
| `immich_machine_learning` | ✅ | `immich_default` | intern | bleibt intern | — |
|
||||||
| `nextcloud` | ✅ | `frontend_net`, `nextcloud_internal` | Traefik | aktiv via `cloud.kaleschke.info`, nativer Nextcloud-Login, WebDAV/CardDAV faehig | CalDAV/CardDAV-Redirect via Traefik-Labels |
|
| `nextcloud` | ✅ | `frontend_net`, `nextcloud_internal` | Traefik | aktiv via `cloud.kaleschke.info`, nativer Nextcloud-Login, WebDAV/CardDAV faehig | CalDAV/CardDAV-Redirect via Traefik-Labels |
|
||||||
|
| `plex` | ✅ | `host` | Plex native / Host-Netz | Compose-Stack unter `host-services/plex/`; Host-Netz bleibt fuer Discovery / Plex GDM dokumentierte Ausnahme | — |
|
||||||
|
|
||||||
### 7.5 Admin / Operations
|
### 7.5 Admin / Operations
|
||||||
|
|
||||||
@@ -282,7 +280,7 @@ Legende Status:
|
|||||||
| `komodo` | ✅ | `frontend_net` | Traefik, native Auth | primaerer GitOps-Stack-Manager | bewusste Ausnahme: keine pauschale ForwardAuth-Middleware vor UI/API/Webhooks/Periphery |
|
| `komodo` | ✅ | `frontend_net` | Traefik, native Auth | primaerer GitOps-Stack-Manager | bewusste Ausnahme: keine pauschale ForwardAuth-Middleware vor UI/API/Webhooks/Periphery |
|
||||||
| `code-server` | ✅ | `frontend_net` | Traefik + Middleware | `PASSWORD_FILE` aktiv | — |
|
| `code-server` | ✅ | `frontend_net` | Traefik + Middleware | `PASSWORD_FILE` aktiv | — |
|
||||||
| `PortainerCE` | ❌ entfernt | - | - | 2026-03-29 abgeschaltet | historisch; nicht mehr deployen |
|
| `PortainerCE` | ❌ entfernt | - | - | 2026-03-29 abgeschaltet | historisch; nicht mehr deployen |
|
||||||
| `filebrowser` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `files.kaleschke.info` | Mounts einschränken (Block F) |
|
| `filebrowser` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `files.kaleschke.info` | Appdata-Breitmount entfernt; nur Documents/Photos/Projekte plus eigener App-State |
|
||||||
| `borg-ui` | ✅ | `frontend_net` | Traefik + Middleware | produktiver Borg-/Restore-Dienst; `/local/secrets` ist bewusst Teil des Restore-Scopes | BorgBase-Repo und Key laufend pflegen |
|
| `borg-ui` | ✅ | `frontend_net` | Traefik + Middleware | produktiver Borg-/Restore-Dienst; `/local/secrets` ist bewusst Teil des Restore-Scopes | BorgBase-Repo und Key laufend pflegen |
|
||||||
| `paperless-gpt` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `paperless-gpt.kaleschke.info` | — |
|
| `paperless-gpt` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `paperless-gpt.kaleschke.info` | — |
|
||||||
| `bentopdf` | ✅ vorbereitet | `frontend_net` | Traefik + Middleware | PDF-Tooling via `pdf.kaleschke.info`; browserseitige Verarbeitung, COOP/COEP fuer Office-Konvertierung | Deploy und fachliche Abnahme offen |
|
| `bentopdf` | ✅ vorbereitet | `frontend_net` | Traefik + Middleware | PDF-Tooling via `pdf.kaleschke.info`; browserseitige Verarbeitung, COOP/COEP fuer Office-Konvertierung | Deploy und fachliche Abnahme offen |
|
||||||
@@ -292,18 +290,21 @@ Legende Status:
|
|||||||
|
|
||||||
| Container | Status | Soll-Netz(e) | Finaler Zugang | Finaler Sollzustand | Offene Punkte |
|
| Container | Status | Soll-Netz(e) | Finaler Zugang | Finaler Sollzustand | Offene Punkte |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `UptimeKuma` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `uptime.kaleschke.info` | — |
|
| `glance` | ✅ | `frontend_net`, `glance_socket_net` | Traefik + Middleware | einziges Homelab-Dashboard via `glance.kaleschke.info`; Docker-Status nur ueber internen Socket-Proxy | — |
|
||||||
| `glances` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `glances.kaleschke.info` | — |
|
| `glances` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `glances.kaleschke.info` | — |
|
||||||
| `scrutiny` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `scrutiny.kaleschke.info`, Git-Stack | `privileged` später prüfen |
|
| `scrutiny` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `scrutiny.kaleschke.info`, Git-Stack | `privileged` später prüfen |
|
||||||
| `speedtest-tracker` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `speedtest.kaleschke.info` | — |
|
| `speedtest-tracker` | ✅ | `frontend_net` | Traefik + Middleware | aktiv via `speedtest.kaleschke.info` | — |
|
||||||
| `grafana` | ✅ | `frontend_net`, `grafana_influx_internal` | Traefik + Middleware | aktiv via `grafana.kaleschke.info`, InfluxDB-Datenquelle provisioniert; laeuft aktuell als `user: "0"` wegen Host-Appdata-Permissions | Wetter-/HA-Dashboard aufbauen; UID/GID-Hardening spaeter pruefen |
|
| `monitoring-grafana` | ✅ | `frontend_net`, `monitoring_net` | Traefik + Middleware | zentrale UI via `monitoring.kaleschke.info`; Datasources fuer Prometheus, Loki und InfluxDB | — |
|
||||||
| `influxdb3-core` | ✅ | `grafana_influx_internal`, `grafana_influx_lan` + LAN-Bind | LAN-Port nur fuer interne Writer | InfluxDB 3 Core fuer Metriken; keine Traefik-/Public-Freigabe; Port 8181 nur via `INFLUXDB_BIND_IP`; laeuft aktuell als `user: "0"` wegen Host-Appdata-Permissions | HA-Write-Token und Sensor-Export finalisieren; UID/GID-Hardening spaeter pruefen |
|
| `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 |
|
||||||
|
| `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
|
### 7.7 Noch offene Sonderfälle
|
||||||
|
|
||||||
| Container | Status | Ziel |
|
| Container | Status | Ziel |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `Plex-Media-Server` | ⏳ Dockerman | Compose-Migration, `host`-Netz bleibt (Discovery) |
|
| — | — | Plex ist nicht mehr offen: der Dienst ist als Repo-Compose-Stack unter `host-services/plex/` dokumentiert; `host`-Netz bleibt als Discovery-Ausnahme. |
|
||||||
|
|
||||||
### 7.8 Entfernte Container
|
### 7.8 Entfernte Container
|
||||||
|
|
||||||
@@ -319,11 +320,15 @@ Legende Status:
|
|||||||
| `dashdot` | 2026-03-28 | nicht mehr aktiv |
|
| `dashdot` | 2026-03-28 | nicht mehr aktiv |
|
||||||
| `netdata` | 2026-03-28 | nicht mehr aktiv |
|
| `netdata` | 2026-03-28 | nicht mehr aktiv |
|
||||||
| `netalertx` | 2026-03-28 | nicht mehr aktiv |
|
| `netalertx` | 2026-03-28 | nicht mehr aktiv |
|
||||||
| `luckyBackup` | 2026-03-28 | nicht mehr aktiv; Backup via backrest |
|
| `luckyBackup` | 2026-03-28 | nicht mehr aktiv; Backup via Borg |
|
||||||
|
| `backrest` | 2026-05-15 | entfernt; Borg ist die alleinige Backup-Technologie, WD MyBookLive ist kein Backup-Ziel mehr |
|
||||||
| `Stash` | 2026-03-28 | nicht mehr aktiv |
|
| `Stash` | 2026-03-28 | nicht mehr aktiv |
|
||||||
| `PortainerCE` | 2026-03-29 | abgeschaltet; Komodo ist alleiniger Stack-Manager |
|
| `PortainerCE` | 2026-03-29 | abgeschaltet; Komodo ist alleiniger Stack-Manager |
|
||||||
| `beszel` | nicht dokumentiert | bereits entfernt; nicht mehr Teil des Zielbilds |
|
| `beszel` | nicht dokumentiert | bereits entfernt; nicht mehr Teil des Zielbilds |
|
||||||
| `beszel-agent` | nicht dokumentiert | bereits entfernt; nicht mehr Teil des Zielbilds |
|
| `beszel-agent` | nicht dokumentiert | bereits entfernt; nicht mehr Teil des Zielbilds |
|
||||||
|
| `jellyfin` | 2026-05-25 | doppelter Medienserver neben Plex; Plex bleibt einziger Medienserver |
|
||||||
|
| `homepage` | 2026-05-25 | doppeltes Dashboard neben Glance; Glance bleibt einziges Homelab-Dashboard |
|
||||||
|
| `uptime-kuma` | 2026-05-25 | durch `monitoring-blackbox-exporter`, Prometheus-Alerts und `monitoring-grafana` ersetzt |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -384,18 +389,19 @@ Für den laufenden Betrieb gilt stattdessen:
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `traefik` | Host-Ports 80/443 | zentraler Reverse Proxy |
|
| `traefik` | Host-Ports 80/443 | zentraler Reverse Proxy |
|
||||||
| `tailscale` | `host`, `NET_ADMIN`, `NET_RAW`, `/dev/net/tun` | VPN-Zugang benoetigt Kernel-Netzwerkfunktionen; Umstellung nur kontrolliert moeglich |
|
| `tailscale` | `host`, `NET_ADMIN`, `NET_RAW`, `/dev/net/tun` | VPN-Zugang benoetigt Kernel-Netzwerkfunktionen; Umstellung nur kontrolliert moeglich |
|
||||||
| `AdGuard Home` | Port 53 (TCP/UDP) direkt + Port 8082 auf Container-Port 80 | DNS benötigt direkten Port 53; kein HTTP-Proxy für DNS möglich |
|
| `AdGuard Home` | Port 53 (TCP/UDP) direkt + `100.80.98.33:8082` auf Container-Port 80 | DNS benoetigt direkten Port 53; Admin-Port 8082 bleibt bewusst ohne Traefik/2FA, aber nur via Tailscale |
|
||||||
| `Plex-Media-Server` | `host` | Discovery / mDNS / Plex GDM |
|
| `Plex-Media-Server` | `host` | Discovery / mDNS / Plex GDM |
|
||||||
| `scrutiny` | `privileged: true` | SMART-Datenzugriff auf Laufwerke |
|
| `scrutiny` | `privileged: true` | SMART-Datenzugriff auf Laufwerke |
|
||||||
| `Komodo` | Docker-Socket Zugriff | Stack-Deployments benötigen Socket |
|
| `Komodo` | Docker-Socket Zugriff | Stack-Deployments benötigen Socket |
|
||||||
|
| `glance-docker-socket-proxy` | Docker-Socket read-only | Glance benoetigt Containerstatus; Zugriff wird ueber einen internen Socket-Proxy auf lesende Docker-API-Endpunkte begrenzt und nicht ins `frontend_net` gelegt |
|
||||||
| `Komodo` | keine pauschale zentrale Middleware | Webhooks (`/listener`), API und Periphery-WebSocket (`/ws/periphery`) sollen nicht durch vorgeschaltete ForwardAuth gebrochen werden |
|
| `Komodo` | keine pauschale zentrale Middleware | Webhooks (`/listener`), API und Periphery-WebSocket (`/ws/periphery`) sollen nicht durch vorgeschaltete ForwardAuth gebrochen werden |
|
||||||
| `gitea` | SSH-Port 222 direkt gebunden | Git-SSH-Zugang; kein HTTP-Proxy für SSH möglich |
|
| `gitea` | SSH-Port 222 direkt gebunden | Git-SSH-Zugang; kein HTTP-Proxy für SSH möglich |
|
||||||
| `ddns-updater` | bleibt in `frontend_net` statt `backend_net` | braucht Cloudflare-API-Zugang; `backend_net` ist `internal: true` |
|
| `ddns-updater` | bleibt in `frontend_net` statt `backend_net` | braucht Cloudflare-API-Zugang; `backend_net` ist `internal: true` |
|
||||||
| `mail-archiver` | `frontend_net` + `backend_net` | braucht Internetzugang für IMAP-Abruf (GMX, Gmail) und DB-Zugang |
|
| `mail-archiver` | `frontend_net` + `backend_net` | braucht Internetzugang für IMAP-Abruf (GMX, Gmail) und DB-Zugang |
|
||||||
| `traefik/dynamic/*` | manueller Host-Sync trotz GitOps | File-Provider bleibt bewusst fuer `middlewares.yml`, `tls.yml` und `dashboards.yml`; Komodo deployed diese Dateien nicht automatisch |
|
| `traefik/dynamic/*` | manueller Host-Sync trotz GitOps | File-Provider bleibt bewusst fuer `middlewares.yml`, `tls.yml` und `dashboards.yml`; Komodo deployed diese Dateien nicht automatisch |
|
||||||
| `nextcloud` | keine zentrale ForwardAuth-Middleware | Nextcloud bringt eigene Auth, Clients und WebDAV/CardDAV-Endpunkte mit; Traefik bleibt Reverse Proxy, Auth bleibt app-nativ |
|
| `nextcloud` | keine zentrale ForwardAuth-Middleware | Nextcloud bringt eigene Auth, Clients und WebDAV/CardDAV-Endpunkte mit; Traefik bleibt Reverse Proxy, Auth bleibt app-nativ |
|
||||||
| `influxdb3-core` | Host-Port 8181 auf LAN-IP | Home Assistant laeuft in einer VM ausserhalb des Compose-Netzes und muss Metriken schreiben koennen; keine Traefik-Route, kein `frontend_net`, Zugriff nur ueber Token und LAN-IP `INFLUXDB_BIND_IP` |
|
| `monitoring-influxdb3-core` | Host-Port 8181 auf LAN-IP; `user: "0"` | Home Assistant laeuft in einer VM ausserhalb des Compose-Netzes und muss Metriken schreiben koennen; keine Traefik-Route, kein `frontend_net`, Zugriff nur ueber Token und LAN-IP `INFLUXDB_BIND_IP`; InfluxDB 3 Core benoetigt im aktuellen Container-Setup Root-Rechte fuer den lokalen Object-Store-Pfad im named volume |
|
||||||
| `grafana`, `influxdb3-core` | `user: "0"` | aktueller Live-Stand fuer Host-Appdata-/Plugin-Permissions; als Ausnahme dokumentiert, spaeter separat mit UID/GID-Test haerten |
|
| `monitoring-promtail` | Docker-Socket read-only | Docker-Log-Discovery fuer Loki; keine Schreibrechte, keine Appdaten-Persistenz ueber den Socket |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -485,7 +491,7 @@ Komodo ist nun der primäre GitOps-Stack-Manager:
|
|||||||
- AdGuard läuft als Git-Stack (`host-services/Adguard/docker-compose.yml`)
|
- AdGuard läuft als Git-Stack (`host-services/Adguard/docker-compose.yml`)
|
||||||
- Netzwerke: `dns_net` (feste IP 172.23.0.3) + `frontend_net`
|
- Netzwerke: `dns_net` (feste IP 172.23.0.3) + `frontend_net`
|
||||||
- Port 53 (DNS) direkt gebunden — dokumentierte Ausnahme
|
- Port 53 (DNS) direkt gebunden — dokumentierte Ausnahme
|
||||||
- Admin-UI direkt gebunden via Host-Port 8082 auf Container-Port 80 — Traefik-Absicherung ausstehend (Block F)
|
- Admin-UI direkt gebunden via Tailscale-IP `100.80.98.33:8082` auf Container-Port 80 — 2026-05-26 bewusst als einfache Operator-Entscheidung ohne Traefik-/2FA-Umstellung
|
||||||
- `unbound` läuft weiterhin als Upstream-Resolver in `dns_net`
|
- `unbound` läuft weiterhin als Upstream-Resolver in `dns_net`
|
||||||
|
|
||||||
### diun — Entfernung (2026-03-28)
|
### diun — Entfernung (2026-03-28)
|
||||||
@@ -535,9 +541,9 @@ Host-Pfade in `env_file` (z.B. `/mnt/...`) sind in Git-Stacks nicht verfügbar.
|
|||||||
### Reproduzierbare Deployments (2026-04-17)
|
### Reproduzierbare Deployments (2026-04-17)
|
||||||
Mutable Tags wie `latest`, `stable`, `release` oder reine Major-Tags wurden auf die **aktuell laufenden Digests** eingefroren. Das ist bewusst **kein Upgrade-Mechanismus**, sondern dient dazu, den heute funktionierenden Laufzeitstand exakt im Repo festzuhalten. Echte Versions-Upgrades bleiben ein eigener, geplanter Schritt.
|
Mutable Tags wie `latest`, `stable`, `release` oder reine Major-Tags wurden auf die **aktuell laufenden Digests** eingefroren. Das ist bewusst **kein Upgrade-Mechanismus**, sondern dient dazu, den heute funktionierenden Laufzeitstand exakt im Repo festzuhalten. Echte Versions-Upgrades bleiben ein eigener, geplanter Schritt.
|
||||||
|
|
||||||
### Stateful Digest-Pinning (2026-05-05)
|
### Stateful Digest-Pinning (2026-05-05, ergaenzt 2026-05-16)
|
||||||
- Tier-1/stateful Basisdienste werden bevorzugt mit sprechendem Minor-/Patch-Tag plus Digest gepinnt, z. B. `postgres:17.9@sha256:...` oder `mongo:7.0.32@sha256:...`.
|
- Tier-1/stateful Basisdienste werden bevorzugt mit sprechendem Minor-/Patch-Tag plus Digest gepinnt, z. B. `postgres:17.9@sha256:...` oder `mongo:7.0.32@sha256:...`.
|
||||||
- Redis-Caches bleiben bewusst ohne Digest-Pin. Cache-Verlust ist akzeptabel, und Sicherheits-Patches sollen dort ohne eigenen Datenbank-Upgrade-Sprint fliessen koennen.
|
- Redis-Caches sind seit dem Hardening-Sprint 2026-05-16 auf `redis:7.4-alpine@sha256:...` vereinheitlicht. Updates erfolgen bewusst stackweise mit Smoke-Test.
|
||||||
- Bereits versionierte Apps koennen optional spaeter ebenfalls Digests erhalten; dieser Schritt ist getrennt vom Datenhalter-Pinning.
|
- Bereits versionierte Apps koennen optional spaeter ebenfalls Digests erhalten; dieser Schritt ist getrennt vom Datenhalter-Pinning.
|
||||||
|
|
||||||
### Nextcloud und Stirling-PDF (2026-04-19)
|
### Nextcloud und Stirling-PDF (2026-04-19)
|
||||||
@@ -545,14 +551,22 @@ Mutable Tags wie `latest`, `stable`, `release` oder reine Major-Tags wurden auf
|
|||||||
- `nextcloud` bleibt bei nativer App-Authentifizierung ohne zentrale ForwardAuth-Middleware vor dem Router, damit Browser-Login, Desktop-/Mobile-Clients sowie WebDAV/CardDAV sauber funktionieren.
|
- `nextcloud` bleibt bei nativer App-Authentifizierung ohne zentrale ForwardAuth-Middleware vor dem Router, damit Browser-Login, Desktop-/Mobile-Clients sowie WebDAV/CardDAV sauber funktionieren.
|
||||||
- `stirling-pdf` wird als geschuetzter Tool-Stack hinter `authelia@file,secure-headers@file` betrieben; die interne Stirling-Login-Funktion bleibt deaktiviert, um Doppel-Login zu vermeiden.
|
- `stirling-pdf` wird als geschuetzter Tool-Stack hinter `authelia@file,secure-headers@file` betrieben; die interne Stirling-Login-Funktion bleibt deaktiviert, um Doppel-Login zu vermeiden.
|
||||||
|
|
||||||
### BentoPDF und Grafana/InfluxDB vorbereitet (2026-04-30)
|
### BentoPDF und Monitoring-Zielstack (2026-04-30, aktualisiert 2026-05-17)
|
||||||
- `bentopdf` ersetzt repo-seitig `stirling-pdf` auf der bestehenden Domain `pdf.kaleschke.info`, bleibt aber bis zum bewussten Komodo-Deploy nur vorbereitet.
|
- `bentopdf` ersetzt repo-seitig `stirling-pdf` auf der bestehenden Domain `pdf.kaleschke.info`, bleibt aber bis zum bewussten Komodo-Deploy nur vorbereitet.
|
||||||
- BentoPDF benoetigt fuer Office-Konvertierung die Cross-Origin-Isolation-Header `Cross-Origin-Opener-Policy: same-origin` und `Cross-Origin-Embedder-Policy: require-corp`; diese werden per Traefik-Docker-Middleware gesetzt.
|
- BentoPDF benoetigt fuer Office-Konvertierung die Cross-Origin-Isolation-Header `Cross-Origin-Opener-Policy: same-origin` und `Cross-Origin-Embedder-Policy: require-corp`; diese werden per Traefik-Docker-Middleware gesetzt.
|
||||||
- `grafana` wird als geschuetztes Monitoring-UI unter `grafana.kaleschke.info` betrieben.
|
- `monitoring/` ist der zentrale Zielstack fuer Prometheus, Loki, Promtail, Grafana, node-exporter, cAdvisor und InfluxDB 3 Core.
|
||||||
- `influxdb3-core` bleibt ohne Traefik-/Public-Route; fuer interne Writer wie Home Assistant kann Port `8181` per `INFLUXDB_BIND_IP` auf eine LAN-Adresse gebunden werden.
|
- `monitoring-grafana` wird als geschuetztes Monitoring-UI unter `monitoring.kaleschke.info` betrieben.
|
||||||
- Fuer dieses Port-Publishing nutzt `influxdb3-core` zusaetzlich zum internen Grafana-Netz `grafana_influx_lan`. Das ist keine Public-App-Freigabe und ersetzt nicht die Token-Authentifizierung.
|
- `monitoring-influxdb3-core` bleibt ohne Traefik-/Public-Route; fuer interne Writer wie Home Assistant kann Port `8181` per `INFLUXDB_BIND_IP` auf eine LAN-Adresse gebunden werden.
|
||||||
|
- Fuer dieses Port-Publishing nutzt `monitoring-influxdb3-core` zusaetzlich `monitoring_influx_lan`. Das ist keine Public-App-Freigabe und ersetzt nicht die Token-Authentifizierung.
|
||||||
- InfluxDB 3 Core nutzt einen festen Versionstag statt `latest`, weil der InfluxDB-`latest`-Tag versionsstrategisch im Umbruch ist.
|
- InfluxDB 3 Core nutzt einen festen Versionstag statt `latest`, weil der InfluxDB-`latest`-Tag versionsstrategisch im Umbruch ist.
|
||||||
- `grafana` und `influxdb3-core` laufen aktuell als `user: "0"`; das ist als Host-Appdata-Permissions-Ausnahme dokumentiert und wird nicht nebenbei geaendert.
|
- Die alten Pfade `ops/grafana-influxdb` und `ops/loki` wurden am 2026-05-26 aus dem aktiven Repo entfernt; `monitoring/` ist der einzige Observability-Zielstack.
|
||||||
|
- Uptime Kuma wurde nach erfolgreichem Blackbox-/Grafana-Smoke-Test entfernt; `monitoring/` ist die Quelle fuer HTTP-Erreichbarkeit und Alerts.
|
||||||
|
|
||||||
|
### Monitoring-Logging-Baseline (2026-05-17)
|
||||||
|
- `monitoring-loki` laeuft intern auf `monitoring_net`, ohne Traefik-Route und ohne Host-Port.
|
||||||
|
- `monitoring-promtail` sammelt Docker-Logs ueber `/var/run/docker.sock:ro` und `/var/lib/docker/containers:ro` und schreibt sie an Loki.
|
||||||
|
- `monitoring-grafana` bekommt provisionierte Datasources fuer Prometheus, Loki und InfluxDB 3 Core.
|
||||||
|
- Loki-Logdaten sind Diagnosematerial mit begrenzter Retention, keine primaere Restore-Quelle.
|
||||||
|
|
||||||
### Authelia ohne Redis-Session-Backend (2026-05-04)
|
### Authelia ohne Redis-Session-Backend (2026-05-04)
|
||||||
- Authelia nutzt PostgreSQL fuer persistente Storage-Daten, aber bewusst kein Redis-Session-Backend.
|
- Authelia nutzt PostgreSQL fuer persistente Storage-Daten, aber bewusst kein Redis-Session-Backend.
|
||||||
@@ -569,7 +583,7 @@ Benötigt `backend_net` (PostgreSQL) + `frontend_net` (IMAP-Abruf von GMX/Gmail)
|
|||||||
- App → `frontend_net` + internes Netzwerk
|
- App → `frontend_net` + internes Netzwerk
|
||||||
- Datenbank → nur internes Netzwerk (`internal: true`)
|
- Datenbank → nur internes Netzwerk (`internal: true`)
|
||||||
|
|
||||||
Beispiel (Mealie): `mealie` → `frontend_net` + `mealie_mealie_internal`, `mealie-postgres` → nur `mealie_mealie_internal`.
|
Beispiel (Mealie): `mealie` → `frontend_net` + `mealie_internal`, `mealie-postgres` → nur `mealie_internal`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ Bei Restore-, Host-Ausfall- oder Wiederanlauf-Fragen zusaetzlich:
|
|||||||
|
|
||||||
3. `docs/DISASTER_RECOVERY.md`
|
3. `docs/DISASTER_RECOVERY.md`
|
||||||
4. `docs/RESTORE_MATRIX.md`
|
4. `docs/RESTORE_MATRIX.md`
|
||||||
|
5. `docs/SERVICES_RECOVERY.md`
|
||||||
|
|
||||||
|
Bei Hardware-, Netzwerk-, Provider- oder Kapazitaetsfragen zusaetzlich:
|
||||||
|
|
||||||
|
6. `docs/HARDWARE_INVENTORY.md`
|
||||||
|
7. `docs/NETWORK_INVENTORY.md`
|
||||||
|
8. `docs/EXTERNAL_DEPENDENCIES.md`
|
||||||
|
9. `docs/CAPACITY_AND_LIFECYCLE.md`
|
||||||
|
|
||||||
## Architektur
|
## Architektur
|
||||||
|
|
||||||
@@ -38,7 +46,8 @@ Bei Restore-, Host-Ausfall- oder Wiederanlauf-Fragen zusaetzlich:
|
|||||||
- `security/` -> sicherheitskritische Dienste
|
- `security/` -> sicherheitskritische Dienste
|
||||||
- `infra/` -> Datenbanken und technische Services
|
- `infra/` -> Datenbanken und technische Services
|
||||||
- `apps/` -> Anwendungen
|
- `apps/` -> Anwendungen
|
||||||
- `ops/` -> Monitoring und Tools
|
- `ops/` -> operative Tools
|
||||||
|
- `monitoring/` -> zentraler Observability-Stack
|
||||||
- `host-services/` -> Dienste mit Host-Netz
|
- `host-services/` -> Dienste mit Host-Netz
|
||||||
- `traefik/` -> Reverse Proxy Konfiguration
|
- `traefik/` -> Reverse Proxy Konfiguration
|
||||||
- `docs/` -> Dokumentation und Prozesse
|
- `docs/` -> Dokumentation und Prozesse
|
||||||
@@ -59,9 +68,11 @@ Bei Restore-, Host-Ausfall- oder Wiederanlauf-Fragen zusaetzlich:
|
|||||||
- Komodo ist der primaere und einzige produktive Stack-Manager.
|
- Komodo ist der primaere und einzige produktive Stack-Manager.
|
||||||
- Komodo bleibt bewusst bei nativer Authentifizierung; zentrale Traefik-Auth wird dort nicht pauschal vorgeschaltet.
|
- Komodo bleibt bewusst bei nativer Authentifizierung; zentrale Traefik-Auth wird dort nicht pauschal vorgeschaltet.
|
||||||
- Portainer CE ist abgeschaltet und kein Teil des aktiven Betriebs mehr.
|
- Portainer CE ist abgeschaltet und kein Teil des aktiven Betriebs mehr.
|
||||||
- Homepage ist das aktive produktive Frontend / Start-Dashboard.
|
- Glance ist das aktive produktive Homelab-Dashboard.
|
||||||
- Traefik `dynamic/` bleibt eine dokumentierte manuelle Host-Sync-Ausnahme ausserhalb des normalen Komodo-Deployments.
|
- Traefik `dynamic/` bleibt eine dokumentierte manuelle Host-Sync-Ausnahme ausserhalb des normalen Komodo-Deployments.
|
||||||
- Mutable Image-Tags sind auf die aktuell laufenden Digests eingefroren; echte Versions-Upgrades erfolgen bewusst separat.
|
- Mutable Image-Tags sind auf die aktuell laufenden Digests eingefroren; echte Versions-Upgrades erfolgen bewusst separat.
|
||||||
- Disaster-Recovery und dienstspezifische Restore-Quellen sind in `docs/DISASTER_RECOVERY.md` und `docs/RESTORE_MATRIX.md` beschrieben.
|
- Disaster-Recovery und dienstspezifische Restore-Quellen sind in `docs/DISASTER_RECOVERY.md` und `docs/RESTORE_MATRIX.md` beschrieben.
|
||||||
|
- Recovery-kritische Services-Pfade wie Gitea-Repositories, Komodo-Workspaces und Host-Automation sind in `docs/SERVICES_RECOVERY.md` beschrieben.
|
||||||
|
- Hardware-, Netzwerk-, Provider- und Capacity-Inventare sind als operative Audit-Dokumente unter `docs/HARDWARE_INVENTORY.md`, `docs/NETWORK_INVENTORY.md`, `docs/EXTERNAL_DEPENDENCIES.md` und `docs/CAPACITY_AND_LIFECYCLE.md` vorbereitet.
|
||||||
- Der verbindliche Detailablauf steht in `docs/WORKFLOW.md`.
|
- Der verbindliche Detailablauf steht in `docs/WORKFLOW.md`.
|
||||||
- `nextcloud`, `bentopdf` und `grafana-influxdb` folgen dem dokumentierten Netz-/Secret-/Traefik-Modell; Grafana/InfluxDB ist fuer Home-Assistant-Wetterdaten vorbereitet.
|
- `nextcloud`, `bentopdf` und `monitoring` folgen dem dokumentierten Netz-/Secret-/Traefik-Modell; der zentrale Monitoring-Stack buendelt Prometheus, Loki, Promtail, Grafana und InfluxDB 3 Core.
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
services:
|
|
||||||
homepage:
|
|
||||||
image: ghcr.io/gethomepage/homepage:latest@sha256:cc84f2f5eb3c7734353701ccbaa24ed02dacb0d119114e50e4251e2005f3990a
|
|
||||||
container_name: homepage
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
HOMEPAGE_ALLOWED_HOSTS: home.kaleschke.info
|
|
||||||
HOMEPAGE_VAR_GITEA_TOKEN: ${HOMEPAGE_VAR_GITEA_TOKEN}
|
|
||||||
HOMEPAGE_VAR_ADGUARD_USERNAME: ${HOMEPAGE_VAR_ADGUARD_USERNAME}
|
|
||||||
HOMEPAGE_VAR_ADGUARD_PASSWORD: ${HOMEPAGE_VAR_ADGUARD_PASSWORD}
|
|
||||||
HOMEPAGE_VAR_KOMODO_API_KEY: ${HOMEPAGE_VAR_KOMODO_API_KEY}
|
|
||||||
HOMEPAGE_VAR_KOMODO_API_SECRET: ${HOMEPAGE_VAR_KOMODO_API_SECRET}
|
|
||||||
HOMEPAGE_VAR_BACKREST_USERNAME: ${HOMEPAGE_VAR_BACKREST_USERNAME}
|
|
||||||
HOMEPAGE_VAR_BACKREST_PASSWORD: ${HOMEPAGE_VAR_BACKREST_PASSWORD}
|
|
||||||
HOMEPAGE_VAR_SPEEDTEST_API_KEY: ${HOMEPAGE_VAR_SPEEDTEST_API_KEY}
|
|
||||||
HOMEPAGE_VAR_PAPERLESS_TOKEN: ${HOMEPAGE_VAR_PAPERLESS_TOKEN}
|
|
||||||
HOMEPAGE_VAR_FILEBROWSER_USERNAME: ${HOMEPAGE_VAR_FILEBROWSER_USERNAME}
|
|
||||||
HOMEPAGE_VAR_FILEBROWSER_PASSWORD: ${HOMEPAGE_VAR_FILEBROWSER_PASSWORD}
|
|
||||||
HOMEPAGE_VAR_IMMICH_API_KEY: ${HOMEPAGE_VAR_IMMICH_API_KEY}
|
|
||||||
HOMEPAGE_VAR_MEALIE_TOKEN: ${HOMEPAGE_VAR_MEALIE_TOKEN}
|
|
||||||
HOMEPAGE_VAR_UPTIME_SLUG: ${HOMEPAGE_VAR_UPTIME_SLUG}
|
|
||||||
volumes:
|
|
||||||
- /mnt/user/appdata/homepage:/app/config
|
|
||||||
- /mnt/user/appdata/homepage/images:/app/public/images
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
||||||
networks:
|
|
||||||
- frontend_net
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.docker.network=frontend_net
|
|
||||||
- traefik.http.routers.homepage.rule=Host(`home.kaleschke.info`)
|
|
||||||
- traefik.http.routers.homepage.entrypoints=websecure
|
|
||||||
- traefik.http.routers.homepage.tls=true
|
|
||||||
- traefik.http.routers.homepage.tls.certresolver=le
|
|
||||||
- traefik.http.routers.homepage.middlewares=authelia@file,secure-headers@file
|
|
||||||
- traefik.http.services.homepage.loadbalancer.server.port=3000
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
|
|
||||||
networks:
|
|
||||||
frontend_net:
|
|
||||||
external: true
|
|
||||||
@@ -12,10 +12,10 @@ services:
|
|||||||
DB_PASSWORD: ${IMMICH_DB_PASSWORD}
|
DB_PASSWORD: ${IMMICH_DB_PASSWORD}
|
||||||
DB_DATABASE_NAME: immich
|
DB_DATABASE_NAME: immich
|
||||||
REDIS_HOSTNAME: redis
|
REDIS_HOSTNAME: redis
|
||||||
|
TZ: Europe/Berlin
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/user/photos/immich:/usr/src/app/upload
|
- /mnt/user/photos/immich:/usr/src/app/upload
|
||||||
- /mnt/user/photos/family_archive:/usr/src/app/external
|
- /mnt/user/photos/family_archive:/usr/src/app/external
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
networks:
|
networks:
|
||||||
- immich_default
|
- immich_default
|
||||||
- frontend_net
|
- frontend_net
|
||||||
@@ -43,7 +43,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:7
|
image: redis:7.4-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- immich_default
|
- immich_default
|
||||||
|
|||||||
@@ -14,13 +14,12 @@ services:
|
|||||||
POSTGRES_SERVER: mealie-postgres
|
POSTGRES_SERVER: mealie-postgres
|
||||||
POSTGRES_DB: mealie
|
POSTGRES_DB: mealie
|
||||||
POSTGRES_USER: mealie
|
POSTGRES_USER: mealie
|
||||||
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
|
POSTGRES_PASSWORD: ${MEALIE_POSTGRES_PASSWORD}
|
||||||
|
|
||||||
BASE_URL: https://mealie.kaleschke.info
|
BASE_URL: https://mealie.kaleschke.info
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/user/appdata/mealie/data:/app/data
|
- /mnt/user/appdata/mealie/data:/app/data
|
||||||
- /mnt/user/appdata/secrets/mealie_postgres_password.txt:/run/secrets/postgres_password:ro
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
- frontend_net
|
- frontend_net
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
nextcloud:
|
nextcloud:
|
||||||
image: nextcloud:33.0.2-apache
|
image: nextcloud:33.0.2-apache@sha256:39b2ba219271a22851f8409a7b1295d5892aba1696d9193500311c02e60591a4
|
||||||
container_name: nextcloud
|
container_name: nextcloud
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -64,7 +64,7 @@ services:
|
|||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
nextcloud-redis:
|
nextcloud-redis:
|
||||||
image: redis:7.4-alpine
|
image: redis:7.4-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
|
||||||
container_name: nextcloud-redis
|
container_name: nextcloud-redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: redis-server --save 60 1 --loglevel warning
|
command: redis-server --save 60 1 --loglevel warning
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
unbound:
|
unbound:
|
||||||
image: shaanmajid/unbound:latest@sha256:0a163e92e55698ddc45c0265884a86c7363da245ab4a909a8e5cb0f541aeeb4d
|
image: shaanmajid/unbound:1.24.2@sha256:d278b71c592b2555cc802911bb0757a6a24f4a8ad7f5848720296c04876eeb63
|
||||||
container_name: unbound
|
container_name: unbound
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -11,9 +11,17 @@ services:
|
|||||||
- GITEA__server__DOMAIN=git.kaleschke.info
|
- GITEA__server__DOMAIN=git.kaleschke.info
|
||||||
- GITEA__server__ROOT_URL=https://git.kaleschke.info/
|
- GITEA__server__ROOT_URL=https://git.kaleschke.info/
|
||||||
- GITEA__database__DB_TYPE=sqlite3
|
- GITEA__database__DB_TYPE=sqlite3
|
||||||
- GITEA__webhook__ALLOWED_HOST_LIST=*
|
- GITEA__service__DISABLE_REGISTRATION=true
|
||||||
|
- GITEA__service__REGISTER_EMAIL_CONFIRM=true
|
||||||
|
- GITEA__openid__ENABLE_OPENID_SIGNIN=false
|
||||||
|
- GITEA__openid__ENABLE_OPENID_SIGNUP=false
|
||||||
|
- GITEA__migrations__ALLOWED_DOMAINS=github.com
|
||||||
|
- GITEA__webhook__ALLOWED_HOST_LIST=komodo-core,localhost,127.0.0.1,192.168.178.0/24
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/user/services/gitea/data:/data
|
- /mnt/user/services/gitea/data:/data
|
||||||
|
dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
ports:
|
ports:
|
||||||
- "222:22"
|
- "222:22"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
+40
-10
@@ -39,6 +39,8 @@ Traefik ist der zentrale Web-Einstieg fuer HTTP(S). Admin-/Ops-UIs liegen entwed
|
|||||||
- Gitea hostet das Repo unter `git.kaleschke.info`.
|
- Gitea hostet das Repo unter `git.kaleschke.info`.
|
||||||
- Komodo ist Stack-Manager und Deploy-Consumer.
|
- Komodo ist Stack-Manager und Deploy-Consumer.
|
||||||
- Komodo Periphery braucht Docker-Socket und `/mnt/user/services` Mount, um Stacks reproduzierbar zu deployen.
|
- Komodo Periphery braucht Docker-Socket und `/mnt/user/services` Mount, um Stacks reproduzierbar zu deployen.
|
||||||
|
- Neue produktive Komodo-Stacks aus `Micha/homelab-infra` muessen einen aktiven Gitea->Komodo-Webhook auf die aktuelle Stack-ID haben; Ausnahmen wie deaktivierte/pausierte Stacks muessen dokumentiert werden.
|
||||||
|
- Der `komodo`-Self-Stack ist eine dokumentierte Ausnahme ohne aktiven Gitea-Webhook; Bootstrap/Recovery laeuft ueber `docs/SERVICES_RECOVERY.md`.
|
||||||
|
|
||||||
### Identity / Security
|
### Identity / Security
|
||||||
|
|
||||||
@@ -50,16 +52,42 @@ Traefik ist der zentrale Web-Einstieg fuer HTTP(S). Admin-/Ops-UIs liegen entwed
|
|||||||
|
|
||||||
### Apps
|
### Apps
|
||||||
|
|
||||||
Wichtige Apps sind Paperless, Immich, Mealie, Mail Archiver, Nextcloud, ntfy, Vaultwarden und Gitea. Admin-/Ops-Tools sind u. a. Homepage, Komodo, Borg UI, Backrest, Uptime Kuma, Filebrowser, code-server, Glances, Scrutiny, Speedtest, Grafana und Hermes Agent.
|
Wichtige Apps sind Paperless, Immich, Mealie, Mail Archiver, Nextcloud, ntfy, Vaultwarden und Gitea. Admin-/Ops-Tools sind u. a. Glance, Komodo, Borg UI, Filebrowser, code-server, Glances, Scrutiny, Speedtest, Monitoring Grafana und Hermes Agent.
|
||||||
|
|
||||||
|
### Hermes Agent — Architektur und Ops-Monitor
|
||||||
|
|
||||||
|
Hermes laeuft nach Model C (siehe `ops/hermes-agent/README.md`):
|
||||||
|
|
||||||
|
- `hermes-gateway` als Docker-Container auf dem Unraid-Host, intern auf `hermes_net:8642`
|
||||||
|
- Terminal-Befehle werden via SSH auf eine dedizierte Linux-VM ausgefuehrt
|
||||||
|
- VM-IP: `192.168.178.143`, SSH-User: `hermes`
|
||||||
|
- Repo-Clone auf der VM: `/srv/hermes-workspace/homelab-infra/`
|
||||||
|
|
||||||
|
**Fuer KI-Agenten wichtig:** Das Hermes-Terminal laeuft auf der VM, nicht auf dem Unraid-Host.
|
||||||
|
`/mnt/user/...`-Pfade sind von der VM aus nicht direkt erreichbar.
|
||||||
|
Docker-CLI ist auf der VM nicht installiert — fuer Homelab-Checks wird `check_health.py` verwendet.
|
||||||
|
|
||||||
|
**Ops-Monitor (homelab-ops-monitor):**
|
||||||
|
|
||||||
|
- Skill: `ops/hermes-agent/skills/homelab-ops-monitor.md`
|
||||||
|
- Script: `ops/hermes-agent/scripts/check_health.py` — prueft Services via HTTP, keine externen Deps
|
||||||
|
- Wissensbasis: `ops/hermes-agent/services.json` — maschinenlesbare Ableitung aus `docs/SERVICE_CATALOG.md`
|
||||||
|
- Check-Strategie: HTTP GET fuer URL-basierte Services, interne Services (DBs, Redis) als `"internal"` markiert
|
||||||
|
- ntfy-Topic fuer Alerts: `homelab-alerts` auf `https://ntfy.kaleschke.info`
|
||||||
|
|
||||||
|
Nach Aenderungen an `services.json` oder `check_health.py`: `git pull` auf der VM ausfuehren.
|
||||||
- Mail Archiver ist ein Hybrid-Dienst mit `frontend_net` fuer IMAP/Traefik und `backend_net` fuer PostgreSQL; die Web-UI liegt hinter Authelia und behaelt zusaetzlich App-eigene Auth.
|
- Mail Archiver ist ein Hybrid-Dienst mit `frontend_net` fuer IMAP/Traefik und `backend_net` fuer PostgreSQL; die Web-UI liegt hinter Authelia und behaelt zusaetzlich App-eigene Auth.
|
||||||
|
|
||||||
### Monitoring / Metriken
|
### Monitoring / Metriken
|
||||||
|
|
||||||
- Grafana laeuft unter `grafana.kaleschke.info` hinter Traefik + Authelia.
|
- Zielzustand ist ein zentraler Stack `monitoring/` unter `https://monitoring.kaleschke.info`.
|
||||||
- InfluxDB 3 Core ist nicht public und nicht im `frontend_net`.
|
- `monitoring-grafana` ist die zentrale UI fuer Prometheus, Loki und InfluxDB 3 Core.
|
||||||
|
- `monitoring-prometheus` sammelt Infrastruktur-Metriken; `monitoring-loki` + `monitoring-promtail` sammeln Docker-Logs.
|
||||||
|
- `monitoring-influxdb3-core` ist nicht public und nicht im `frontend_net`.
|
||||||
- Home Assistant schreibt ueber LAN-only Port 8181 nach InfluxDB, gebunden ueber `INFLUXDB_BIND_IP`.
|
- Home Assistant schreibt ueber LAN-only Port 8181 nach InfluxDB, gebunden ueber `INFLUXDB_BIND_IP`.
|
||||||
- Ein `401 Unauthorized` von InfluxDB ohne Token ist beim Reachability-Test ein Erfolgssignal.
|
- Ein `401 Unauthorized` von InfluxDB ohne Token ist beim Reachability-Test ein Erfolgssignal.
|
||||||
- Grafana und InfluxDB laufen aktuell als `user: "0"`; nicht ad hoc aendern, sondern als eigenen UID/GID-Hardening-Sprint behandeln.
|
- Die frueheren Altstaende `ops/loki` und `ops/grafana-influxdb` wurden aus dem aktiven Repo entfernt; fuer Monitoring immer `monitoring/` verwenden, Rollback nur ueber Git-Historie.
|
||||||
|
- Uptime Kuma ist entfernt; HTTP-Verfuegbarkeit laeuft ueber Blackbox Exporter, Prometheus-Alerts und `Homelab / Availability` in Monitoring Grafana.
|
||||||
|
|
||||||
## Deployment-Logik
|
## Deployment-Logik
|
||||||
|
|
||||||
@@ -76,6 +104,8 @@ Normalfall:
|
|||||||
|
|
||||||
Wichtig: Komodo-Web-Editor ist nicht der Bearbeitungsort. Wenn Komodo und Git voneinander abweichen, zuerst Git und Komodo Workspace pruefen, nicht live herumprobieren.
|
Wichtig: Komodo-Web-Editor ist nicht der Bearbeitungsort. Wenn Komodo und Git voneinander abweichen, zuerst Git und Komodo Workspace pruefen, nicht live herumprobieren.
|
||||||
|
|
||||||
|
Beim Anlegen neuer produktiver Stacks ist der Gitea->Komodo-Webhook Pflicht. Nach dem Anlegen muss ein Test-Push oder Test-Delivery zeigen, dass Gitea die aktuelle Komodo-Stack-ID erreicht.
|
||||||
|
|
||||||
## Netzwerkmodell
|
## Netzwerkmodell
|
||||||
|
|
||||||
| Netzwerk | Bedeutung |
|
| Netzwerk | Bedeutung |
|
||||||
@@ -83,7 +113,7 @@ Wichtig: Komodo-Web-Editor ist nicht der Bearbeitungsort. Wenn Komodo und Git vo
|
|||||||
| `frontend_net` | Web-/Proxy-Netz fuer Traefik-geroutete Dienste und Dienste mit Internetbedarf |
|
| `frontend_net` | Web-/Proxy-Netz fuer Traefik-geroutete Dienste und Dienste mit Internetbedarf |
|
||||||
| `backend_net` | internes Netz fuer shared PostgreSQL, Redis und Backends |
|
| `backend_net` | internes Netz fuer shared PostgreSQL, Redis und Backends |
|
||||||
| `dns_net` | AdGuard + Unbound |
|
| `dns_net` | AdGuard + Unbound |
|
||||||
| app-interne Netze | Isolation von App + DB/Cache, z. B. Immich, Mealie, Nextcloud, Grafana/Influx |
|
| app-interne Netze | Isolation von App + DB/Cache, z. B. Immich, Mealie, Nextcloud, Monitoring |
|
||||||
| `host` | nur dokumentierte Sonderfaelle wie Tailscale/Plex |
|
| `host` | nur dokumentierte Sonderfaelle wie Tailscale/Plex |
|
||||||
|
|
||||||
Regeln:
|
Regeln:
|
||||||
@@ -91,7 +121,7 @@ Regeln:
|
|||||||
- Datenbanken nie ins `frontend_net`.
|
- Datenbanken nie ins `frontend_net`.
|
||||||
- Admin-UIs nur mit Traefik + Middleware oder dokumentierter Ausnahme.
|
- Admin-UIs nur mit Traefik + Middleware oder dokumentierter Ausnahme.
|
||||||
- Direkte Host-Ports sind Ausnahme, nicht Default.
|
- Direkte Host-Ports sind Ausnahme, nicht Default.
|
||||||
- Runtime-Netznamen koennen Compose-Projektpraefixe bekommen, z. B. `grafana_grafana_influx_lan`.
|
- Runtime-Netznamen koennen Compose-Projektpraefixe bekommen, z. B. `monitoring_monitoring_influx_lan`.
|
||||||
|
|
||||||
## Security-Modell
|
## Security-Modell
|
||||||
|
|
||||||
@@ -106,12 +136,12 @@ Bekannte Ausnahmen:
|
|||||||
|
|
||||||
- Traefik: 80/443
|
- Traefik: 80/443
|
||||||
- Gitea: SSH 222
|
- Gitea: SSH 222
|
||||||
- AdGuard: DNS 53 und Admin 8082
|
- AdGuard: DNS 53 direkt; Admin 8082 ist bewusst ohne Traefik/2FA, aber auf Tailscale-IP `100.80.98.33` begrenzt
|
||||||
- Tailscale: Host-Netz, `NET_ADMIN`, `NET_RAW`, `/dev/net/tun`
|
- Tailscale: Host-Netz, `NET_ADMIN`, `NET_RAW`, `/dev/net/tun`
|
||||||
- Scrutiny: privileged
|
- Scrutiny: privileged
|
||||||
- Komodo: Docker-Socket, native Auth
|
- Komodo: Docker-Socket, native Auth
|
||||||
- InfluxDB: LAN-only 8181 fuer Home Assistant Writer
|
- InfluxDB: LAN-only 8181 fuer Home Assistant Writer
|
||||||
- Grafana/InfluxDB: `user: "0"` als dokumentierte Host-Appdata-Permissions-Ausnahme
|
- `monitoring-influxdb3-core`: `user: "0"` als dokumentierte Host-Appdata-Permissions-Ausnahme
|
||||||
- Traefik dynamic config: manueller Host-Sync
|
- Traefik dynamic config: manueller Host-Sync
|
||||||
|
|
||||||
## Backup- und Restore-Modell
|
## Backup- und Restore-Modell
|
||||||
@@ -160,8 +190,8 @@ KI-Agenten sollen konservativ arbeiten: keine indirekten Live-Aenderungen, keine
|
|||||||
- Authelia nutzt PostgreSQL, aber bewusst kein Redis-Session-Backend; Redis ist kein Authelia-Bootstrap-Blocker.
|
- Authelia nutzt PostgreSQL, aber bewusst kein Redis-Session-Backend; Redis ist kein Authelia-Bootstrap-Blocker.
|
||||||
- Authelia-Notifier ist SMTP; bei Auth-Aenderungen Host-Config backupen, `authelia validate-config` ausfuehren und erst danach neu starten.
|
- Authelia-Notifier ist SMTP; bei Auth-Aenderungen Host-Config backupen, `authelia validate-config` ausfuehren und erst danach neu starten.
|
||||||
- `paperless-ngx` nutzt fuer DB/Redis bewusst Stack ENV statt `_FILE`.
|
- `paperless-ngx` nutzt fuer DB/Redis bewusst Stack ENV statt `_FILE`.
|
||||||
- `homepage`, `glances` und `komodo-periphery` nutzen Docker-Socket-Mounts; Zugriff bewusst behandeln.
|
- `glance-docker-socket-proxy`, `glances` und `komodo-periphery` nutzen Docker-/Socket-Zugriff; Zugriff bewusst behandeln.
|
||||||
- `backrest`, `borg-ui` und `filebrowser` haben breite Mounts; bei Hardening nicht ad hoc, sondern gezielt vorgehen.
|
- `borg-ui` und `filebrowser` haben breite Mounts; bei Hardening nicht ad hoc, sondern gezielt vorgehen.
|
||||||
- `scrutiny` ist privilegiert und hat Device-Mounts.
|
- `scrutiny` ist privilegiert und hat Device-Mounts.
|
||||||
- `Plex-Media-Server` ist im Architekturziel als Host-Sonderfall dokumentiert, aber nicht als Repo-Compose-Stack enthalten.
|
- `Plex-Media-Server` ist im Architekturziel als Host-Sonderfall dokumentiert, aber nicht als Repo-Compose-Stack enthalten.
|
||||||
- Echte `stack.env`- und `.env`-Dateien gehoeren nicht ins Repo; fuer Hermes liegt nur `ops/hermes-agent/stack.env.example` im Git.
|
- Echte `stack.env`- und `.env`-Dateien gehoeren nicht ins Repo; fuer Hermes liegt nur `ops/hermes-agent/stack.env.example` im Git.
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Alerting Map
|
||||||
|
|
||||||
|
Stand: 2026-05-23
|
||||||
|
|
||||||
|
Ziel: Alle problemrelevanten Homelab-Meldungen landen auf einem Handy-Topic.
|
||||||
|
|
||||||
|
## ntfy Topics
|
||||||
|
|
||||||
|
| Topic | Zweck |
|
||||||
|
|---|---|
|
||||||
|
| `homelab-alerts` | Alles, was Aufmerksamkeit braucht: Prometheus, Docker-Events, Posture, Zertifikate/Token, Compose-Drift, Borg-Pre-Hook-Fehler und Restore-Fehler |
|
||||||
|
| `homelab-info` | Optionale Erfolgsmeldungen, z. B. erfolgreiche Restore-Testlaeufe |
|
||||||
|
|
||||||
|
## Sender
|
||||||
|
|
||||||
|
| Sender | Pfad | Problem-Topic | Hinweis |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Prometheus / Alertmanager | `monitoring/alertmanager/alertmanager.yml`, `monitoring/alertmanager-ntfy-bridge/bridge.py` | `homelab-alerts` | Zentrale Monitoring-Alerts via Bridge |
|
||||||
|
| Posture Check | `services/posture-check/posture-check.sh` | `homelab-alerts` | Warning und Critical gehen auf dasselbe Handy-Topic |
|
||||||
|
| Cert / Token Check | `services/posture-check/cert-token-check.sh` | `homelab-alerts` | Prueft produktive HTTPS-Domains und Cloudflare Token |
|
||||||
|
| Compose Runtime Drift | `services/posture-check/compose-runtime-drift.sh` | `homelab-alerts` | Meldet Abweichungen zwischen Repo-Compose und Runtime-Image |
|
||||||
|
| Docker Critical Events | `services/posture-check/docker-critical-events.sh` | `homelab-alerts` | Meldet Docker `die`, `oom` und `kill` Events |
|
||||||
|
| Borg Pre-Hook | `ops/borg-ui/scripts/pre-borg.sh` | `homelab-alerts` | Meldet Fehler vor Borg, z. B. Posture-, Dump- oder Restore-Freshness-Fehler |
|
||||||
|
| Restore Jobs | `ops/restore-tests/run-restore-job-with-ntfy.sh` | `homelab-alerts` | Erfolg geht an `homelab-info`, Fehler immer an `homelab-alerts` |
|
||||||
|
|
||||||
|
## Konvention
|
||||||
|
|
||||||
|
- `NTFY_BASE_URL` zeigt standardmaessig auf `https://ntfy.kaleschke.info`.
|
||||||
|
- Neue Problem-Alerts sollen `homelab-alerts` nutzen.
|
||||||
|
- Erfolgsmeldungen sind optional und sollen nicht in `homelab-alerts` landen, ausser sie sind bewusst als Lebenszeichen gewuenscht.
|
||||||
|
- Blackbox-Endpoint-Alerts sollen bekannte WAN-/Provider-Sammelausfaelle zusammenfassen, damit kurze DSL-Reconnects keine ntfy-Flut pro Domain erzeugen.
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
# Homelab Audit - 2026-05-23
|
||||||
|
|
||||||
|
Stand: 2026-05-23, repo-basiert. Erstellt nach `docs/WORKFLOW.md` und `docs/GITOPS_DRIFT_RUNBOOK.md`. Quellebasis: `origin/master` plus lokaler Clone, ohne Schreibbefehle, ohne Deploy.
|
||||||
|
|
||||||
|
Dieser Audit ist eine punktuelle Sollzustands-Bewertung, kein Live-Status. Die Live-Verifikations-Schritte stehen am Ende in Abschnitt 9; alle dortigen Outputs ersetzen Vermutungen durch Messwerte.
|
||||||
|
|
||||||
|
## 0. Executive Summary
|
||||||
|
|
||||||
|
Ampel-Bewertung pro Bereich:
|
||||||
|
|
||||||
|
| Bereich | Ampel | Kernaussage |
|
||||||
|
|---|---|---|
|
||||||
|
| GitOps-Konsistenz (lokal/Gitea) | 🟡 | Lokaler Clone ist **1 Commit voraus** auf `master` (`cd650b1`, Haertungs-Commit). Bis zum Push existiert dieser Stand nur lokal, nicht in Gitea — bei einem Clone-Verlust ist er weg. |
|
||||||
|
| GitOps-Konsistenz (Working Tree) | 🟢* | Keine echten Inhaltsaenderungen offen. Die 47 "modified files" aus `git status` im Linux-Mount sind voraussichtlich CRLF/LF-Mount-Artefakte (durch `git diff -w --stat` auf Stichprobe bestaetigt leer). Bitte am Windows-Host gegenpruefen. |
|
||||||
|
| Hardening-Sprint (Mai 2026) | 🟢 | Alle vier Post-Restore-Sprint-Items sind im Repo umgesetzt (Filebrowser-Mounts, Authelia Argon2id, Gitea Webhook-Allowlist, Backup-Dump-Konsistenz). |
|
||||||
|
| Backup/Restore-Readiness | 🟢 | `pre-backup-dumps.sh` deckt alle relevanten SQLite/PostgreSQL/Mongo-Quellen ab. Borg-UI-Scope umfasst `/mnt/user/services`, `homelab-infra`, `stacks`, `posture-check`. Live-Frische ist offen (Abschnitt 9). |
|
||||||
|
| Monitoring-Migration | 🟡 | `monitoring/` Stack im Repo komplett, aber Live-Deploy laut `docs/NEXT_SPRINT_TODO_2026-05-16.md` noch ausstehend. Alte Stacks `ops/grafana-influxdb` und `ops/loki` sollen erst nach Live-Smoke-Test gestoppt werden. |
|
||||||
|
| Doku-Drift Repo vs. Master-Doku | 🟠 | `apps/jellyfin/`, `host-services/plex/` und einige andere existieren produktiv als Compose-Stacks, sind aber in `HOMELAB_ARCHITECTURE_MASTER_V2.md`, `docs/SERVICE_CATALOG.md` und `docs/REPO_MAP.md` **nicht aufgefuehrt**. Authelia-ACL kennt `jellyfin.kaleschke.info` als bypass, im Masterdoku-Hostkatalog steht es nicht. |
|
||||||
|
| Repo-Hygiene | 🟡 | 8 leere Verzeichnisse im Working Tree (siehe 4.3). `.serena/` ist untracked und nicht in `.gitignore`. Drei `ops/windows-reinstall/*.ps1` sind untracked. |
|
||||||
|
| Bekannte Ausnahmen | 🟢 | Alle Ausnahmen aus `HOMELAB_ARCHITECTURE_MASTER_V2.md` Abschnitt 10 sind weiterhin dokumentiert und durch den Policy-Check abgedeckt (0 Critical, 4 Warnings, 9 Info – alles dokumentierte Ausnahmen). |
|
||||||
|
|
||||||
|
**Kernfazit:** Das Homelab ist sehr nah an der "Endstufe". Es gibt keine kritischen Befunde. Die einzige Pflichtaktion vor dem naechsten geplanten Schritt ist der **Push des lokalen Commits `cd650b1` nach Gitea**, damit `origin/master` wieder die Quelle der Wahrheit ist. Danach sind nur noch zwei priorisierte Pakete offen: Monitoring-Stack live finalisieren und Doku auf den Stand der neuen Stacks (Jellyfin/Plex/...) nachziehen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Methodik und Quellen
|
||||||
|
|
||||||
|
Diese Audit-Quellen wurden gelesen (repo-seitig):
|
||||||
|
|
||||||
|
- `HOMELAB_ARCHITECTURE_MASTER_V2.md`
|
||||||
|
- `docs/WORKFLOW.md`
|
||||||
|
- `docs/REPO_MAP.md`
|
||||||
|
- `docs/SERVICE_CATALOG.md`
|
||||||
|
- `docs/RESTORE_MATRIX.md`
|
||||||
|
- `docs/GITOPS_DRIFT_RUNBOOK.md`
|
||||||
|
- `docs/NEXT_SPRINT_TODO_2026-05-16.md`
|
||||||
|
- `ops/borg-ui/scripts/pre-backup-dumps.sh`
|
||||||
|
- `ops/borg-ui/docker-compose.yml`
|
||||||
|
- `ops/borg-ui/all-important-sources.txt`
|
||||||
|
- `ops/filebrowser/docker-compose.yml`
|
||||||
|
- `security/authelia/configuration.yml`
|
||||||
|
- `core/gitea/docker-compose.yml`
|
||||||
|
- `apps/jellyfin/docker-compose.yml`
|
||||||
|
- `host-services/plex/docker-compose.yml`
|
||||||
|
- `ops/policy-checks/last-report.md`
|
||||||
|
|
||||||
|
Schreibbefehle: keine. Deploys: keine. Containerlaufzeit, Komodo-Webhook-Status, Borg-Lauf-Frische und Host-Listener wurden bewusst nicht angetastet — dafuer steht der Live-Daten-Block in Abschnitt 9.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Schicht A — GitOps und Konsistenz
|
||||||
|
|
||||||
|
### 2.1 Lokaler Clone vs. `origin/master`
|
||||||
|
|
||||||
|
```
|
||||||
|
## master...origin/master [ahead 1]
|
||||||
|
HEAD = cd650b19ac057a1b74ac63503e5dba50eaf5b8ea
|
||||||
|
origin/master = af231dd4e835b19005cc0842509199d480af00d9
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Befund:** Lokaler Clone ist 1 Commit voraus.
|
||||||
|
- **Commit:** `cd650b1 Close Gitea signup, dedup posture-check alerts, extend Borg scope` (Sat May 23 11:01:24 2026 +0200).
|
||||||
|
- **Inhalt** (laut Commit-Message und betroffenen Dateien):
|
||||||
|
- Gitea: `DISABLE_REGISTRATION=true`, `ENABLE_OPENID_SIGNIN=false`, `ENABLE_OPENID_SIGNUP=false`
|
||||||
|
- Repo-Pflicht-Doku ergaenzt: Komodo-Stack-Webhook-Pflicht in `CLAUDE.md`, `AI_CONTEXT.md`, `WORKFLOW.md`
|
||||||
|
- `posture-check.sh`: Disk1-NTFS-Funktion ausgelagert, Inode-Check auf NTFS uebersprungen, ntfy-Dedup via Fingerprint-State + `ALERT_REPEAT_SECONDS`
|
||||||
|
- `docker-critical-events.sh`: JSON-Parsing, `die exit=0` gefiltert, strukturierte ntfy-Message
|
||||||
|
- `borg-ui`: `/mnt/user/services` als `/local/services:ro` gemountet, `all-important-sources.txt` ergaenzt
|
||||||
|
- Unraid User Scripts dokumentiert (daily report)
|
||||||
|
- `MIGRATION_LOG.md`, `RESTORE_MATRIX.md`, `DISASTER_RECOVERY.md` aktualisiert
|
||||||
|
- **Risiko:** Bei Verlust des Windows-Clones (Reinstall, Diskcrash) ist dieser Stand verloren, weil er nicht in Gitea liegt. Komodo deployt ausserdem aus Gitea und kennt diese Aenderungen noch nicht.
|
||||||
|
- **Empfohlene Aktion (Pflicht vor weiterer Arbeit):** In GitHub Desktop `Push origin` ausfuehren. Danach Komodo-Reaktion fuer die betroffenen Stacks (`gitea`, `borg-ui`) pruefen und Smoke-Tests laufen lassen.
|
||||||
|
|
||||||
|
### 2.2 Working-Tree-Status
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git status --short
|
||||||
|
M CLAUDE.md
|
||||||
|
M HOMELAB_ARCHITECTURE_MASTER_V2.md
|
||||||
|
M apps/homepage/docker-compose.yml
|
||||||
|
...
|
||||||
|
(47 Dateien als modified gemeldet, plus 4 Untracked)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Bewertung:** Die 47 "modified files" sind mit hoher Wahrscheinlichkeit **Mount-Artefakte** durch CRLF/LF zwischen Windows-Clone und Linux-Mount. Stichprobe `git diff -w --stat CLAUDE.md HOMELAB_ARCHITECTURE_MASTER_V2.md` lieferte leer — d. h. keine inhaltlichen Diffs.
|
||||||
|
- **Aktion:** Bitte am Windows-Host in GitHub Desktop `git status --short` ausfuehren. Wenn dort der Tree leer ist (nur die 4 Untracked), gibt es keinen echten Working-Tree-Drift. Wenn dort echte Diffs erscheinen, hier bitte zurueckmelden — dann ist das ein eigener Befund.
|
||||||
|
- **Optional (nicht Pflicht):** `.gitattributes` mit `* text=auto eol=lf` haerten, damit dieser Mount-Effekt fuer KI-Audits aus dem Weg geht. Das ist ein eigener kleiner Commit, kein Audit-Output.
|
||||||
|
|
||||||
|
### 2.3 Untracked Files
|
||||||
|
|
||||||
|
```
|
||||||
|
?? .serena/
|
||||||
|
?? ops/windows-reinstall/backup-delta-after-2026-05-07.ps1
|
||||||
|
?? ops/windows-reinstall/cleanup-dualboot-bcd.ps1
|
||||||
|
?? ops/windows-reinstall/repair-disk0-boot-to-new-windows.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
- `.serena/` ist das Working-Directory des Serena Code-Search-Tools. Hat eigene `.gitignore` intern, aber das `.serena/`-Verzeichnis selbst ist nicht in der Repo-`.gitignore`.
|
||||||
|
- **Aktion (klein):** `.serena/` in `.gitignore` aufnehmen, damit es nicht versehentlich committet wird.
|
||||||
|
- Die drei PowerShell-Scripts unter `ops/windows-reinstall/` sind Windows-Reinstall-Helfer. Entscheidung offen: ins Repo aufnehmen (mit Kontextkommentar warum sie dort liegen) oder lokal halten und in `.gitignore` aufnehmen. Vorschlag: aufnehmen, weil `ops/` der dokumentierte Ort fuer Ops-Skripte ist.
|
||||||
|
|
||||||
|
### 2.4 Letzte Commit-Historie (Top 10)
|
||||||
|
|
||||||
|
```
|
||||||
|
cd650b1 Close Gitea signup, dedup posture-check alerts, extend Borg scope [LOKAL, NICHT GEPUSHT]
|
||||||
|
af231dd Fix zero-count noise pattern handling
|
||||||
|
428223d Mark posture report scripts executable
|
||||||
|
b6d3ed4 Tune homelab availability alerts
|
||||||
|
9e7bebb Add daily operations report with hardened log-noise filtering
|
||||||
|
b7cbbe5 Fix Jellyfin external DNS
|
||||||
|
71ac18b Fix Jellyfin native auth routing
|
||||||
|
90f270b Fix Jellyfin config permissions
|
||||||
|
e28f8da Add Jellyfin media server stack
|
||||||
|
edfec5b Add Plex media server stack
|
||||||
|
```
|
||||||
|
|
||||||
|
- Die letzten Tage waren sichtbar: Jellyfin/Plex hinzugefuegt, Availability-Alerts feinjustiert, Posture-Check-Skripte produktiv gemacht, dann der grosse Haertungs-Commit gestern (2026-05-23 11:01).
|
||||||
|
|
||||||
|
### 2.5 Compose-Inventar vs. Doku
|
||||||
|
|
||||||
|
Repo hat folgende Compose-Stacks, die in den Doku-Quellen (`HOMELAB_ARCHITECTURE_MASTER_V2.md`, `docs/SERVICE_CATALOG.md`, `docs/REPO_MAP.md`) **nicht oder nur teilweise** aufgefuehrt sind:
|
||||||
|
|
||||||
|
| Stack | Status im Repo | Status in Master-Doku |
|
||||||
|
|---|---|---|
|
||||||
|
| `apps/jellyfin/docker-compose.yml` | produktiv vorhanden, gepinnt `jellyfin:10.11.8@sha256:...`, Traefik `jellyfin.kaleschke.info`, `secure-headers@file`, native Auth, `/mnt/user/media:ro` + `/mnt/user/photos:ro` | **fehlt** in 7.4 Apps; Authelia-ACL kennt aber bereits `jellyfin.kaleschke.info` als bypass — Doku hinkt hinterher |
|
||||||
|
| `host-services/plex/docker-compose.yml` | produktiv vorhanden, gepinnt `plexinc/pms-docker:1.43.1.10611-1e34174b1@sha256:...`, `network_mode: host`, `/mnt/user/media:ro` + `/mnt/user/photos:ro` | Master-Doku sagt explizit "Plex-Media-Server ist historischer Host-Sonderfall, nicht als Repo-Compose-Stack enthalten" — **das stimmt nicht mehr**, Plex ist jetzt ein Repo-Compose-Stack |
|
||||||
|
| `host-services/docker/` | leeres Verzeichnis | nicht erwaehnt |
|
||||||
|
| `infra/dns/` | leeres Verzeichnis | nicht erwaehnt |
|
||||||
|
| `ops/Semaphore/` | Skripten/Playbooks aber kein Compose | nicht erwaehnt |
|
||||||
|
| `ops/backrest/` | leeres Verzeichnis (Stack laut Master-Doku am 2026-05-15 entfernt) | korrekt als entfernt dokumentiert; Verzeichnis sollte leer bleiben oder weg |
|
||||||
|
| `apps/firefly/`, `apps/firefly-fints/` | leere Verzeichnisse | nicht erwaehnt |
|
||||||
|
| `apps/stirling-pdf/` | leeres Verzeichnis (durch `bentopdf` abgeloest) | korrekt als abgeloest dokumentiert |
|
||||||
|
|
||||||
|
- **Aktion (Doku-Synchronisierung):** `HOMELAB_ARCHITECTURE_MASTER_V2.md` Abschnitt 7 (Container-Zielbild), `docs/SERVICE_CATALOG.md` und `docs/REPO_MAP.md` um Jellyfin und Plex erweitern. Plex-Doku im Master umschreiben: nicht mehr "historisch ausserhalb Repo", sondern "Compose-Stack mit `network_mode: host` als VPN-Discovery-Ausnahme".
|
||||||
|
- **Aktion (Repo-Hygiene):** Die leeren Verzeichnisse `apps/firefly`, `apps/firefly-fints`, `apps/stirling-pdf`, `host-services/docker`, `infra/dns`, `ops/backrest`, `ops/grafana-influxdb/scripts`, `ops/Semaphore/playbooks`, `ops/Semaphore/Scripts` aufraeumen — Master-Doku sagt: "Leere `.keep`-Platzhalter wurden entfernt; neue Verzeichnisse sollen erst mit konkretem Inhalt ins Repo." Diese Verzeichnisse verletzen diese Regel passiv.
|
||||||
|
|
||||||
|
### 2.6 Image-Pinning
|
||||||
|
|
||||||
|
Lt. `docs/NEXT_SPRINT_TODO_2026-05-16.md` sind diese Stacks noch nicht voll versioniert gepinnt:
|
||||||
|
- `ddns-updater` — `latest...@sha256`
|
||||||
|
- `glances` — `latest-full@sha256`
|
||||||
|
- `scrutiny` — `latest-omnibus@sha256`
|
||||||
|
|
||||||
|
Das ist bewusst dokumentiert und kein Audit-Befund.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Schicht B — Hardening-Sprint 2026-05 (Sitrep)
|
||||||
|
|
||||||
|
Dies war der Sprint, der nach dem 2026-05 Restore explizit gesetzt wurde. Stand im Repo:
|
||||||
|
|
||||||
|
| Sprint-Item | Stand 2026-05-16 (Plan) | Stand 2026-05-23 (Repo) | Beleg |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **(1) Backup-Konsistenz** — `dump_sqlite_container` fuer Gitea/Vaultwarden/Uptime-Kuma/Speedtest/Filebrowser + `pg_dump` Nextcloud | offen | ✅ erledigt | `ops/borg-ui/scripts/pre-backup-dumps.sh` Z. 97–139 (`dump_sqlite_container`), Z. 253–258 (Nextcloud `pg_dump`), Z. 261–264 (alle SQLite-Container mit Host-Fallback), Z. 267 (Filebrowser BoltDB). Borg-Scope erweitert um `/mnt/user/services` (Borg-UI Compose Z. 26 + `all-important-sources.txt` Z. 23–25). |
|
||||||
|
| **(2) Filebrowser entschaerfen** — `/mnt/user/appdata:/srv/appdata` weg, gezielte RW-Subpfade | offen | ✅ erledigt | `ops/filebrowser/docker-compose.yml` Z. 11–16. Keine Appdata-Mounts mehr. Nur noch `/mnt/user/documents`, `/mnt/user/photos`, `/mnt/user/projekte` als Datenmounts plus eigener `/database` und `/config`. |
|
||||||
|
| **(3) Authelia Argon2id haerten** — iterations 3, memory 65536, parallelism 4 | offen | ✅ erledigt | `security/authelia/configuration.yml` Z. 17–25. Exakt die geplanten Parameter sind aktiv. |
|
||||||
|
| **(4) Gitea Webhook-Allowlist** — `ALLOWED_HOST_LIST=*` einschraenken | offen | ✅ erledigt | `core/gitea/docker-compose.yml` Z. 18: `GITEA__webhook__ALLOWED_HOST_LIST=komodo-core,localhost,127.0.0.1,192.168.178.0/24`. Zusatz aus heutigem Commit: Public Registration und OpenID-Signup/Signin sind deaktiviert. |
|
||||||
|
|
||||||
|
Alle vier Items sind **im Repo abgeschlossen**. Live-Wirksamkeit haengt am Komodo-Deploy aus Gitea — und genau da haengt aktuell der ungepushte Commit `cd650b1` davor (siehe 2.1). Solange er nicht in Gitea ist, ist insbesondere die Gitea-Signup-Schliessung im Live-Stand nicht garantiert.
|
||||||
|
|
||||||
|
**Bewusst nicht angefasste Liste (Operator-Entscheidung 2026-05-16) ist weiterhin gueltig:**
|
||||||
|
- Hermes — bleibt VM-seitig offen, NAS-Stack bewusst nicht starten
|
||||||
|
- Disk1 NTFS — Phase-2-Migration nach Plan
|
||||||
|
- Komodo native Auth ohne ForwardAuth
|
||||||
|
- Grafana/Influxdb3-core `user: "0"`
|
||||||
|
- Image-Pinning-Vereinheitlichung (`:latest@sha256:`) fuer ddns-updater/glances/scrutiny
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Schicht C — "Endstufe?"-Bewertung
|
||||||
|
|
||||||
|
### 4.1 Backup/Restore-Readiness
|
||||||
|
|
||||||
|
- **Dump-Coverage:** `pre-backup-dumps.sh` deckt 14 Quellen ab: PostgreSQL-Globals + 3 Shared-DBs + 3 dedizierte Postgres (mealie, immich, nextcloud) + 4 SQLite (gitea, vaultwarden, uptime-kuma, speedtest-tracker) + Filebrowser-BoltDB + Borg-UI + Grafana + Komodo-Mongo. Deckt 1:1 die Restore-Matrix-Eintraege ab.
|
||||||
|
- **Borg-Scope:** `all-important-sources.txt` enthaelt 27 Eintraege inkl. neuer `services/homelab-infra`, `services/stacks`, `services/posture-check` und `secrets`.
|
||||||
|
- **Restore-Validierungen:** Laut `docs/RESTORE_MATRIX.md` sind am 2026-05-07 Mini-Restores fuer `gitea`, `vaultwarden` und `paperless` validiert worden — dokumentierter Stand.
|
||||||
|
- **Live offen:** Wann lief der letzte Borg-Lauf? Sind alle Dumps unter `/mnt/user/backups/borg/dumps/latest` frischer als 24h? Siehe Live-Checkliste 9.4.
|
||||||
|
|
||||||
|
### 4.2 Monitoring-Migration
|
||||||
|
|
||||||
|
- Repo-Zielzustand `monitoring/docker-compose.yml` (337 Zeilen Compose) existiert mit Prometheus, Alertmanager, ntfy-Bridge, Blackbox-Exporter, Loki, Promtail, Grafana, node-exporter, cAdvisor, InfluxDB3 Core.
|
||||||
|
- Provisioning unter `monitoring/grafana/provisioning/` und `monitoring/prometheus/`, `monitoring/loki/`, `monitoring/promtail/`, `monitoring/alertmanager/`, `monitoring/blackbox/` vollstaendig vorhanden.
|
||||||
|
- Alte Stacks `ops/grafana-influxdb/` und `ops/loki/` bewusst noch im Repo (dokumentierter Altstand, Rollback-Referenz).
|
||||||
|
- **Live offen:** Ist `monitoring` schon als Komodo-Stack deployed? Laufen die Container? Sind die Secret-Dateien `monitoring_grafana_admin_password.txt`, `monitoring_grafana_influxdb_token.txt`, `influxdb3_admin_token.json` auf dem Host? Siehe Live-Checkliste 9.5.
|
||||||
|
|
||||||
|
### 4.3 Repo-Hygiene
|
||||||
|
|
||||||
|
| Befund | Schwere | Aktion |
|
||||||
|
|---|---|---|
|
||||||
|
| 8 leere Verzeichnisse (`apps/firefly`, `apps/firefly-fints`, `apps/stirling-pdf`, `host-services/docker`, `infra/dns`, `ops/backrest`, `ops/grafana-influxdb/scripts`, `ops/Semaphore/playbooks`, `ops/Semaphore/Scripts`) | klein | Aufraeumen, danach committen |
|
||||||
|
| `.serena/` untracked, nicht in `.gitignore` | klein | `.serena/` zu `.gitignore` hinzufuegen |
|
||||||
|
| 3 `ops/windows-reinstall/*.ps1` untracked | klein | Entscheidung treffen: ins Repo oder ignorieren |
|
||||||
|
|
||||||
|
### 4.4 Bekannte dokumentierte Ausnahmen
|
||||||
|
|
||||||
|
Aus `HOMELAB_ARCHITECTURE_MASTER_V2.md` Abschnitt 10 — alle weiterhin gueltig und durch den Policy-Check abgedeckt (`ops/policy-checks/last-report.md` 0 Critical):
|
||||||
|
|
||||||
|
- Traefik 80/443
|
||||||
|
- Tailscale Host-Netz + Capabilities
|
||||||
|
- AdGuard Port 53 + 8082 (Admin-Port LAN-only, dokumentiert; **offener Punkt im Master:** "Traefik-Absicherung ausstehend (Block F)" — bewusst spaeter)
|
||||||
|
- Plex Host-Netz (aber Master-Doku-Eintrag jetzt falsch, siehe 2.5)
|
||||||
|
- Scrutiny `privileged: true`
|
||||||
|
- Komodo Docker-Socket + keine pauschale Middleware
|
||||||
|
- glance-docker-socket-proxy Read-only Socket
|
||||||
|
- Gitea SSH 222
|
||||||
|
- ddns-updater `frontend_net`
|
||||||
|
- mail-archiver Hybrid-Netze
|
||||||
|
- `traefik/dynamic/*` manueller Host-Sync
|
||||||
|
- nextcloud native Auth
|
||||||
|
- monitoring-influxdb3-core LAN 8181 + `user: "0"`
|
||||||
|
- monitoring-promtail Docker-Socket read-only
|
||||||
|
|
||||||
|
Keine ungeplanten neuen Ausnahmen.
|
||||||
|
|
||||||
|
### 4.5 Endstufen-Definition
|
||||||
|
|
||||||
|
"Endstufe" ist erreicht, wenn alle folgenden Punkte gruen sind:
|
||||||
|
|
||||||
|
1. **Gitea = Quelle der Wahrheit** — kein lokaler Commit ohne Push 🟡 (heute: `cd650b1` ungepusht)
|
||||||
|
2. **Hardening-Sprint im Repo abgeschlossen** 🟢
|
||||||
|
3. **Backup-Konsistenz live verifiziert (Borg laeuft, Dumps frisch)** ❓ Live
|
||||||
|
4. **Monitoring-Stack live, alte Altstaende gestoppt** 🟡
|
||||||
|
5. **Doku synchron mit Repo (Jellyfin/Plex, leere Verzeichnisse, ...)** 🟠
|
||||||
|
6. **Policy-Check 0 Critical** 🟢 (4 Warnings sind dokumentierte Ausnahmen)
|
||||||
|
7. **Restore-Lab gepflegt (`mail-archiver` als naechste Uebung empfohlen)** 🟡 dokumentiert offen
|
||||||
|
|
||||||
|
Sechs der sieben Punkte sind in Reichweite ohne neue Architekturentscheidungen. Punkt 3 und 4 brauchen Live-Daten (Abschnitt 9). Punkt 1 ist 1 Push entfernt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Priorisierte Restliste
|
||||||
|
|
||||||
|
| Prio | Aktion | Begruendung | Aufwand |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **P0** | `cd650b1` nach Gitea pushen | GitOps-Quelle-der-Wahrheit, Voraussetzung fuer alles weitere | 30 Sekunden |
|
||||||
|
| **P0** | Live-Daten aus Abschnitt 9 einholen | Ohne Live-Frische ist Endstufen-Bewertung unvollstaendig | 5 Minuten |
|
||||||
|
| **P1** | Monitoring-Stack live finalisieren (Secrets pruefen, deployen, Smoke-Test, alte Altstaende stoppen) | Aus `docs/NEXT_SPRINT_TODO_2026-05-16.md` der naechste produktive Schritt | 1–2 Stunden mit Tests |
|
||||||
|
| **P2** | Doku-Drift schliessen: Jellyfin und Plex in `HOMELAB_ARCHITECTURE_MASTER_V2.md` 7.4 + 7.1, `docs/SERVICE_CATALOG.md`, `docs/REPO_MAP.md` ergaenzen; Plex-Eintrag in Abschnitt 7.7 "noch offene Sonderfaelle" entfernen (ist umgesetzt) | Doku ist Source of Truth fuer KI-Audits und Nachfolge | 30 Minuten |
|
||||||
|
| **P2** | Home Assistant -> InfluxDB final testen, HA-Dashboard in `monitoring-grafana` anlegen | aus NEXT_SPRINT_TODO | 1–2 Stunden |
|
||||||
|
| **P3** | Repo-Hygiene: 8 leere Verzeichnisse loeschen, `.serena/` in `.gitignore`, Entscheidung zu `ops/windows-reinstall/*.ps1` | minor, aber dokumentiert | 15 Minuten |
|
||||||
|
| **P3** | Naechster Restore-Lab-Lauf: `mail-archiver` (empfohlen in `RESTORE_MATRIX.md`) | Restore-Routine ueben, bevor sie gebraucht wird | 1 Stunde |
|
||||||
|
| **P4** | `.gitattributes` mit `* text=auto eol=lf` hinzufuegen, um CRLF/LF-Mount-Effekte bei KI-Audits zu vermeiden | klein, kosmetisch fuer kuenftige Audits | 5 Minuten |
|
||||||
|
| **bleibt** | Hermes VM-Seite, Disk1-NTFS Phase 2, AdGuard Admin-Port hinter Traefik (Block F), Image-Pinning ddns/glances/scrutiny | bewusste Operator-Entscheidung, kein Audit-Beduerfnis | nicht jetzt |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Was bewusst NICHT angetastet wurde (Audit-Verzicht)
|
||||||
|
|
||||||
|
Konsistent mit der bekannten Nicht-Anfassen-Liste:
|
||||||
|
|
||||||
|
- Hermes (VM-seitig offen)
|
||||||
|
- Disk1 NTFS (Phase-2-Migration nach Plan)
|
||||||
|
- Komodo native Auth ohne ForwardAuth
|
||||||
|
- Grafana / influxdb3-core `user: "0"` Uebergangsausnahme
|
||||||
|
- Image-Pinning-Vereinheitlichung fuer ddns-updater/glances/scrutiny
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Risiken und Drift-Indikatoren
|
||||||
|
|
||||||
|
| Risiko | Wahrscheinlichkeit | Wirkung | Migitation |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Lokaler Clone-Verlust (Disk, Reinstall) bevor `cd650b1` gepusht wurde | gering, aber real (Du bist im Reinstall-Kontext, siehe `ops/windows-reinstall/`!) | Verlust von Gitea-Signup-Closure und Posture-Check-Verbesserungen | **Sofort pushen** |
|
||||||
|
| Komodo deployt aus Gitea, `gitea` und `borg-ui` laufen aktuell ohne die heutigen Verbesserungen | mittel | Gitea Signup steht noch offen, Borg-Scope umfasst `/mnt/user/services` noch nicht | Push + Komodo-Reaktion pruefen |
|
||||||
|
| 47 vermeintlich modified files koennten doch echte Diffs sein, wenn der Windows-Host etwas anderes zeigt | gering | falsche Audit-Aussage | Punkt 9.1 auf dem Windows-Host pruefen |
|
||||||
|
| Doku-Drift wird groesser, wenn weitere Stacks ohne Doku-Update hinzukommen | mittel | KI-Audits und Onboarding leiden | P2-Doku-Sync nicht aufschieben |
|
||||||
|
| Monitoring-Stack-Migration unfertig, alter und neuer Stack koennten parallel werden | mittel | Doppelte Metric-/Log-Pipeline, Verwirrung bei Diagnose | Live-Status klaeren bevor Deploy |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Sources of Truth — Schnellzugriff
|
||||||
|
|
||||||
|
- Operative Quelle der Wahrheit: Gitea `origin/master` (https://git.kaleschke.info/Micha/homelab-infra)
|
||||||
|
- Architektur-Master: `HOMELAB_ARCHITECTURE_MASTER_V2.md`
|
||||||
|
- Workflow / GitOps-Regeln: `docs/WORKFLOW.md`
|
||||||
|
- Drift-Runbook: `docs/GITOPS_DRIFT_RUNBOOK.md`
|
||||||
|
- Restore-Quellen: `docs/RESTORE_MATRIX.md`, `docs/DISASTER_RECOVERY.md`
|
||||||
|
- Letzter Policy-Check: `ops/policy-checks/last-report.md` (0 Critical)
|
||||||
|
- Letzte Sprint-Restliste: `docs/NEXT_SPRINT_TODO_2026-05-16.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Live-Daten-Checkliste — bitte ausfuehren und zurueckspielen
|
||||||
|
|
||||||
|
Fuehre die folgenden Bloecke am Unraid-Host (per SSH oder Web-Terminal) und am Windows-Host (Git Bash / PowerShell in `G:\Gitea_Clone\homelab-infra`) aus und pastiere die Outputs zurueck. Ich integriere sie dann in diesen Report.
|
||||||
|
|
||||||
|
### 9.1 Windows-Host: Echter Working-Tree-Status
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd G:\Gitea_Clone\homelab-infra
|
||||||
|
git status --short
|
||||||
|
git log origin/master..HEAD --oneline
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: Working tree leer (oder nur die 4 Untracked). `cd650b1` als einziger lokaler Commit.
|
||||||
|
|
||||||
|
### 9.2 Unraid-Host: Gitea online
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sI --max-time 5 https://git.kaleschke.info/ | head -5
|
||||||
|
docker exec gitea sh -lc 'gitea --version'
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: `HTTP/2 200` (oder ein Auth-Code, der den Erreichbarkeitstest erfuellt). Gitea-Version stimmt mit Image-Tag `1.25.4` ueberein.
|
||||||
|
|
||||||
|
### 9.3 Unraid-Host: Komodo-Webhook-Status
|
||||||
|
|
||||||
|
In Komodo UI fuer jeden produktiven Stack aus `Micha/homelab-infra` pruefen:
|
||||||
|
- `webhook_enabled: true`
|
||||||
|
- Gitea-Hook auf `http://komodo-core:9120/listener/github/stack/<stack-id>/deploy` aktiv
|
||||||
|
- `last_status` der letzten Webhook-Delivery in Gitea (Repository -> Settings -> Webhooks)
|
||||||
|
|
||||||
|
Pflicht-Stacks zum Pruefen: `traefik`, `gitea`, `authelia`, `vaultwarden`, `postgresql17`, `redis`, `paperless-ngx`, `immich`, `nextcloud`, `mealie`, `mail-archiver`, `ntfy`, `homepage`, `paperless-gpt`, `borg-ui`, `filebrowser`, `code-server`, `uptime-kuma`, `glance`, `glances`, `scrutiny`, `speedtest-tracker`, `bentopdf`, `ddns-updater`, `komodo`, `jellyfin`, `plex`, `adguard`, `tailscale`, `monitoring`, `hermes-agent` (sofern produktiv).
|
||||||
|
|
||||||
|
Bei Stacks **ohne** aktiven Webhook bitte den Grund vermerken (dokumentierte Ausnahme oder Nachholbedarf).
|
||||||
|
|
||||||
|
### 9.4 Unraid-Host: Borg-Lauf-Frische und Dump-Coverage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -lah /mnt/user/backups/borg/dumps/latest/
|
||||||
|
stat -c '%y %n' /mnt/user/backups/borg/dumps/latest/*.dump /mnt/user/backups/borg/dumps/latest/*.sql /mnt/user/backups/borg/dumps/latest/*.archive.gz /mnt/user/backups/borg/dumps/latest/*.sqlite 2>/dev/null | sort
|
||||||
|
docker exec borg-ui borg list --short 2>&1 | tail -10
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: Alle 14 Artefakte aus 4.1 sind vorhanden, mtime juenger als 24h. Borg-Archive-Liste zeigt regelmaessige Laeufe.
|
||||||
|
|
||||||
|
### 9.5 Unraid-Host: Monitoring-Stack live?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" | grep monitoring
|
||||||
|
ls -la /mnt/user/appdata/secrets/ | grep -E 'monitoring|influxdb3'
|
||||||
|
curl -sI --max-time 5 https://monitoring.kaleschke.info/ | head -5
|
||||||
|
ss -ltnp 2>/dev/null | grep -E ':8181|:9090|:3100' | head -10
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: Entweder alle `monitoring-*` Container laufen (dann ist 4.2 🟢) oder gar nicht (dann ist 4.2 🟡 wie aktuell bewertet). Halb-Zustand ist ein Audit-Befund.
|
||||||
|
|
||||||
|
### 9.6 Unraid-Host: GitOps-Pflichtmatrix Spot-Check fuer einen Stack
|
||||||
|
|
||||||
|
Beispiel `gitea` (weil der heutige Commit ihn betrifft):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/user/services/stacks/gitea
|
||||||
|
git rev-parse --short HEAD
|
||||||
|
git status -sb
|
||||||
|
docker inspect gitea --format '{{.Image}}'
|
||||||
|
docker exec gitea sh -lc 'env | grep -E "ALLOWED_HOST_LIST|DISABLE_REGISTRATION|ENABLE_OPENID"'
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: Komodo-Workspace `HEAD` zeigt entweder auf `cd650b1` (wenn Push schon erfolgt + Komodo deployed) oder auf `af231dd` (vor dem Push). ENV-Vars in der Live-Runtime spiegeln den Commit, der Komodo zuletzt deployed hat.
|
||||||
|
|
||||||
|
### 9.7 Unraid-Host: Host-Listener-Spot-Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ss -ltnp 2>/dev/null | grep -E ':80|:443|:53|:222|:8082|:8181' | sort
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet exakt die dokumentierten Ausnahmen aus `HOMELAB_ARCHITECTURE_MASTER_V2.md` Abschnitt 10. Andere Listener = Befund.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Nachhalte-Vorschlag
|
||||||
|
|
||||||
|
Wenn Du moechtest, halte ich diesen Audit in zwei Schritten zu Ende:
|
||||||
|
|
||||||
|
1. Du fuehrst Abschnitt 9 aus und pastierst die Outputs zurueck.
|
||||||
|
2. Ich aktualisiere diesen Report mit den Live-Ergebnissen, ergaenze die Ampel und schliesse die offenen 🟡/🟠 Punkte oder benenne sie als echte Restliste.
|
||||||
|
|
||||||
|
Bis dahin gilt der Stand dieses Reports als Repo-Audit, nicht als Endstufen-Zertifikat.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Homelab Audit Final - 2026-05-23
|
||||||
|
|
||||||
|
Stand: 2026-05-25 07:33 CEST. Ergebnis nach Push, Live-Messung, Doku-Sync, Repo-Hygiene und erneuter Live-Nachmessung.
|
||||||
|
|
||||||
|
| Punkt | Ampel | Beleg |
|
||||||
|
|---|---|---|
|
||||||
|
| P0 `cd650b1` nach Gitea pushen | gruen | Push `af231dd..cd650b1 master -> master`; produktiver Runtime-Stand `66ee10c` enthaelt `cd650b1`. Die abschliessenden Audit-Doku-Commits liegen in Gitea; der runtime-relevante Stack-Inhalt fuer `gitea`, `borg-ui` und `monitoring` ist seit `66ee10c` unveraendert. |
|
||||||
|
| P0 Live-Daten ablegen | gruen | `docs/AUDIT_2026-05-23_LIVE.md` angelegt, keine Secret-Werte dokumentiert. |
|
||||||
|
| P1 Monitoring live / Altstaende down | gruen | 10 `monitoring-*` Container laufen, `0` unhealthy, `0` starting, `0` stopped; alte `grafana`/`influxdb3-core`/`loki`/`alloy` Container sind nicht vorhanden. `monitoring.kaleschke.info` liefert 302 zu Authelia, Prometheus ist bereit, Loki `/ready` liefert `ready`. |
|
||||||
|
| P1 Jellyfin/Plex Doku | gruen | `HOMELAB_ARCHITECTURE_MASTER_V2.md`, `docs/SERVICE_CATALOG.md` und `docs/REPO_MAP.md` ergaenzt; Plex ist als Repo-Compose-Stack dokumentiert, nicht mehr als nicht migrierter Dockerman-Sonderfall. |
|
||||||
|
| P2 Borg-Frische | gruen | Borg-UI DB: letzter Backup-Job `completed`, Archiv `Taegliche-Sicherung-2026-05-25T05:52:44.157`, `nfiles=100221`; 15 kanonische Dump-/Archive-Artefakte von 2026-05-25 06:09/06:10 CEST und damit juenger als 24 h. |
|
||||||
|
| P3 Repo-Hygiene | gruen | `.serena/` in `.gitignore`; leere Verzeichnisse entfernt; `ops/windows-reinstall/*.ps1` bewusst ins Repo aufgenommen. |
|
||||||
|
| Policy-Check | gruen | `ops/policy-checks/check_repo.ps1`: 0 Critical, dokumentierte Warnings zu Plex Host-Netz, InfluxDB/Grafana `user: "0"` und bekannten mutable-tag-Ausnahmen. |
|
||||||
|
|
||||||
|
## Bewusst offen
|
||||||
|
|
||||||
|
- Keine offenen Punkte aus der Audit-Restliste.
|
||||||
|
- Nicht Teil des Audits: Nach Disk1 Phase 2 laeuft seit 2026-05-25 eine nicht korrigierende Parity-Pruefung (`NOCORRECT`) im Hintergrund; Zwischenstand der Nachmessung: `mdResyncAction=check P`, `mdResyncCorr=0`.
|
||||||
|
|
||||||
|
## Schlussbewertung
|
||||||
|
|
||||||
|
Das Homelab ist fuer die Audit-Restliste in der Endstufe. Es gibt keine kritischen Repo-, GitOps- oder Live-Befunde aus diesem Audit. Monitoring ist produktiv und ready, alte Altstaende sind down, Backups und Dumps sind frisch.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Homelab Audit Live-Daten - 2026-05-23
|
||||||
|
|
||||||
|
Stand: 2026-05-23 11:27 CEST. Quelle: lokaler Windows-Clone und SSH auf `Kallilabcore`. Secret-Werte wurden nicht ausgelesen oder redaktiert; dokumentiert sind nur Status, Dateinamen, Modi, Env-Key-Namen und nicht geheime Bool-/Host-Werte.
|
||||||
|
|
||||||
|
## 9.1 Windows-Host / Git
|
||||||
|
|
||||||
|
- `git status --short` nach dem initialen Push: keine tracked Modifikationen, untracked waren `.serena/`, `docs/AUDIT_2026-05-23.md`, `docs/CODEX_ENDSTUFE_PROMPT_2026-05-23.md` und drei `ops/windows-reinstall/*.ps1`.
|
||||||
|
- `cd650b1` wurde nach `origin/master` gepusht: `af231dd..cd650b1 master -> master`.
|
||||||
|
|
||||||
|
## 9.2 Gitea online
|
||||||
|
|
||||||
|
- `curl -sI https://git.kaleschke.info/`: `HTTP/2 200`.
|
||||||
|
- `docker exec gitea gitea --version`: `1.25.4`.
|
||||||
|
- Signup-Smoke: `/user/sign_up` meldet Registration disabled.
|
||||||
|
|
||||||
|
## 9.3 Komodo / Workspace-Reaktion
|
||||||
|
|
||||||
|
| Stack | Workspace HEAD | Status | Live-Beleg |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| `gitea` | `cd650b1` | `## master...origin/master` | Container neu gestartet, Env-Keys aktiv |
|
||||||
|
| `borg-ui` | `cd650b1` | `## master...origin/master` | Container healthy, `/mnt/user/services -> /local/services` gemountet |
|
||||||
|
| `monitoring` | `cd650b1` | `## master...origin/master`, untracked Host-Backup `monitoring/prometheus/alerts.yml.bak-20260520-incident` | Stack laeuft seit 4 Tagen |
|
||||||
|
|
||||||
|
Gitea Live-Env ohne Secrets:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GITEA__service__DISABLE_REGISTRATION=true
|
||||||
|
GITEA__openid__ENABLE_OPENID_SIGNIN=false
|
||||||
|
GITEA__openid__ENABLE_OPENID_SIGNUP=false
|
||||||
|
GITEA__webhook__ALLOWED_HOST_LIST=komodo-core,localhost,127.0.0.1,192.168.178.0/24
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9.4 Borg-Lauf-Frische und Dump-Coverage
|
||||||
|
|
||||||
|
- Borg UI DB: Repository `appdata-critical`, `last_backup=2026-05-23 02:30:12 UTC`, `archive_count=30`, letzter Job `completed`, `nfiles=100056`.
|
||||||
|
- Schedule: `Taegliche Sicherung` enabled, letzter Lauf `2026-05-23 02:30:11 UTC`, naechster Lauf `2026-05-24 02:30:00 UTC`.
|
||||||
|
- Hinweis: `docker exec borg-ui borg list --short` funktioniert ohne Repository-Parameter/Env nicht (`Invalid location format: ""`). Der Borg-UI-Status wurde deshalb ueber die lokale Borg-UI-Datenbank ohne Secret-Spalten ausgewertet.
|
||||||
|
|
||||||
|
Frische Artefakte in `/mnt/user/backups/borg/dumps/latest`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
2026-05-23 04:00 postgresql17-globals.sql
|
||||||
|
2026-05-23 04:00 postgresql17-mailarchiver.dump
|
||||||
|
2026-05-23 04:00 postgresql17-paperless.dump
|
||||||
|
2026-05-23 04:01 postgresql17-authelia.dump
|
||||||
|
2026-05-23 04:01 mealie.dump
|
||||||
|
2026-05-23 04:01 gitea.sqlite.dump
|
||||||
|
2026-05-23 04:01 immich.dump
|
||||||
|
2026-05-23 04:01 nextcloud.dump
|
||||||
|
2026-05-23 04:01 uptime-kuma.sqlite.dump
|
||||||
|
2026-05-23 04:01 vaultwarden.sqlite.dump
|
||||||
|
2026-05-23 04:01 speedtest-tracker.sqlite.dump
|
||||||
|
2026-05-23 04:01 filebrowser.bolt.dump
|
||||||
|
2026-05-23 04:01 borg-ui.sqlite
|
||||||
|
2026-05-23 04:01 grafana.sqlite
|
||||||
|
2026-05-23 04:01 komodo-mongo.archive.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
Bewertung: 15 aktuelle Dump-/Archive-Artefakte sind juenger als 24 h. Aeltere nackte `.sqlite`-Dateien vom 2026-05-16 liegen noch im Verzeichnis, sind aber Legacy-Kopien ohne `.dump`-Suffix und nicht Teil der aktuellen Dump-Serie.
|
||||||
|
|
||||||
|
## 9.5 Monitoring-Stack
|
||||||
|
|
||||||
|
- Aktive Container: `monitoring-grafana`, `monitoring-promtail`, `monitoring-prometheus`, `monitoring-cadvisor`, `monitoring-influxdb3-core`, `monitoring-loki`, `monitoring-blackbox-exporter`, `monitoring-alertmanager-ntfy-bridge`, `monitoring-alertmanager`, `monitoring-node-exporter`.
|
||||||
|
- Alte Altcontainer `grafana`, `influxdb3-core`, `loki`, `alloy`: nicht vorhanden in `docker ps -a`.
|
||||||
|
- Secret-Dateien vorhanden und mode `600`: `monitoring_grafana_admin_password.txt`, `monitoring_grafana_influxdb_token.txt`, `influxdb3_admin_token.json`.
|
||||||
|
- `https://monitoring.kaleschke.info/`: `HTTP/2 302` zu Authelia, wie erwartet.
|
||||||
|
- Host-Listener fuer Monitoring: nur `127.0.0.1:8181`; keine Host-Listener fuer `:9090` oder `:3100`.
|
||||||
|
- Prometheus readiness: `Prometheus Server is Ready.`
|
||||||
|
- Grafana health: `database=ok`, Version `12.4.3`.
|
||||||
|
- Loki: Container laeuft und API/metrics liefert `loki_build_info`; `/ready` liefert aktuell `503 Service Unavailable`, waehrend Query-/Metrics-Endpunkte 200-Zaehler zeigen. Kein Reparaturversuch gestartet, weil der produktive Logpfad nachweislich Daten annimmt und die Stop-Regel keine blinden Eingriffe erlaubt.
|
||||||
|
|
||||||
|
## 9.6 GitOps Spot-Check Gitea
|
||||||
|
|
||||||
|
- `/mnt/user/services/stacks/gitea`: `cd650b1`, `## master...origin/master`.
|
||||||
|
- Docker-Image: `docker.gitea.com/gitea:1.25.4`.
|
||||||
|
- Live-ENV spiegelt `cd650b1` fuer Registrierung, OpenID und Webhook-Allowlist.
|
||||||
|
|
||||||
|
## 9.7 Host-Listener
|
||||||
|
|
||||||
|
Dokumentierte Listener gefunden:
|
||||||
|
|
||||||
|
- `:80`, `:443` Traefik
|
||||||
|
- `:53` AdGuard DNS
|
||||||
|
- `:222` Gitea SSH
|
||||||
|
- `:8082` AdGuard Admin
|
||||||
|
- `127.0.0.1:8181` Monitoring InfluxDB
|
||||||
|
|
||||||
|
Zusaetzlich sichtbar: mehrere `wsdd2` Listener auf `:5355` je Interface. Das ist Host-/Unraid-Service-Discovery, kein Compose-Webdienst und kein GitOps-Stack-Port.
|
||||||
@@ -0,0 +1,895 @@
|
|||||||
|
# Homelab-Audit KalliLab CORE
|
||||||
|
|
||||||
|
Stand: 2026-05-25
|
||||||
|
Methode: Repo-basierter Audit auf `master` (lokaler Clone). Keine Live-Messung gegen den Host. Querverweise auf Audit-Live-Daten aus `docs/AUDIT_2026-05-23_LIVE.md`, wo verfuegbar.
|
||||||
|
Auftrag: externer, kritischer Audit-Blick zusaetzlich zur internen `docs/STRATEGISCHE_BEWERTUNG_2026-05-23.md`.
|
||||||
|
|
||||||
|
## Wichtige Vorbemerkung
|
||||||
|
|
||||||
|
Es gibt seit dem 23.05. eine fundierte interne Bewertung (`docs/STRATEGISCHE_BEWERTUNG_2026-05-23.md`) und eine konsolidierte Hausaufgabenliste (`docs/CODEX_KONSOLIDIERUNG_2026-05-23.md`). Davon wurden seit dem 25.05. bereits umgesetzt:
|
||||||
|
|
||||||
|
- Jellyfin entfernt (MASTER 7.8)
|
||||||
|
- Homepage entfernt (MASTER 7.8)
|
||||||
|
- Uptime-Kuma entfernt (MASTER 7.8, SECRETS_MAP 29)
|
||||||
|
- Monitoring-Stack produktiv (AUDIT_FINAL 9), Altstaende-Container down
|
||||||
|
- Disk1 NTFS -> XFS Phase 2 abgeschlossen am 2026-05-25 (STORAGE_LAYOUT 3)
|
||||||
|
- Unraid-Flash-Backup live (`unraid-flash-config.tar.gz` im Borg-Lauf)
|
||||||
|
- GitHub-Push-Mirror `michaelkaleschke-spec/homelab-infra` aktiv (DR 10, MASTER 7.1)
|
||||||
|
|
||||||
|
Diese geloesten Punkte werden hier nicht wiederholt. Dieser Audit konzentriert sich auf das, was nach dem 23.05.-Sprint **noch offen ist** und auf das, was die strategische Bewertung **nicht oder nur kurz angerissen** hat.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 1: Repo-Inventar
|
||||||
|
|
||||||
|
## Ordnerstruktur (Ist-Zustand)
|
||||||
|
|
||||||
|
```
|
||||||
|
homelab-infra/
|
||||||
|
├── apps/ 9 Stacks (bentopdf, immich, mail-archiver, mealie, nextcloud, ntfy, paperless, paperless-gpt, unbound)
|
||||||
|
├── core/ 1 Stack (gitea)
|
||||||
|
├── docs/ 28 Markdown-Dokumente
|
||||||
|
├── env/ 2 *.example
|
||||||
|
├── host-services/ 3 Stacks (Adguard, plex, tailscale)
|
||||||
|
├── infra/ 3 Stacks (ddns-updater, postgresql17, redis)
|
||||||
|
├── monitoring/ 1 Compose mit 10 Services + Provisioning
|
||||||
|
├── ops/ 17 Verzeichnisse (Semaphore, borg-ui, code-server, filebrowser, glance, glances, grafana-influxdb [Altstand], hermes-agent, komodo, loki [Altstand], policy-checks, restore-tests, scrutiny, speedtest, uptime-kuma [Altrest], windows-reinstall)
|
||||||
|
├── security/ 2 Stacks (authelia, vaultwarden)
|
||||||
|
├── services/ 1 posture-check (Host-Skripte)
|
||||||
|
└── traefik/ 1 Compose + dynamic/ (3 Files)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Inventar-Befund:**
|
||||||
|
|
||||||
|
- ~30 Compose-Dateien, 1 zentraler Compose-Multi-Service (`monitoring/`).
|
||||||
|
- 29 Composes wurden vom Policy-Checker validiert (`ops/policy-checks/last-report.md`): **0 Critical, 4 Warnings, 9 Info**.
|
||||||
|
- Doku-Dichte ist hoch (REPO_MAP, SERVICE_CATALOG, RESTORE_MATRIX, DISASTER_RECOVERY, SECRETS_MAP, WORKFLOW, STORAGE_LAYOUT, GITOPS_DRIFT_RUNBOOK, ALERTING_MAP).
|
||||||
|
- Restore-Tests sind als echte Scripts versioniert (`ops/restore-tests/`). Ueberdurchschnittlich.
|
||||||
|
|
||||||
|
## Gut dokumentierte Bereiche (Belegt)
|
||||||
|
|
||||||
|
| Bereich | Quelle |
|
||||||
|
|---|---|
|
||||||
|
| Architektur, Netze, Ausnahmen | `HOMELAB_ARCHITECTURE_MASTER_V2.md` |
|
||||||
|
| GitOps-Workflow, Drift | `docs/WORKFLOW.md`, `docs/GITOPS_DRIFT_RUNBOOK.md` |
|
||||||
|
| Backup-Scope, Restore-Wege, Tier-Modell | `docs/RESTORE_MATRIX.md`, `docs/DISASTER_RECOVERY.md`, `ops/borg-ui/BACKUP_SCOPE.md` |
|
||||||
|
| Storage / Cache-Policy / FS / Posture | `docs/STORAGE_LAYOUT.draft.md` |
|
||||||
|
| Secret-Inventur | `docs/SECRETS_MAP.md` |
|
||||||
|
| Alert-Pfade | `docs/ALERTING_MAP.md` |
|
||||||
|
|
||||||
|
## Luecken / Unklar (vermutet bzw. Rueckfrage noetig)
|
||||||
|
|
||||||
|
| Luecke | Warum es ein Audit-Loch ist |
|
||||||
|
|---|---|
|
||||||
|
| `docs/STORAGE_LAYOUT.draft.md` ist `Draft 1.3`, nicht `Active` | Mehrere Hard Rules (12 Constitution) gelten formal noch nicht. Hard Rule 11 (kein Stack ohne Restore-Pfad in RESTORE_MATRIX) wird heute schon eingehalten — also nur Formal-Luecke. |
|
||||||
|
| `docs/SERVICES_RECOVERY.md` ist als verbindlich angekuendigt (STORAGE_LAYOUT 4), aber nicht im Repo | Konkrete Mirror-Mechanik fuer `services/gitea/git/repositories/` ≤ 6 h ist nirgends spezifiziert. |
|
||||||
|
| Hardware-Inventar: kein zentrales Dokument | Keine Stelle im Repo nennt CPU-Modell, RAM-Groesse, NIC-Speed, Mainboard, Parity-Disk-Groessen — nur "Samsung 970 EVO Plus 2 TB" steht in STORAGE_LAYOUT 3. |
|
||||||
|
| USV: keine Erwaehnung | Keine Datei nennt eine USV. Unklar, ob vorhanden. |
|
||||||
|
| Familien-/User-Onboarding-Doku | Keine Doku, die deine Frau/Kinder lesen muessten ("So loggst du dich in Nextcloud ein"). Aktuell ist alles Operator-Doku. |
|
||||||
|
|
||||||
|
## Fuer den Audit besonders wichtige Dateien (verwendet)
|
||||||
|
|
||||||
|
- `HOMELAB_ARCHITECTURE_MASTER_V2.md` — komplett
|
||||||
|
- `docs/WORKFLOW.md`, `docs/REPO_MAP.md`, `docs/SERVICE_CATALOG.md` — komplett
|
||||||
|
- `docs/DISASTER_RECOVERY.md`, `docs/RESTORE_MATRIX.md`, `docs/SECRETS_MAP.md` — komplett
|
||||||
|
- `docs/STORAGE_LAYOUT.draft.md`, `docs/STRATEGISCHE_BEWERTUNG_2026-05-23.md` — komplett
|
||||||
|
- `docs/AUDIT_2026-05-23_LIVE.md`, `docs/AUDIT_2026-05-23_FINAL.md`
|
||||||
|
- `ops/policy-checks/last-report.md`
|
||||||
|
- `monitoring/docker-compose.yml`, `monitoring/prometheus/alerts.yml`
|
||||||
|
- `traefik/docker-compose.yml`, `traefik/dynamic/middlewares.yml`
|
||||||
|
- `security/authelia/configuration.yml`, `security/authelia/docker-compose.yml`
|
||||||
|
- `apps/paperless/docker-compose.yml`, `apps/immich/docker-compose.yml`, `apps/nextcloud/docker-compose.yml`
|
||||||
|
- `host-services/Adguard/docker-compose.yml`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# A. Executive Summary
|
||||||
|
|
||||||
|
**Was schon stark ist:**
|
||||||
|
|
||||||
|
- GitOps-Disziplin: Gitea als Sollzustand, Komodo als Consumer, dokumentierter Drift-Runbook, Stop-Regel ("zwei Fehlversuche -> Pflichtmatrix"). Seltene Reife.
|
||||||
|
- Backup-Architektur: Pre-Backup-Dumps + Borg + Restore-Tests mit echtem Smoke-Test-Kriterium ("Container startet ≠ Erfolg"). 15 frische Dumps < 24 h alt (`AUDIT_LIVE 9.4`).
|
||||||
|
- Architektur-Klarheit: `frontend_net` / `backend_net` / app-interne Netze, keine Sammelnetze, dokumentierte Ausnahmen.
|
||||||
|
- Image-Pinning: Tier-1-Stateful mit `<minor>@sha256:...`. Konsequent durchgezogen.
|
||||||
|
- Secrets-Hygiene: Keine Secret-Werte im Repo, `_FILE`-Mounts + Komodo Stack ENV, explizit dokumentierte Ausnahmen.
|
||||||
|
- Policy-as-Code: `check_repo.ps1` mit 0 Critical und sauber dokumentierten Exceptions.
|
||||||
|
|
||||||
|
**Was kritisch ist:**
|
||||||
|
|
||||||
|
- **AdGuard Admin-Port 8082 ohne Authelia/2FA am LAN gebunden** (`host-services/Adguard/docker-compose.yml:16`) — dokumentierte "Operator-Entscheidung" 2026-05-25. Im Heim-LAN tolerierbar, mit IoT/Gaeste-WLAN potenziell ein Pfad zur DNS-Manipulation. Niedrigster Aufwand: Bind nur auf Tailscale-Interface.
|
||||||
|
- **Authelia ACL: 2FA nur fuer `files.kaleschke.info` und `scrutiny.kaleschke.info`** (`security/authelia/configuration.yml:44-48`). Borg-UI, Code-Server, Filebrowser, Glance — alles nur `one_factor`. Bei Pwd-Kompromittierung des Operator-Accounts ist Borg-UI + Code-Server der direkteste Pfad zur Datenexfiltration.
|
||||||
|
- **Authelia-Repo-Baseline ↔ Host-Config-Drift "by design"** (`docs/REPO_MAP.md:48`, `SERVICE_CATALOG 23`). User-DB, OIDC-Clients und Secrets sind hostseitig, Manual-Merge-Pflicht. Stelle, an der Drift mit Anlauf passiert.
|
||||||
|
- **Komodo Self-Bootstrap-Problem ist nur dokumentiert, nicht geloest** (MASTER 13: Self-Stack Drift-Recovery 2026-05-04). Bei Recovery vom kalten Host musst du Komodo aus `compose.yaml` neu erzeugen — dafuer brauchst du die `.env` mit `KOMODO_*`-Secrets, die nur auf Host und ggf. Vaultwarden liegen.
|
||||||
|
- **Backup-Off-Site-Diversitaet:** BorgBase Hetzner ist Single-Provider; Borg-Passphrase analog gesichert ist als TODO markiert (`docs/DISASTER_RECOVERY.md:64,401`). Wenn Hetzner-Account verloren geht, ist das halbe DR-Versprechen weg.
|
||||||
|
|
||||||
|
**Was unnoetig komplex ist:**
|
||||||
|
|
||||||
|
- Drei dokumentierte Monitoring-/Logging-Pfade gleichzeitig im Repo: `ops/grafana-influxdb` (Altstand), `ops/loki` (Altstand), `monitoring/` (Ziel). Die Altstaende sind als Container down, aber **Verzeichnisse noch im Repo** — Doppelpflege-Risiko. Der versprochene Repo-Cleanup (`git rm`) fehlt.
|
||||||
|
- Hermes-Agent: NAS-Stack bewusst deaktiviert ("VM-seitig offen"), aber Stack-Verzeichnis und Compose mit Dashboard-Domain bleiben im Repo. Mehr "Schwebezustand" als operativer Wert.
|
||||||
|
- BentoPDF: "vorbereitet", noch nie produktiv abgenommen (`SERVICE_CATALOG 52`, `MASTER 7.5`).
|
||||||
|
- `infra/redis` ist als shared Cache deklariert, wird de facto nur von Paperless genutzt (Authelia nicht, Immich/Nextcloud/Mealie haben eigene Redis). Das "shared" stimmt im Repo nicht mit der Realitaet ueberein.
|
||||||
|
|
||||||
|
**Groesster Hebel:**
|
||||||
|
|
||||||
|
**Authelia OIDC-Provider aktivieren** — wenn Nextcloud, Immich, Grafana (und perspektivisch Mealie via OIDC-Bridge) per SSO laufen, gewinnst du gleichzeitig:
|
||||||
|
|
||||||
|
- (a) Familien-Onboarding-Komfort (ein Login),
|
||||||
|
- (b) zentrale Brute-Force-Regulation und Audit,
|
||||||
|
- (c) Voraussetzung fuer sinnvolles CrowdSec/Fail2Ban,
|
||||||
|
- (d) zentrale 2FA-Pflicht statt App-by-App.
|
||||||
|
|
||||||
|
Das ist ein Sprint, nicht ein Quartal, und macht aus deinem "Admin-Authelia" ein echtes Identity-System.
|
||||||
|
|
||||||
|
**Was ein erfahrener Homelabber sofort aendern wuerde:**
|
||||||
|
|
||||||
|
1. AdGuard-Admin-Port nur auf Tailscale-Interface binden (5 Min, Compose-Edit).
|
||||||
|
2. Borg-Passphrase auf Papier in Bankschliessfach (15 Min, off-system).
|
||||||
|
3. `scrutiny` und `ddns-updater` `no-new-privileges` Warning aufraeumen (10 Min) — kosmetisch, aber Policy-Check sollte clean sein.
|
||||||
|
4. Altstaende `ops/grafana-influxdb/` und `ops/loki/` aus Repo entfernen (Backup-Branch dann `git rm`).
|
||||||
|
5. Renovate-Bot gegen Gitea einrichten — beendet manuelle Digest-Pflicht.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# B. Scorecard (1 = exzellent, 10 = ungenuegend)
|
||||||
|
|
||||||
|
| Bereich | Note | Begruendung |
|
||||||
|
|---|---:|---|
|
||||||
|
| Hardware | **nicht bewertbar** | Keine Inventar-Doku im Repo. Nur Cache-NVMe genannt. Siehe Phase H. |
|
||||||
|
| Ordnerstruktur | **2** | Klare Trennung apps/infra/ops/security/core; konsistente Namenskonvention (mit Migrationsplan in STORAGE_LAYOUT 6). Kleinerer Haenger: `host-services/Adguard/` mit Grossbuchstabe. |
|
||||||
|
| Storage | **3** (Repo-Stand) / **2** (mit STORAGE_LAYOUT Active) | Cache-XFS, Disk1-XFS jetzt erreicht. Pfad-Disziplin via `/mnt/user/...`. Posture-Check etabliert. Note durch Draft-Status und fehlendes `SERVICES_RECOVERY.md` gedrueckt. |
|
||||||
|
| Docker-Architektur | **2** | Netzmodell klar, Healthchecks fehlen grossflaechig, `latest@sha256` als bewusster Kompromiss bei einigen Images dokumentiert. Keine Memory-Limits. |
|
||||||
|
| Reverse Proxy / Zugriff | **2** | DNS-Challenge, Wildcard-faehig, Authelia ForwardAuth, dynamic config sauber getrennt. Manuelle Host-Sync-Ausnahme ist pragmatisch. |
|
||||||
|
| Security | **3** | Solides Fundament (Authelia Argon2id, no-new-privileges-Standard, Webhook-Allowlist, `cloudflare_dns_api_token` als Docker Secret), aber: nur 2 Domains mit 2FA, AdGuard-Admin direkt am LAN, kein WAF/Bouncer, Authelia Regulation 5-Min-Ban ist gentil. |
|
||||||
|
| Netzwerk / DNS | **2** | AdGuard + Unbound + Tailscale-Trias ist Best-of-Class-Homelab. FritzBox als Router nicht im Repo dokumentiert, daher Note nicht 1. |
|
||||||
|
| Backup | **2** | Borg, Pre-Dumps, Tier-Modell, dokumentierter Scope. Punkt-Abzug: Single-Provider Off-Site, Passphrase nicht analog, Komodo-Mongo-Dump-Verifikation nicht im Auto-Cron. |
|
||||||
|
| Restore-Faehigkeit | **2** | RESTORE_MATRIX mit Smoke-Test je Dienst, Restore-Test-Schedule + Validierungen fuer Vaultwarden/Gitea/Paperless dokumentiert. Punkt-Abzug: Immich-Restore noch nie geuebt — groesster Datentopf. |
|
||||||
|
| GitOps | **1-2** | Webhook-Pflicht fuer neue Stacks, Source-of-Truth-Hierarchie, Drift-Runbook. Self-Bootstrap-Problem von Komodo zieht von 1 auf 2. |
|
||||||
|
| Monitoring | **3** | Stack produktiv, aber Altstaende noch im Repo, Family-View-Dashboard fehlt, Alerts (`alerts.yml`) sehr knapp (5 Regeln), keine Cert-Expiry-Alert auf Prometheus-Ebene (Cert-Token-Check laeuft separat). |
|
||||||
|
| Dokumentation | **1** | Aussergewoehnlich. SERVICE_CATALOG ist Gold. Einziger Punkt: kein End-User-/Familien-Onboarding. |
|
||||||
|
| Automatisierung | **3** | Borg-Dumps automatisiert, posture-check Host-Cron, Alert->ntfy-Pipe. Aber: keine CI gegen Repo, kein Renovate, kein automatisches Image-Update-Tracking. |
|
||||||
|
| Wartbarkeit | **2** | Doku traegt; Sprintpflege-Disziplin sichtbar (MIGRATION_LOG, AUDIT_FINAL). Risiko: Authelia-Drift, Hermes-Schwebezustand. |
|
||||||
|
| Nerd-Faktor | **2-** | Komodo + Borg-UI + ntfy-Bridge + Posture-Check + Restore-Lab + Hermes-Experiment + Push-Mirror + Digest-Pinning. Liegt zwischen "Solider Senior" und "Spielwiese halten lernen". |
|
||||||
|
|
||||||
|
**Gesamteindruck: 2 (gut).** Strukturell weit ueber durchschnittlichem Homelab; konkrete Luecken sind klar benennbar und nicht systemisch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# C. Top-20 Findings
|
||||||
|
|
||||||
|
> Format: priorisiert nach Risiko-zu-Aufwand-Hebel. Jeder Eintrag hat Fundstelle, Empfehlung und Prio.
|
||||||
|
|
||||||
|
### F-01 · AdGuard-Admin am LAN ohne Auth
|
||||||
|
|
||||||
|
- **Kategorie:** Security / Zugriff
|
||||||
|
- **Fundstelle:** `host-services/Adguard/docker-compose.yml:16`, `MASTER 10`, `docs/SERVICE_CATALOG.md:14`
|
||||||
|
- **Beobachtung:** Port `8082:80` direkt auf alle Interfaces. Bewusste Operator-Entscheidung 2026-05-25.
|
||||||
|
- **Risiko:** Jedes Geraet im LAN kann DNS-Filterregeln, Upstream und Logging manipulieren. IoT-Kompromittierung oder Gast-WLAN -> DNS-Hijack moeglich.
|
||||||
|
- **Best Practice:** Admin-UIs nicht im LAN ohne Auth. Entweder hinter Traefik+Authelia mit `two_factor` oder Bind auf Tailscale-Interface (z. B. `100.x.y.z:8082:80`).
|
||||||
|
- **Empfehlung:** Schritt 1 — Bind auf Tailscale-IP (S, 5 Min). Schritt 2 — optional spaeter Traefik-Route hinter Authelia.
|
||||||
|
- **Prioritaet:** Sollte zeitnah
|
||||||
|
- **Aufwand:** S
|
||||||
|
- **Validierung:** `ss -ltnp | grep :8082` zeigt nur Tailscale-IP; LAN-Browser-Zugriff schlaegt fehl.
|
||||||
|
- **Rollback:** Compose-Diff zurueck, Komodo redeploy.
|
||||||
|
|
||||||
|
### F-02 · Borg-Passphrase nicht analog gesichert
|
||||||
|
|
||||||
|
- **Kategorie:** Backup / DR
|
||||||
|
- **Fundstelle:** `docs/DISASTER_RECOVERY.md:64,401`, `docs/SECRETS_MAP.md:48`
|
||||||
|
- **Beobachtung:** `borg_repo_passphrase.txt` liegt im Host-Filesystem unter `/mnt/user/appdata/secrets/`. Doku weist explizit darauf hin, dass eine externe analoge Sicherung Operator-Aufgabe ist.
|
||||||
|
- **Risiko:** Wenn Unraid-Host und ggf. Vaultwarden gleichzeitig defekt sind, ist das verschluesselte Borg-Repo bei Hetzner nutzlos.
|
||||||
|
- **Empfehlung:** Auf Papier ausdrucken, in Bankschliessfach oder bei vertrauter Person versiegelt. Zusaetzlich in Vaultwarden hinterlegen (aber Vaultwarden hilft nicht, wenn es selbst restauriert werden muss).
|
||||||
|
- **Prioritaet:** Muss sofort
|
||||||
|
- **Aufwand:** S
|
||||||
|
- **Validierung:** Du kannst den Wert ohne Host wiederherstellen.
|
||||||
|
|
||||||
|
### F-03 · Single-Provider Off-Site Backup
|
||||||
|
|
||||||
|
- **Kategorie:** Backup
|
||||||
|
- **Fundstelle:** `ops/borg-ui/BACKUP_SCOPE.md`, `docs/RESTORE_MATRIX.md:77-96`, `STORAGE_LAYOUT 8.1`
|
||||||
|
- **Beobachtung:** Hetzner Storage Box als alleiniges Off-Site-Borg-Ziel. STORAGE_LAYOUT 8.1 sieht zusaetzlich lokales Borg-Repo auf `/mnt/user/backups/borg/` vor (gleicher Host) und eine externe Wechselplatte (manuell rotiert).
|
||||||
|
- **Risiko:** Hetzner-Account-Verlust (Payment-Issue, Account-Hack, Provider-Outage) = halbes 3-2-1.
|
||||||
|
- **Best Practice:** Zweites Off-Site-Ziel mit unterschiedlichem Provider oder Cold-Wechselplatte mit fester Rotationskadenz.
|
||||||
|
- **Empfehlung:** (a) Wechselplatten-Rotation in fester Kadenz dokumentieren (zwei Platten, monatlicher Tausch). Oder (b) zweites Borg-Repo bei rsync.net / BorgBase EU2 / privatem 2. Standort.
|
||||||
|
- **Prioritaet:** Sollte zeitnah
|
||||||
|
- **Aufwand:** M
|
||||||
|
- **Validierung:** `borg list` gegen beide Repos, beide < 7 Tage alt.
|
||||||
|
|
||||||
|
### F-04 · Authelia 2FA-Pflicht zu schmal
|
||||||
|
|
||||||
|
- **Kategorie:** Security
|
||||||
|
- **Fundstelle:** `security/authelia/configuration.yml:44-53`
|
||||||
|
- **Beobachtung:** Nur `files.kaleschke.info` und `scrutiny.kaleschke.info` sind `two_factor`. Tier-1-Operator-UIs wie Borg-UI, Code-Server, Filebrowser (zweite Route?), Komodo (eigene Auth), Glance, Grafana laufen mit `one_factor`.
|
||||||
|
- **Risiko:** Operator-Passwort-Kompromittierung (Phishing, Keylogger, Browser-Save-Leak) gibt ohne 2FA Vollzugriff auf Code-Server (Repo + Workspace), Borg-UI (Restore-Pfade), Filebrowser (Documents/Photos).
|
||||||
|
- **Empfehlung:** ACL erweitern: `two_factor` fuer `borg.kaleschke.info`, `code.kaleschke.info`, `files.kaleschke.info` (schon), `glance.kaleschke.info` (debattierbar), `traefik.kaleschke.info`. Komodo bleibt Ausnahme.
|
||||||
|
- **Prioritaet:** Muss sofort
|
||||||
|
- **Aufwand:** S
|
||||||
|
- **Validierung:** Nach Login auf `borg.kaleschke.info` wird 2FA-Prompt erzwungen.
|
||||||
|
- **Rollback:** ACL-Block zurueck.
|
||||||
|
|
||||||
|
### F-05 · Repo-Altstaende `ops/grafana-influxdb/` und `ops/loki/` nicht entfernt
|
||||||
|
|
||||||
|
- **Kategorie:** Wartbarkeit / GitOps
|
||||||
|
- **Fundstelle:** Repo-Wurzeln `ops/grafana-influxdb/`, `ops/loki/`; `MASTER 7.6`, `SERVICE_CATALOG 68-70,80-81`, `AUDIT_FINAL 9`
|
||||||
|
- **Beobachtung:** Container down, aber Compose-Dateien + Provisioning bleiben im Repo. Doku referenziert beide gleichzeitig. Risiko: jemand (zukuenftiges Ich, KI) deployt versehentlich den Altstand.
|
||||||
|
- **Empfehlung:** `git rm` der beiden Verzeichnisse, Tag `pre-monitoring-cleanup` fuer Rollback, MIGRATION_LOG-Eintrag.
|
||||||
|
- **Prioritaet:** Sollte zeitnah
|
||||||
|
- **Aufwand:** S
|
||||||
|
- **Validierung:** `policy-checks` laeuft clean, Repo enthaelt nur noch `monitoring/`.
|
||||||
|
|
||||||
|
### F-06 · Hermes-Agent im Schwebezustand
|
||||||
|
|
||||||
|
- **Kategorie:** App-Landschaft / Wartbarkeit
|
||||||
|
- **Fundstelle:** `ops/hermes-agent/docker-compose.yml`, `MASTER 7.5`, `SERVICE_CATALOG 82-83`
|
||||||
|
- **Beobachtung:** "NAS-Stack bewusst deaktiviert" wegen offener VM-Seite. Dashboard-Domain (`hermes.kaleschke.info`) + Authelia-ACL + Secret-Pfade dokumentiert.
|
||||||
|
- **Risiko:** Schleichender Verfall — in 6 Monaten verstehst du Model-C nicht mehr ohne `ops/hermes-agent/README.md`. Bei jeder Authelia-/Compose-Aenderung musst du Hermes mitpruefen, obwohl es nichts tut.
|
||||||
|
- **Empfehlung:** Operator-Entscheidung mit 60-Tage-Deadline ehrlich treffen. Wenn nicht produktiv bis 2026-07-25: `git rm ops/hermes-agent/`, Domain aus DNS, ACL-Eintrag raus.
|
||||||
|
- **Prioritaet:** Sollte zeitnah (Entscheidung)
|
||||||
|
- **Aufwand:** S (Entfernen) / L (echte Produktiv-Aktivierung)
|
||||||
|
- **Validierung:** Entweder Smoke-Test auf `hermes.kaleschke.info` mit funktionalem Use-Case-Beleg, oder Repo-clean.
|
||||||
|
|
||||||
|
### F-07 · Monitoring-Stack ohne Digest-Pin
|
||||||
|
|
||||||
|
- **Kategorie:** Reproduzierbarkeit / GitOps
|
||||||
|
- **Fundstelle:** `monitoring/docker-compose.yml:3,28,66,84,100,118,276,296`
|
||||||
|
- **Beobachtung:** Prometheus, Alertmanager, Blackbox, Loki, Promtail, Grafana, node-exporter, cAdvisor sind alle nur per Tag gepinnt (`prom/prometheus:v3.7.3`, `grafana/grafana:12.4.3`, ...). Nur `influxdb3-core` hat `@sha256:`. Das widerspricht der Image-Versionierungs-Disziplin der Tier-1-Stateful-Dienste.
|
||||||
|
- **Risiko:** Wenn upstream einen Tag erneut pushed (Versionsdrift, Supply Chain), wird beim Rebuild ein anderer Container deployed — gerade Monitoring sollte stabil sein.
|
||||||
|
- **Empfehlung:** Beim naechsten Komodo-Redeploy aktuellen Digest auslesen und einpinnen. Vorbereitung fuer Renovate (F-12).
|
||||||
|
- **Prioritaet:** Nice to have
|
||||||
|
- **Aufwand:** S
|
||||||
|
- **Validierung:** `grep '@sha256' monitoring/docker-compose.yml` listet alle 10 Services.
|
||||||
|
|
||||||
|
### F-08 · Alert-Regeln zu duenn
|
||||||
|
|
||||||
|
- **Kategorie:** Monitoring
|
||||||
|
- **Fundstelle:** `monitoring/prometheus/alerts.yml`
|
||||||
|
- **Beobachtung:** Exakt 5 Regeln: ExternalConnectivityDown, EndpointDown, EndpointSlow, DiskAlmostFull, MemoryHighUsage, Traefik5xx. Es fehlen:
|
||||||
|
- Borg-Lauf-Frische (ueber `node_exporter` textfile collector oder Pushgateway).
|
||||||
|
- Zertifikatslaufzeit (Blackbox kann `probe_ssl_earliest_cert_expiry`, aber keine Alert-Regel dafuer).
|
||||||
|
- Container-down-Alert (cAdvisor liefert `container_last_seen`).
|
||||||
|
- PostgreSQL-Connection-Saturation.
|
||||||
|
- Loki ingestion-rate / log-volume spike.
|
||||||
|
- InfluxDB-Disk-Pressure.
|
||||||
|
- Backup-Job-Failure.
|
||||||
|
- **Risiko:** Du siehst Probleme nicht, bevor sie weh tun. Cert-Expiry und Borg-Stale sind die schmerzhaftesten Blind-Spots.
|
||||||
|
- **Empfehlung:** Mindestens zwei Regeln nachziehen: `BorgArchiveStale` (>30 h, Pushgateway oder textfile) und `TLSCertExpiryNear` (<14 Tage). Rest als Folge-Sprint.
|
||||||
|
- **Prioritaet:** Sollte zeitnah
|
||||||
|
- **Aufwand:** M
|
||||||
|
- **Validierung:** Alerts feuern in Test-Bedingung (Borg-Dump-File touch -d backwards).
|
||||||
|
|
||||||
|
### F-09 · Komodo-Self-Bootstrap-Problem
|
||||||
|
|
||||||
|
- **Kategorie:** GitOps / DR
|
||||||
|
- **Fundstelle:** `MASTER 13: Komodo Self-Stack Drift-Recovery 2026-05-04`
|
||||||
|
- **Beobachtung:** Du hattest schon einen Drift-Vorfall (Komodo-Core ran aus `/tmp/*repair.yml`, Mongo-Pfad fehlte). Recovery-ENV liegt als "temporaeres Tier-1-Secret-Material" unter `/mnt/user/appdata/secrets/_komodo_stack_env_recovery_2026-05-04.env` (Doku-Stand).
|
||||||
|
- **Risiko:** Bei Totalausfall musst du Komodo aus Compose-Datei wiederbeleben, dafuer brauchst du die Stack-ENV mit `KOMODO_SECRET_KEY`, `KOMODO_MONGO_PASSWORD`, `KOMODO_PERIPHERY_PASSKEY` etc., die nur als Komodo Stack ENV existieren. Klassisches Henne-Ei.
|
||||||
|
- **Empfehlung:** Komodo-Self-Stack aus Komodos eigener Verwaltung herausnehmen und als handgepflegten `docker compose`-Service in `services/komodo-bootstrap/` halten. Stack-ENV als versiegelte Datei unter `/mnt/user/appdata/secrets/` mit deterministischem Restore-Pfad in RESTORE_MATRIX.
|
||||||
|
- **Prioritaet:** Sollte zeitnah
|
||||||
|
- **Aufwand:** M
|
||||||
|
- **Validierung:** Komodo-Stack-Datei lebt im Repo unter `services/`, nicht in Komodos eigener Workspace-Sicht.
|
||||||
|
|
||||||
|
### F-10 · Authelia Repo↔Host Drift "by design"
|
||||||
|
|
||||||
|
- **Kategorie:** GitOps / Security
|
||||||
|
- **Fundstelle:** `docs/REPO_MAP.md:48`, `security/authelia/configuration.yml`, `SERVICE_CATALOG 23`
|
||||||
|
- **Beobachtung:** Repo enthaelt Baseline ohne Secrets, OIDC, Users-DB. Manuelles Merge auf den Host noetig. Es gibt keine automatische Konsistenz-Pruefung.
|
||||||
|
- **Risiko:** Repo-Aenderung (z. B. neue ACL-Regel) wird gepusht, aber nie auf den Host gemerged -> Drift, Authelia hinkt der Wahrheit hinterher.
|
||||||
|
- **Empfehlung:** Symmetrisch zum Traefik-Dynamic-Workflow (manueller Sync explizit als Pflicht in WORKFLOW.md). Zusaetzlich ein einfaches Diff-Script `services/authelia-diff.sh`, das `diff` zwischen Repo-Baseline und Host-Datei zeigt, und das im posture-check als Warning auftaucht, wenn die ACL-Sektion differiert.
|
||||||
|
- **Prioritaet:** Sollte zeitnah
|
||||||
|
- **Aufwand:** S
|
||||||
|
- **Validierung:** Script laeuft, posture-check kennt einen neuen Check `authelia_config_drift`.
|
||||||
|
|
||||||
|
### F-11 · Immich-Restore noch nie geuebt
|
||||||
|
|
||||||
|
- **Kategorie:** Backup / Restore
|
||||||
|
- **Fundstelle:** `docs/RESTORE_MATRIX.md:49`, Restore-Test-Schedule
|
||||||
|
- **Beobachtung:** Vaultwarden / Gitea / Paperless haben Mini-Restore-Tests (2026-05-07). Immich nicht. Immich ist der groesste Datentopf (Familien-Fotos).
|
||||||
|
- **Risiko:** Silent Corruption in Postgres-pgvecto-rs-Daten bemerkst du erst beim Restore-Versuch.
|
||||||
|
- **Empfehlung:** Eigener Sprint: Immich-Restore-Test gegen `/mnt/user/backups/restore-lab/immich/` mit Sub-Set der `immich.dump` und einem Foto-Sample. Smoke-Test = "10 Fotos im Browser sichtbar nach Restore".
|
||||||
|
- **Prioritaet:** Sollte zeitnah
|
||||||
|
- **Aufwand:** M
|
||||||
|
- **Validierung:** Report unter `/mnt/user/backups/restore-reports/immich-<datum>.json`.
|
||||||
|
|
||||||
|
### F-12 · Keine Image-Update-Automatik (Renovate o. ae.)
|
||||||
|
|
||||||
|
- **Kategorie:** Wartbarkeit
|
||||||
|
- **Fundstelle:** Repo-weit; `docs/WORKFLOW.md:282-288` (Image-Versionierung)
|
||||||
|
- **Beobachtung:** Digest-Pinning ist konsequent, aber rein manuell. Bei ~30 Images bedeutet das, du musst monatlich fuer Patch-Updates manuell Digests auslesen — oder es bleibt liegen.
|
||||||
|
- **Risiko:** CVE-Patches werden nicht eingespielt, weil "der laufende Stand ist stabil".
|
||||||
|
- **Empfehlung:** Renovate Bot (self-hosted, gegen Gitea), Gitea-Actions-Runner. Renovate oeffnet PRs fuer Patch-/Minor-Updates; Major-Updates werden mit Labels separiert.
|
||||||
|
- **Prioritaet:** Sollte zeitnah (oder Nice to have, je nach Schmerz)
|
||||||
|
- **Aufwand:** M (Initial-Setup ist substantiell)
|
||||||
|
- **Validierung:** Renovate hat erste PRs in Gitea geoeffnet, du mergst eines davon kontrolliert.
|
||||||
|
|
||||||
|
### F-13 · Keine OIDC-SSO fuer User-Apps
|
||||||
|
|
||||||
|
- **Kategorie:** Security / UX
|
||||||
|
- **Fundstelle:** `security/authelia/configuration.yml`, `docs/SECRETS_MAP.md`
|
||||||
|
- **Beobachtung:** Authelia kann OIDC, ist aber nur als ForwardAuth konfiguriert. Nextcloud, Immich, Grafana, Mealie laufen mit eigenen User-DBs.
|
||||||
|
- **Risiko:** N getrennte Passwortspeicher, N getrennte 2FA-Setups, keine zentrale Sperrung bei Account-Kompromittierung. Familie hat keinen einfachen Onboarding-Pfad.
|
||||||
|
- **Empfehlung:** OIDC-Provider in Authelia aktivieren, Nextcloud (via Plugin), Immich (nativer OIDC-Support), Grafana (nativer OIDC-Support) als Clients konfigurieren. Vaultwarden via OIDC-Bridge nur, wenn der Aufwand klar mehrwertig ist — sonst bewusst auslassen.
|
||||||
|
- **Prioritaet:** Sollte zeitnah (groesster Hebel laut Executive)
|
||||||
|
- **Aufwand:** L
|
||||||
|
- **Validierung:** Familienkonto kann sich mit einem Login bei Nextcloud + Immich + Grafana anmelden.
|
||||||
|
|
||||||
|
### F-14 · Kein WAF / Bouncer vor oeffentlichen Apps
|
||||||
|
|
||||||
|
- **Kategorie:** Security
|
||||||
|
- **Fundstelle:** `traefik/docker-compose.yml`, oeffentliche Hosts in `docs/REPO_MAP.md:127-152`
|
||||||
|
- **Beobachtung:** Sechs oeffentliche Apps mit nativer Auth (vault, paperless, mealie, ntfy, git, immich, cloud) ohne IP-Bouncer. Authelia-Regulation greift nur fuer die ForwardAuth-Pfade; Apps mit eigener Auth bekommen den vollen Traffic.
|
||||||
|
- **Risiko:** Credential-Stuffing-Bot-Wellen treffen die App selbst (Nextcloud, Immich) — Logs sind im Loki, aber kein Sperr-Mechanismus.
|
||||||
|
- **Empfehlung:** CrowdSec als Bouncer fuer Traefik (`crowdsecurity/traefik-bouncer`). Nutzt Loki/Logs fuer Erkennung, sperrt IPs auf Traefik-Ebene, bevor sie die Apps treffen.
|
||||||
|
- **Prioritaet:** Sollte zeitnah
|
||||||
|
- **Aufwand:** M
|
||||||
|
- **Validierung:** CrowdSec-Dashboard zeigt erste Sperren; Test-Brute-Force gegen `nextcloud.kaleschke.info` wird bei N Versuchen geblockt.
|
||||||
|
|
||||||
|
### F-15 · Healthchecks fehlen grossflaechig
|
||||||
|
|
||||||
|
- **Kategorie:** Docker / Operations
|
||||||
|
- **Fundstelle:** Spot-checks: `apps/paperless/docker-compose.yml`, `apps/immich/docker-compose.yml`, `security/authelia/docker-compose.yml`, `traefik/docker-compose.yml` — keiner hat `healthcheck:`-Block.
|
||||||
|
- **Beobachtung:** Restart-Policy ist ueberall `unless-stopped`, aber ohne Healthcheck kann Docker keinen Crash-Loop bei "Container laeuft, aber App tot" erkennen.
|
||||||
|
- **Risiko:** Bei Soft-Failure (Postgres-Connection-Pool tot, Authelia haengt im Storage-Connect) merkst du nichts, weil Container "running" bleibt.
|
||||||
|
- **Empfehlung:** Fuer Tier-1 (Traefik `wget /ping`, Authelia `/api/health`, PostgreSQL `pg_isready`, Komodo `wget /api/healthcheck`) Healthchecks ergaenzen. Fuer Tier-2 schrittweise.
|
||||||
|
- **Prioritaet:** Sollte zeitnah
|
||||||
|
- **Aufwand:** M (pro Stack 5–15 Min)
|
||||||
|
- **Validierung:** `docker ps` zeigt `(healthy)` neben den Tier-1-Containern.
|
||||||
|
|
||||||
|
### F-16 · `infra/redis` als "shared" deklariert, faktisch nur Paperless
|
||||||
|
|
||||||
|
- **Kategorie:** Architektur-Konsistenz
|
||||||
|
- **Fundstelle:** `infra/redis/docker-compose.yml`, `docs/SERVICE_CATALOG.md:31` ("shared Redis Cache")
|
||||||
|
- **Beobachtung:** Immich, Nextcloud, Mealie haben jeweils eigene Redis-Instanzen. Authelia nutzt bewusst kein Redis (MASTER 13). Paperless nutzt es laut Compose. Effektiv "Paperless-Redis im Frack des shared-Caches".
|
||||||
|
- **Risiko:** Niedrig. Aber: Wenn du `infra/redis` fuer etwas anderes wegnimmst, denkst du, es kostet Paperless was — und das waere der Fall.
|
||||||
|
- **Empfehlung:** Doku-Update: SERVICE_CATALOG 31 praezisieren ("dediziertes Redis fuer Paperless; andere Stacks haben eigene Redis-Instanzen"). Architektur bleibt, nur Etikett ehrlich machen. Alternativ: in `apps/paperless/` als App-internes Netz konsolidieren wie Mealie.
|
||||||
|
- **Prioritaet:** Nice to have
|
||||||
|
- **Aufwand:** S (Doku) / M (Architektur)
|
||||||
|
|
||||||
|
### F-17 · Plex bleibt als Host-Net-Stack
|
||||||
|
|
||||||
|
- **Kategorie:** Security / Architektur
|
||||||
|
- **Fundstelle:** `host-services/plex/docker-compose.yml`, `MASTER 7.4`
|
||||||
|
- **Beobachtung:** Plex laeuft als Host-Net wegen Discovery/GDM. Dokumentierte Ausnahme.
|
||||||
|
- **Risiko:** Plex hat hoehere Angriffsoberflaeche als Apps mit Traefik. Plex-Login wurde mehrfach Ziel von Account-Uebernahmen (Plex.tv-Auth-Issues 2024/25). Bei Plex.tv-Kompromittierung greift Authelia nicht — Plex authentifiziert gegen Plex.tv.
|
||||||
|
- **Empfehlung:** Plex bewusst beibehalten (Doku stuetzt), aber: (a) "Remote Access" in Plex-UI deaktivieren, wenn nur lokal/Tailscale genutzt. (b) Plex-Server nicht in `frontend_net` (waere schaedlich) — bleibt Host-Net, korrekt.
|
||||||
|
- **Prioritaet:** Nice to have
|
||||||
|
- **Aufwand:** S
|
||||||
|
- **Validierung:** Plex `Remote Access` UI zeigt "disabled".
|
||||||
|
|
||||||
|
### F-18 · Nextcloud ohne ForwardAuth, ohne dedizierte Brute-Force-Doku
|
||||||
|
|
||||||
|
- **Kategorie:** Security
|
||||||
|
- **Fundstelle:** `apps/nextcloud/docker-compose.yml`, `MASTER 13: Nextcloud-Entscheidung`
|
||||||
|
- **Beobachtung:** Bewusste Ausnahme (WebDAV/CardDAV). In Nextcloud selbst sind Brute-Force-Schutz, 2FA-Pflicht und App-Passwords konfigurierbar, aber nicht im Repo dokumentiert.
|
||||||
|
- **Risiko:** Familien-Konto mit schwachem Passwort + Nextcloud oeffentlich = direkter Pfad zu Dokumenten/Fotos.
|
||||||
|
- **Empfehlung:** `apps/nextcloud/POST_INSTALL.md` mit Pflicht-Checkliste: Brute-Force-Plugin aktiv, 2FA-Provider TOTP installiert, Admin-Account hat 2FA, "Enforce 2FA for admin group" gesetzt. Optional: `OCC`-Befehle als Skript in `services/nextcloud-policy/`.
|
||||||
|
- **Prioritaet:** Sollte zeitnah
|
||||||
|
- **Aufwand:** S
|
||||||
|
- **Validierung:** Test-Login ohne 2FA als Admin schlaegt fehl.
|
||||||
|
|
||||||
|
### F-19 · Keine Container-Memory-Limits
|
||||||
|
|
||||||
|
- **Kategorie:** Docker / Hardware-Schutz
|
||||||
|
- **Fundstelle:** Spot-checks aller Composes
|
||||||
|
- **Beobachtung:** Keine `mem_limit:` oder `deploy.resources.limits` Sektion in Tier-1- oder Tier-2-Stacks.
|
||||||
|
- **Risiko:** Bei Image-Bug oder Memory-Leak (z. B. Immich-ML, Paperless-OCR-Loop) kann ein Container den Host in OOM treiben. Posture-Check + Docker-Critical-Events sehen das nachher, aber praeventiver waere Container-Limit + Docker-OOM-Kill fuer den richtigen Prozess.
|
||||||
|
- **Empfehlung:** Fuer Tier-1 (Postgres, Authelia, Traefik, Komodo) sanfte Limits setzen (z. B. Postgres 2 GB, Authelia 256 MB, Traefik 256 MB). Fuer Immich-ML-Container ein hartes Limit, das Verhungern verhindert.
|
||||||
|
- **Prioritaet:** Nice to have
|
||||||
|
- **Aufwand:** M
|
||||||
|
- **Validierung:** `docker stats` zeigt `MEM USAGE / LIMIT` ungleich `unlimited`.
|
||||||
|
|
||||||
|
### F-20 · Paperless-DBPass weiter als Stack-ENV (dokumentierte Ausnahme)
|
||||||
|
|
||||||
|
- **Kategorie:** Secrets
|
||||||
|
- **Fundstelle:** `MASTER 13: Secrets in Komodo Stacks`, `docs/SECRETS_MAP.md:25`
|
||||||
|
- **Beobachtung:** Paperless unterstuetzt `_FILE` nicht fuer DB-Pass. Bewusste Ausnahme.
|
||||||
|
- **Risiko:** Stack-ENV liegt in Komodo-DB (Mongo), nicht im Repo. Bei Komodo-Mongo-Backup-Luecke fehlt das Passwort beim Restore.
|
||||||
|
- **Empfehlung:** Erweiterung der Disaster-Recovery-Doku: explizite Liste aller "Stack-ENV-only"-Secrets mit Zeiger, dass `komodo-mongo.archive.gz` fuer Restore zwingend ist, oder die ENV manuell vorgehalten werden muss (in Vaultwarden + externer Notiz).
|
||||||
|
- **Prioritaet:** Nice to have
|
||||||
|
- **Aufwand:** S
|
||||||
|
- **Validierung:** DR-Doc Abschnitt "Stack-ENV-Werte" referenziert konkrete Restore-Pfade.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# D. Risiko-Matrix
|
||||||
|
|
||||||
|
| Risiko | Bereich | Wahrscheinlichkeit | Auswirkung | Prioritaet | Massnahme |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Borg-Passphrase weg -> Restore unmoeglich | Backup | niedrig | katastrophal | P0 | F-02 analoge Sicherung |
|
||||||
|
| Hetzner-Account-Verlust -> halbes 3-2-1 | Backup | niedrig-mittel | hoch | P0/P1 | F-03 Zweitziel |
|
||||||
|
| AdGuard-Admin-Manipulation aus LAN | Security | niedrig | hoch (DNS-Hijack) | P0 | F-01 Bind auf Tailscale |
|
||||||
|
| Operator-Pwd-Leak -> 2FA fehlt fuer Borg-UI/Code-Server | Security | mittel | hoch | P0 | F-04 2FA-ACL erweitern |
|
||||||
|
| Komodo-Self-Bootstrap-Failure nach Totalausfall | DR | niedrig | hoch | P1 | F-09 Bootstrap-Datei in `services/` |
|
||||||
|
| Authelia Repo↔Host Drift unbemerkt | GitOps/Security | mittel | mittel | P1 | F-10 Diff-Check |
|
||||||
|
| Immich Silent Corruption -> kein Restore-Test belegt | Backup | niedrig | sehr hoch (Familien-Fotos) | P1 | F-11 Restore-Test |
|
||||||
|
| Cert-Expiry unbemerkt -> Public Apps down | Operations | niedrig | mittel | P1 | F-08 Alert-Regel |
|
||||||
|
| Nextcloud Brute-Force ohne Bouncer | Security | mittel | mittel-hoch | P1 | F-14 CrowdSec / F-18 Nextcloud-Haerten |
|
||||||
|
| Image-Update-Stillstand -> CVE bleibt | Security | mittel | mittel | P1 | F-12 Renovate |
|
||||||
|
| Hermes-Wartungsschuld | Wartbarkeit | hoch | niedrig | P1 | F-06 Entscheidung |
|
||||||
|
| Repo-Altstaende ueberleben -> Doppel-Deploy | GitOps | mittel | niedrig | P1 | F-05 Cleanup |
|
||||||
|
| OOM durch unlimitierte Container | Hardware | niedrig | mittel | P2 | F-19 mem_limit |
|
||||||
|
| Healthcheck-Luecke -> Soft-Failure stumm | Operations | mittel | niedrig | P2 | F-15 Healthchecks |
|
||||||
|
| Monitoring-Stack ohne Digest-Pin | Reproduzierbarkeit | niedrig | niedrig | P2 | F-07 Digests + Renovate |
|
||||||
|
| Hardware-SPOF (kein zweiter Host) | Hardware | niedrig | sehr hoch | P3 | Cold-Standby / 2. Host |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# E. Zielarchitektur (realistisch fuer privates Homelab)
|
||||||
|
|
||||||
|
**Hardware**
|
||||||
|
|
||||||
|
- 1× Unraid-Host (bestehend) als Production. CPU mit AVX2/AVX-512 fuer Immich-ML. ≥ 32 GB RAM (fuer 2× Postgres + Immich-ML + Loki/Prometheus + 2 VMs).
|
||||||
|
- 2× NVMe als BTRFS-RAID1-Cache, sobald Cache-Auslastung > 70 % (STORAGE_LAYOUT 15.3).
|
||||||
|
- Parity-Disk ≥ groesste Daten-Disk.
|
||||||
|
- USV mit USB-Steuerung (NUT-faehig: APC Back-UPS RS 700+, Eaton 3S, CyberPower CP1500EPFCLCD). Direkter Shutdown bei Power-Loss.
|
||||||
|
- Optional: zweiter alter Mini-PC oder NUC als Cold-Standby mit Tailscale, der den letzten Komodo-Bootstrap + Gitea-Mirror tragen kann.
|
||||||
|
|
||||||
|
**Netzwerk**
|
||||||
|
|
||||||
|
- FritzBox (bestehend) als Router + NAT.
|
||||||
|
- VLANs nur wenn IoT-WLAN dazukommt (FritzBox-Gast-WLAN reicht fuer Anfang).
|
||||||
|
- DNS: AdGuard -> Unbound (bestehend). Admin-UI nur Tailscale.
|
||||||
|
- Tailscale (bestehend): Operator-Pfad. Subnet-Router optional fuer LAN-Devices ueber Tailscale.
|
||||||
|
|
||||||
|
**Storage**
|
||||||
|
|
||||||
|
- Cache `only` fuer `appdata`, `system`, `domains` (bestehend STORAGE_LAYOUT 4).
|
||||||
|
- Disk1 (XFS) fuer `services`, `documents`, `photos`, `backups`, `media`, `finance`, `projekte`.
|
||||||
|
- Externe Wechselplatte (XFS) fuer Cold-Off-Site mit fester monatlicher Rotation.
|
||||||
|
|
||||||
|
**Ordnerstruktur (Repo)**
|
||||||
|
|
||||||
|
- Beibehalten. Nur Cleanup von Altstaenden (F-05). Naming `kebab-case`-Migration aus STORAGE_LAYOUT 6 schrittweise.
|
||||||
|
|
||||||
|
**Docker**
|
||||||
|
|
||||||
|
- Compose-only via Komodo (bestehend).
|
||||||
|
- Digest-Pin fuer alle Images (F-07).
|
||||||
|
- Healthchecks fuer Tier-1 (F-15).
|
||||||
|
- Mem-Limits fuer Tier-1 + Immich-ML (F-19).
|
||||||
|
- App-interne Netze fuer Stack-Isolation (bestehend).
|
||||||
|
|
||||||
|
**Reverse Proxy**
|
||||||
|
|
||||||
|
- Traefik v3 (bestehend), DNS-Challenge, Wildcard.
|
||||||
|
- Dynamic Config nur fuer Middlewares, TLS, Dashboard (bestehend).
|
||||||
|
- CrowdSec-Bouncer (F-14) fuer oeffentliche Apps.
|
||||||
|
|
||||||
|
**Auth**
|
||||||
|
|
||||||
|
- Authelia als ForwardAuth **und** OIDC-Provider (F-13).
|
||||||
|
- Nextcloud, Immich, Grafana via OIDC.
|
||||||
|
- 2FA-Pflicht fuer alle Operator-Dienste (F-04).
|
||||||
|
- Komodo bewusste Ausnahme (bestehend).
|
||||||
|
|
||||||
|
**Backup**
|
||||||
|
|
||||||
|
- Borg lokal (`/mnt/user/backups/borg/`) + Borg Hetzner + Wechselplatte.
|
||||||
|
- Pre-Dump-Hooks (bestehend).
|
||||||
|
- Borg-Passphrase off-system analog (F-02).
|
||||||
|
- Restore-Tests automatisiert (F-11 Immich, dann andere via CI).
|
||||||
|
|
||||||
|
**Monitoring**
|
||||||
|
|
||||||
|
- `monitoring/`-Stack als alleinige Quelle. Altstaende raus (F-05).
|
||||||
|
- Family-View-Dashboard in Grafana (alles gruen, Backup-Frische, Cert-Tage).
|
||||||
|
- Alerts ausgebaut (F-08).
|
||||||
|
- Posture-Check + Docker-Critical-Events -> ntfy `homelab-alerts` (bestehend).
|
||||||
|
|
||||||
|
**Dokumentation**
|
||||||
|
|
||||||
|
- Aktuelle Doku-Tiefe halten.
|
||||||
|
- `SERVICES_RECOVERY.md` und `STORAGE_LAYOUT.md` (Active) finalisieren.
|
||||||
|
- Familien-/User-Onboarding-Doku als eigenes kleines Dokument.
|
||||||
|
|
||||||
|
**GitOps**
|
||||||
|
|
||||||
|
- Gitea + Komodo (bestehend).
|
||||||
|
- GitHub-Push-Mirror (umgesetzt, bestaetigt durch MASTER 7.1).
|
||||||
|
- Renovate-Bot gegen Gitea (F-12).
|
||||||
|
- Optional: Staging-Branch + zweites Komodo-Ziel in Tailscale-VM (Phase 3).
|
||||||
|
|
||||||
|
**Restore**
|
||||||
|
|
||||||
|
- RESTORE_MATRIX bleibt fuehrend.
|
||||||
|
- Restore-Lab unter `/mnt/user/backups/restore-lab/` (bestehend).
|
||||||
|
- Immich-Restore als Luecke schliessen (F-11).
|
||||||
|
- Komodo-Self-Bootstrap raus aus Komodo (F-09).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# F. Priorisierte Massnahmenliste
|
||||||
|
|
||||||
|
| # | Aufgabe | Warum | Kategorie | Prio | Aufwand | Risiko (bei Nicht-Tun) | Mehrwert | Abhaengigkeiten | Validierung |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 1 | Borg-Passphrase analog sichern | DR-SPOF schliessen | Backup | P0 | S | katastrophal | DR-Sicherheit | — | Wert ohne Host abrufbar |
|
||||||
|
| 2 | AdGuard-Admin auf Tailscale-IP binden | LAN-Angriffsflaeche | Security | P0 | S | hoch | LAN-IoT-Haertung | — | `ss -ltnp` zeigt nur Tailscale |
|
||||||
|
| 3 | 2FA-ACL erweitern (borg, code, files, traefik) | Operator-Pwd-Leak | Security | P0 | S | hoch | 2FA-Coverage | Authelia-TOTP-Setup | Login erzwingt 2FA |
|
||||||
|
| 4 | Altstaende `ops/grafana-influxdb`+`ops/loki` `git rm` | Repo-Hygiene, kein Re-Deploy | GitOps | P0 | S | niedrig | Klarheit | Tag setzen | Policy-Check clean |
|
||||||
|
| 5 | Hermes 60-Tage-Deadline | Wartungsschuld | App | P1 | S/L | mittel | Komplexitaet raus | Operator-Entscheidung | Entweder produktiv oder weg |
|
||||||
|
| 6 | Immich-Restore-Test einrichten | Groesster Datentopf ungeprueft | Backup | P1 | M | hoch | Restore-Vertrauen | Restore-Lab-Pfad | Smoke-Test-Report |
|
||||||
|
| 7 | Renovate-Bot in Gitea | manuelle Digest-Pflege | Wartung | P1 | M | mittel | Update-Hygiene | Gitea-Runner | erste PR offen |
|
||||||
|
| 8 | Alert-Regeln (Borg-Frische, Cert-Expiry) | Blind-Spot Operations | Monitoring | P1 | M | mittel | echte Alerts | Pushgateway o. textfile | Alert in Test |
|
||||||
|
| 9 | Family-View-Dashboard Grafana | Morgens 30 s Check | Monitoring | P1 | M | niedrig | Uebersicht | Datasources stehen | Dashboard funktioniert |
|
||||||
|
| 10 | Komodo-Self-Bootstrap als `services/komodo-bootstrap/` | Henne-Ei-Problem | GitOps/DR | P1 | M | mittel | sauberer Recovery-Pfad | Komodo-Stack-Doku | Bootstrap aus Repo allein moeglich |
|
||||||
|
| 11 | Authelia-Drift-Diff-Check in posture-check | Repo↔Host Drift | GitOps | P1 | S | mittel | Drift-Detektion | posture-check-Erweiterung | neuer Check sichtbar |
|
||||||
|
| 12 | Healthchecks Tier-1 | Soft-Failure-Erkennung | Docker | P1 | M | niedrig | Self-Healing-Trigger | — | `docker ps` zeigt `healthy` |
|
||||||
|
| 13 | CrowdSec-Bouncer vor Traefik | oeffentliche Apps schuetzen | Security | P1 | M | mittel | Brute-Force-Stop | Traefik-Middleware | Test-IP wird geblockt |
|
||||||
|
| 14 | Nextcloud-Haertung dokumentieren | Public App + native Auth | Security | P1 | S | mittel | App-Haertung | Plugin-Install | 2FA-erzwingt |
|
||||||
|
| 15 | Authelia OIDC-Provider + Nextcloud/Immich/Grafana | SSO, Familien-Onboarding | Security/UX | P2 | L | niedrig | hoher Mehrwert | Authelia-OIDC-Setup | SSO-Login funktioniert |
|
||||||
|
| 16 | Immich-Smartphone-Auto-Backup fuer Familie | Killer-App fuer Familie | App | P2 | S | niedrig | hoher Mehrwert | — | Familien-Foto in Immich |
|
||||||
|
| 17 | Monitoring-Stack Digests + Renovate-Pin | Reproduzierbarkeit | GitOps | P2 | S | niedrig | konsistent | Renovate optional | `@sha256` an allen Images |
|
||||||
|
| 18 | Mem-Limits Tier-1 + Immich-ML | OOM-Schutz | Hardware/Docker | P2 | M | niedrig | Schutz | — | `docker stats` zeigt Limits |
|
||||||
|
| 19 | Off-Site-Zweitziel (rsync.net o. Wechselplatte) | Single-Provider | Backup | P2 | M | mittel | 3-2-1 echt | Borg-Config | beide Repos < 7d |
|
||||||
|
| 20 | Staging-Branch + 2. Komodo-Ziel | Risiko-Aenderung testbar | GitOps | P3 | L | niedrig | Reife | 2. VM/Host | Deploy auf staging klappt |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# G. Refactoring-Plan (Sprints)
|
||||||
|
|
||||||
|
## Sprint 0 — Sicherheitsnetz und Ist-Zustand sichern (1 Tag)
|
||||||
|
|
||||||
|
- **Ziel:** Du kannst danach im schlimmsten Fall alles, was du jetzt aenderst, sicher zurueckrollen.
|
||||||
|
- **Aufgaben:**
|
||||||
|
- Git-Tag `audit-2026-05-25-baseline` auf `master` setzen und nach Gitea + GitHub-Mirror pushen.
|
||||||
|
- Borg-Lauf manuell ausloesen ("freshen up"), Erfolg im Log dokumentieren.
|
||||||
|
- Aktuellen Komodo-Mongo-Dump verifizieren (`mongorestore --dry-run`).
|
||||||
|
- `docs/MIGRATION_LOG.md` Eintrag "Audit-Sprint-Start".
|
||||||
|
- **Erfolgskriterium:** Tag pushed, Borg-Lauf gruen, Mongo-Dump verifiziert.
|
||||||
|
- **Validierung:** `git fetch && git tag | grep audit-2026-05-25-baseline` und `ls /mnt/user/backups/borg/dumps/latest/` zeigt fresh.
|
||||||
|
- **Rollback:** N/A (rein additiv).
|
||||||
|
- **Risiko bei Nichtumsetzung:** Keine Notbremse fuer Sprint 1.
|
||||||
|
|
||||||
|
## Sprint 1 — Offensichtliche Risiken entschaerfen (1 Woche)
|
||||||
|
|
||||||
|
- **Ziel:** P0-Risiken weg, Repo-Hygiene wieder gruen.
|
||||||
|
- **Aufgaben (in dieser Reihenfolge):**
|
||||||
|
1. F-02 Borg-Passphrase analog sichern (off-system, kein Code-Change).
|
||||||
|
2. F-01 AdGuard-Admin-Port auf Tailscale-IP — Edit `host-services/Adguard/docker-compose.yml:16`.
|
||||||
|
3. F-04 Authelia ACL erweitern (`two_factor` fuer borg, code, files, traefik) — Edit `security/authelia/configuration.yml` + Host-Sync.
|
||||||
|
4. F-05 Altstaende `ops/grafana-influxdb/`, `ops/loki/` entfernen — `git rm`, MIGRATION_LOG.
|
||||||
|
5. Policy-Check-Warnings `SEC001` (ddns-updater, scrutiny) aufraeumen.
|
||||||
|
- **Erfolgskriterium:** Policy-Check 0 Warnings fuer SEC001, AdGuard-Admin nur via Tailscale, 2FA-Login auf borg.kaleschke.info.
|
||||||
|
- **Validierung:** Policy-Check-Report; Browser-Test mit/ohne 2FA-Cookie.
|
||||||
|
- **Rollback:** Commit-Revert pro Block.
|
||||||
|
|
||||||
|
## Sprint 2 — GitOps-Robustheit (1–2 Wochen)
|
||||||
|
|
||||||
|
- **Ziel:** Self-Bootstrap-Problem entschaerft, Drift-Detektion automatisiert.
|
||||||
|
- **Aufgaben:**
|
||||||
|
1. F-09 Komodo-Bootstrap-Compose nach `services/komodo-bootstrap/` extrahieren + dokumentierter Standalone-Restore-Pfad.
|
||||||
|
2. F-10 Authelia-Drift-Diff in posture-check ergaenzen.
|
||||||
|
3. F-11 Immich-Restore-Test einrichten (analog zu vaultwarden/gitea/paperless).
|
||||||
|
4. F-06 Hermes-Entscheidung mit 60-Tage-Deadline schriftlich.
|
||||||
|
- **Erfolgskriterium:** Komodo laesst sich aus Repo allein wiederherstellen. Posture-Check zeigt `authelia_config_drift: false`. Immich-Restore-Report unter `/mnt/user/backups/restore-reports/`.
|
||||||
|
- **Validierung:** Trockenversuch (Komodo-Container stoppen, `docker compose up -d` aus `services/komodo-bootstrap/`).
|
||||||
|
- **Rollback:** Bootstrap-Verzeichnis loeschen, Komodo-Self-Stack wie vorher.
|
||||||
|
|
||||||
|
## Sprint 3 — Backup & Restore belastbar machen (2–3 Wochen)
|
||||||
|
|
||||||
|
- **Ziel:** 3-2-1 echt, Restore-Tests breiter, Stack-ENV im DR-Pfad.
|
||||||
|
- **Aufgaben:**
|
||||||
|
1. F-03 Zweitziel: Wechselplatten-Rotation dokumentieren ODER zweites Borg-Repo (rsync.net / BorgBase EU2).
|
||||||
|
2. F-20 Stack-ENV-Liste in DR-Doc explizit machen (Restore-Reihenfolge).
|
||||||
|
3. Borg-Verifikation Cron fuer `borg check --repository-only` weekly (STORAGE_LAYOUT 8.4).
|
||||||
|
4. Quartalsweise End-to-End-Restore-Drill in Schedule aufnehmen.
|
||||||
|
- **Erfolgskriterium:** Beide Off-Site-Ziele < 7 Tage alt; DR-Doc enthaelt "ENV-Restore-Reihenfolge".
|
||||||
|
- **Validierung:** `borg list` gegen beide Repos.
|
||||||
|
|
||||||
|
## Sprint 4 — Monitoring & Alerting ausbauen (2 Wochen)
|
||||||
|
|
||||||
|
- **Ziel:** Sichtbarkeit auf das, was wirklich weh tut.
|
||||||
|
- **Aufgaben:**
|
||||||
|
1. F-08 Alert-Regeln: `BorgArchiveStale`, `TLSCertExpiryNear`, `ContainerDown`, `PostgresConnSaturation`.
|
||||||
|
2. F-15 Healthchecks fuer Traefik, Authelia, Postgres, Komodo, Gitea.
|
||||||
|
3. F-07 Digest-Pin in `monitoring/docker-compose.yml`.
|
||||||
|
4. Family-View-Dashboard in Grafana (1 Panel: Service-Up, 1 Panel: Backup-Frische, 1 Panel: Cert-Tage, 1 Panel: Disk-Fuellung).
|
||||||
|
- **Erfolgskriterium:** Family-View zeigt alles gruen; Cert-Alert feuert in Test (Datum vorgespult).
|
||||||
|
- **Validierung:** Dashboard sichtbar unter `monitoring.kaleschke.info/d/family-view`.
|
||||||
|
|
||||||
|
## Sprint 5 — Auth-Konsolidierung & Frontdoor-Haertung (3–4 Wochen)
|
||||||
|
|
||||||
|
- **Ziel:** SSO fuer die Familie, Brute-Force-Bouncer vor oeffentlichen Apps.
|
||||||
|
- **Aufgaben:**
|
||||||
|
1. F-13 Authelia OIDC-Provider aktivieren.
|
||||||
|
2. Nextcloud OIDC-Plugin + Test-Login.
|
||||||
|
3. Immich OIDC + Test-Login.
|
||||||
|
4. Grafana OIDC + Test-Login.
|
||||||
|
5. F-14 CrowdSec-Bouncer vor Traefik.
|
||||||
|
6. F-18 Nextcloud-Haertung-Dokument + 2FA-Pflicht.
|
||||||
|
- **Erfolgskriterium:** Familien-Konto loggt sich mit einem Login bei drei Apps ein; CrowdSec sperrt Test-IP nach N fehlerhaften Versuchen.
|
||||||
|
- **Validierung:** OIDC-Sequenz im Browser ohne Eingabe-Wiederholung; CrowdSec-Dashboard zeigt Sperre.
|
||||||
|
|
||||||
|
## Sprint 6 — Automatisierung und Nerd-Level (laufend)
|
||||||
|
|
||||||
|
- **Ziel:** Image-Update-Pipeline, optional Staging.
|
||||||
|
- **Aufgaben:**
|
||||||
|
1. F-12 Renovate-Bot gegen Gitea.
|
||||||
|
2. F-19 Mem-Limits Tier-1.
|
||||||
|
3. Restore-Test-CI via Gitea Actions (P3).
|
||||||
|
4. Optional: Staging-Branch + zweites Komodo-Ziel in Tailscale-VM (P3).
|
||||||
|
5. Optional: Firefly III / Actual Budget fuer `/mnt/user/finance`.
|
||||||
|
- **Erfolgskriterium:** Renovate-PRs erscheinen woechentlich; mindestens ein automatisches Patch-Update gemerged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# H. Fehlende Informationen
|
||||||
|
|
||||||
|
> Nur, was den Audit konkret schaerfer machen wuerde.
|
||||||
|
|
||||||
|
| Frage | Warum | Bereich | Kommando / Datei |
|
||||||
|
|---|---|---|---|
|
||||||
|
| CPU-Modell, RAM-Groesse, Mainboard | Hardware-Bewertung, OOM-Risiko, Immich-ML-Eignung, AVX-Verfuegbarkeit | Hardware | `cat /proc/cpuinfo \| grep -E 'model name\|flags'`, `free -h`, `dmidecode -t baseboard \| head -20` |
|
||||||
|
| USV vorhanden? Modell? | DR-Beurteilung Power-Loss, Shutdown-Pfad | Hardware/DR | physische Sichtpruefung; `apcaccess` falls APC mit NUT |
|
||||||
|
| Stromverbrauch idle / Last | Betriebskosten, Sizing | Hardware | Smartmeter / Tibber-API |
|
||||||
|
| NIC-Speed (1 GbE? 2.5 GbE?) | Backup-Durchsatz, Plex-Streaming | Netzwerk | `ip -br link`, `ethtool eth0` |
|
||||||
|
| Disk-Inventar (Anzahl, Modelle, Alter) | Storage-Health, Replacement-Plan | Storage | `lsblk -o NAME,SIZE,MODEL,SERIAL`, Scrutiny-UI |
|
||||||
|
| Aktueller Cache-Fuellstand | Wann zweite NVMe? | Storage | `df -h /mnt/cache` |
|
||||||
|
| FritzBox-Modell + Firmware | Net-Sicherheit, VLAN-Faehigkeit | Netzwerk | FritzBox-UI / `fritzconnection` |
|
||||||
|
| Tatsaechlich genutzte Plex- vs. ungenutzte App | Konsolidierungs-Belege | App-Landschaft | Plex-Server-Logs, ggf. Glances-Container-CPU pro Stack |
|
||||||
|
| Existiert `/mnt/user/finance/`-Share schon? | Ist Firefly-Vorbereitung trivial? | Storage | `ls /mnt/user/finance/` |
|
||||||
|
| Authelia Live-User-DB-Tiefe (Anzahl User, 2FA-Status) | 2FA-Coverage-Bewertung | Security | `cat /mnt/user/appdata/authelia/config/users_database.yml` (nur Strukturansicht, keine Hashes hier zitieren) |
|
||||||
|
| Komodo-Mongo-Dump letzter Integrity-Check | F-09-Vorbereitung | Backup | `mongorestore --dry-run --archive=komodo-mongo.archive.gz --gzip` |
|
||||||
|
| Aktuelle Cert-Restlaufzeit | F-08-Test-Vorbereitung | Operations | `openssl s_client -connect git.kaleschke.info:443 -servername git.kaleschke.info < /dev/null \| openssl x509 -noout -dates` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# I. Pruefkommandos (Linux / Unraid / Docker / Windows)
|
||||||
|
|
||||||
|
> Strukturiert nach Bereich. Sicher zum Ausfuehren am Host.
|
||||||
|
|
||||||
|
### Hardware
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# CPU + Flags (AVX fuer Immich-ML)
|
||||||
|
cat /proc/cpuinfo | awk '/model name|flags/ {print; if(/flags/) exit}'
|
||||||
|
|
||||||
|
# RAM
|
||||||
|
free -h
|
||||||
|
dmidecode -t memory | grep -E "Size|Speed" | head -20
|
||||||
|
|
||||||
|
# Mainboard
|
||||||
|
dmidecode -t baseboard | head -20
|
||||||
|
|
||||||
|
# PCI / SATA / NVMe
|
||||||
|
lspci
|
||||||
|
nvme list
|
||||||
|
lsblk -o NAME,SIZE,MODEL,SERIAL,FSTYPE,MOUNTPOINT,VENDOR
|
||||||
|
|
||||||
|
# SMART
|
||||||
|
smartctl -a /dev/nvme0n1 | head -40
|
||||||
|
smartctl -a /dev/sdb | head -40
|
||||||
|
|
||||||
|
# Stromverbrauch (Unraid Plugin oder ipmitool falls IPMI)
|
||||||
|
sensors | head -30
|
||||||
|
```
|
||||||
|
|
||||||
|
### Filesystem / Storage / Mounts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Filesystem-Typen (Hard Rule 12.1)
|
||||||
|
findmnt -no FSTYPE /mnt/cache /mnt/disk1 /boot
|
||||||
|
mount | grep -E "ntfs3|fuseblk" # darf leer sein
|
||||||
|
|
||||||
|
# Share-Settings
|
||||||
|
ls -la /boot/config/shares/
|
||||||
|
|
||||||
|
# Cache-Fuellstand
|
||||||
|
df -h /mnt/cache /mnt/disk1 /mnt/user
|
||||||
|
du -sh /mnt/user/appdata/* | sort -hr | head -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container-Inventur
|
||||||
|
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Image}}" | sort
|
||||||
|
docker ps -a --filter "status=exited" --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
|
||||||
|
|
||||||
|
# Netzwerke
|
||||||
|
docker network ls
|
||||||
|
docker network inspect frontend_net | jq '.[0].Containers | keys'
|
||||||
|
docker network inspect backend_net | jq '.[0].Internal'
|
||||||
|
|
||||||
|
# Volumes ohne Container (Waisen)
|
||||||
|
docker volume ls -qf dangling=true
|
||||||
|
|
||||||
|
# Effektive Ports
|
||||||
|
ss -ltnp | sort -k4
|
||||||
|
|
||||||
|
# Healthchecks
|
||||||
|
docker ps --format "{{.Names}}\t{{.Status}}" | grep -E "healthy|unhealthy|starting"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Privileged-Container und Docker-Socket-Mounts
|
||||||
|
for c in $(docker ps -q); do
|
||||||
|
docker inspect "$c" --format '{{.Name}}: priv={{.HostConfig.Privileged}}; sock={{range .HostConfig.Binds}}{{println .}}{{end}}'
|
||||||
|
done | grep -E "priv=true|docker.sock"
|
||||||
|
|
||||||
|
# Direkte Host-Ports
|
||||||
|
docker ps --format "{{.Names}}: {{.Ports}}" | grep -v "^[^:]*: $"
|
||||||
|
|
||||||
|
# Secret-Datei-Rechte
|
||||||
|
ls -la /mnt/user/appdata/secrets/
|
||||||
|
stat -c "%a %n" /mnt/user/appdata/secrets/*.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup / Restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Borg-Frische
|
||||||
|
ls -lat /mnt/user/backups/borg/dumps/latest/ | head
|
||||||
|
find /mnt/user/backups/borg/dumps/latest -mmin +1440 -type f # aelter 24h
|
||||||
|
|
||||||
|
# Borg-Repo (Passphrase per File)
|
||||||
|
export BORG_PASSPHRASE=$(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)
|
||||||
|
borg list ssh://... --short | tail -5
|
||||||
|
borg info ssh://... ::Taegliche-Sicherung-2026-05-25T05:52:44.157
|
||||||
|
|
||||||
|
# Posture-Check
|
||||||
|
cat /mnt/user/services/posture-check/last.json | jq '.warning_count, .critical_count'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Netzwerk / DNS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tailscale
|
||||||
|
tailscale status
|
||||||
|
|
||||||
|
# DNS auf AdGuard testen
|
||||||
|
dig @127.0.0.1 git.kaleschke.info
|
||||||
|
dig @127.0.0.1 example.com # Unbound-Recursion
|
||||||
|
|
||||||
|
# Cert-Restlaufzeit
|
||||||
|
for h in vault git immich cloud paperless mealie ntfy; do
|
||||||
|
echo -n "$h.kaleschke.info: "
|
||||||
|
openssl s_client -connect ${h}.kaleschke.info:443 -servername ${h}.kaleschke.info </dev/null 2>/dev/null \
|
||||||
|
| openssl x509 -noout -dates 2>/dev/null
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitOps-Konsistenz
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Komodo Stack-Workspace vs. Repo
|
||||||
|
cd /mnt/user/services/stacks/<stackname>
|
||||||
|
git rev-parse HEAD
|
||||||
|
git status --short
|
||||||
|
|
||||||
|
# Webhook-Liveness
|
||||||
|
docker logs komodo-core 2>&1 | grep -i "webhook\|deploy" | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (lokal)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Repo-Status
|
||||||
|
git status --short
|
||||||
|
git fetch origin
|
||||||
|
git log origin/master..HEAD --oneline # ungepushte Commits
|
||||||
|
git log HEAD..origin/master --oneline # ungepullte Commits
|
||||||
|
|
||||||
|
# Policy-Check
|
||||||
|
.\ops\policy-checks\check_repo.ps1
|
||||||
|
|
||||||
|
# Restore-Freshness
|
||||||
|
.\ops\restore-tests\check-restore-freshness.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# J. Community- / Best-Practice-Abgleich
|
||||||
|
|
||||||
|
> Nur fuer die Architekturentscheidungen, bei denen der Markt eindeutig ist oder Gegenpositionen relevant sind.
|
||||||
|
|
||||||
|
| Entscheidung | Markt-Best-Practice | Stuetzende Quellen | Bewertung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Traefik mit Docker-Labels statt File-Provider | Standard in Selfhosted (siehe `awesome-selfhosted-docker`, Smarthome-Beginner Templates) | Traefik-Doc v3 docs.traefik.io/providers/docker | passt zu MASTER 13 (Wechsel 2026-03-28) |
|
||||||
|
| DNS-Challenge mit Cloudflare statt HTTP-01 | Standard fuer Wildcard und reduzierte Angriffsflaeche | acme.sh / lego docs | passt, korrekt |
|
||||||
|
| Authelia ForwardAuth statt Authentik | Authelia ist leichtgewichtiger, Authentik maechtiger; beide valide | r/selfhosted-Konsens 2024-25 | Authelia richtig fuer Single-Family-Setup |
|
||||||
|
| Authelia ohne Redis-Session-Backend | Markt-Standard ist mit Redis; deine Vereinfachung ist begruendet (MASTER 13 2026-05-04) | Authelia-Doc | Trade-off klar; Bewertung: vertretbar fuer Homelab |
|
||||||
|
| Komodo statt Portainer/Dockge | Komodo ist neuer (2024), Dockge etabliert, Portainer kommerziell | Selfh.st 2025 Comparison | Komodo legitim, mehr GitOps-nativ als Dockge |
|
||||||
|
| Borg statt Restic/Kopia | Borg ist klassische Wahl fuer Linux-Backup mit Deduplikation; Kopia/Restic gewinnen mit Multi-Backend | r/datahoarder, ServeTheHome 2024 | Borg passt zu Unraid-Stack; bewusste Vereinfachung |
|
||||||
|
| Glance statt Homepage als Single-Dashboard | beide auf Augenhoehe; Homepage etablierter, Glance moderner, schneller, mit Live-Widgets | github.com/glanceapp/glance vs. github.com/gethomepage/homepage | Glance legitim; Bewertung: deine Wahl ist verteidigbar |
|
||||||
|
| Immich nicht hinter Authelia ForwardAuth | offizielle Immich-Doku raet bei nativer App-Auth davon ab, weil Sync-Clients OIDC oder direkte Auth brauchen | immich.app/docs/administration/reverse-proxy | korrekt; OIDC spaeter (F-13) ist der Weg |
|
||||||
|
| Nextcloud klassisch statt AIO | NC-AIO ist offizielle Empfehlung fuer Neuaufbau, klassisch hat mehr Flexibilitaet fuer GitOps | NC-Blog 2024 | bewusste Ausnahme MASTER 13; vertretbar, da GitOps-Anbindung wichtiger |
|
||||||
|
| Single-Host + Borg statt Proxmox-Cluster + ZFS-Send | fuer Familien-Homelab ist Cluster Overkill | r/homelab, LTT-Forum | Single-Host korrekt; Cold-Standby (Sprint 6) ist die richtige naechste Stufe |
|
||||||
|
| AdGuard + Unbound statt Pi-hole | aequivalent; AdGuard hat moderne UI, Unbound recursion | gowri/networkchuck Tutorials 2024 | passt |
|
||||||
|
| Posture-Check als Skript statt Goss/InSpec | fuer Single-Host pragmatisch | Goss ist maechtiger, aber Overkill | Skript-Loesung legitim |
|
||||||
|
| Renovate gegen Gitea | mehrere Erfahrungsberichte in Gitea-Issues + Renovate-Docs | docs.renovatebot.com/modules/platform/gitea/ | Standard-Pfad |
|
||||||
|
| CrowdSec vor Traefik | starker Trend 2024-25 in Selfhosted-Community | crowdsec.net/blog, Marius-Hosting-Tutorials | sinnvolle Haertung |
|
||||||
|
|
||||||
|
**Gegenpositionen, die du kennen solltest:**
|
||||||
|
|
||||||
|
- **"Authelia OIDC ist kompliziert, lieber Authentik."** Korrekt, wenn du auch B2B-SAML brauchst. Fuer reine Familien-OIDC ist Authelia leichter wartbar.
|
||||||
|
- **"CrowdSec laedt zentrale Reputation-Listen -> Privacy-Bedenken."** Stimmt, du kannst Local-Only-Mode fahren. Fuer Homelab egal.
|
||||||
|
- **"Renovate-Bot erzeugt Laerm."** Mit Group/Schedule-Rules zaehmbar. Wert > Laerm.
|
||||||
|
- **"Komodo ist zu jung."** Gegenargument: du benutzt es seit Q1/2026 produktiv, Major-Inzidenz war beherrschbar. Der Wechsel zurueck zu Portainer/Dockge waere hoehere Kosten als der Reifegrad-Nachteil.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# K. Endziel — "Nerd-Level Homelab"
|
||||||
|
|
||||||
|
So sieht dein Homelab aus, wenn es wirklich auf Senior-Level ist:
|
||||||
|
|
||||||
|
**Betrieb im Alltag**
|
||||||
|
|
||||||
|
- Morgens 30 Sekunden auf `monitoring.kaleschke.info`: Family-View zeigt 7 Tier-1-Services gruen, Backup-Job in der Nacht hat 100 % Files erfasst, alle Certs > 30 Tage, Disk < 80 %.
|
||||||
|
- Push-Benachrichtigung auf dem Handy nur, wenn wirklich etwas brennt (Posture-Check critical, Borg > 30 h, Endpoint down ≥ 8 Min, Cert < 14 Tage).
|
||||||
|
- Familienmitglieder loggen sich mit einem Login bei Nextcloud, Immich, Mealie ein. 2FA per TOTP-App, kein App-by-App-Passwortzettel mehr.
|
||||||
|
|
||||||
|
**Updates**
|
||||||
|
|
||||||
|
- Renovate oeffnet woechentlich 5–10 Pull-Requests in Gitea fuer Patch-Versionen. Du siehst sie im Web-UI, pruefst Release Notes, klickst Merge. Komodo deployt automatisch via Webhook. Smoke-Test in der naechsten Glance-Seite.
|
||||||
|
- Major-Updates kommen separat mit Label `major`, behandelst du in einem geplanten Slot mit Restore-Snapshot davor.
|
||||||
|
|
||||||
|
**Backups**
|
||||||
|
|
||||||
|
- Borg lokal alle 6 h, Borg Hetzner taeglich, Wechselplatte monatlich. Borg-Passphrase auf Papier im Bankschliessfach. Alle drei Ziele juenger als 36 h Alert-Schwelle.
|
||||||
|
- Pre-Dump-Hooks erzeugen 15 konsistente Dump-Artefakte pro Lauf, automatische Posture-Check-Vor-Hook bricht Backup ab, wenn FS oder Mount sich veraendert haben.
|
||||||
|
|
||||||
|
**Restore**
|
||||||
|
|
||||||
|
- Monatlicher Mini-Restore-Test fuer Vaultwarden/Gitea/Paperless/Immich nach `/mnt/user/backups/restore-lab/<dienst>/` laeuft automatisiert per Gitea Actions, Erfolgs-Report landet als Datei + ntfy-Info.
|
||||||
|
- Quartalsweise End-to-End-Drill: ein kompletter Stack restauriert, App startet, Smoke-Test passt, Doku validiert. Komodo-Bootstrap-Pfad ist getestet.
|
||||||
|
- Im Ernstfall folgst du `docs/DISASTER_RECOVERY.md` Phase 0–5 und bist nach < 8 h wieder im Vollbetrieb. Repo-Bootstrap aus GitHub-Mirror, Stacks in Stufen 1–5, Verifikation pro Stufe.
|
||||||
|
|
||||||
|
**Monitoring**
|
||||||
|
|
||||||
|
- Prometheus + Loki + Grafana + Alertmanager-ntfy-Bridge. ~15 Alert-Regeln, alle in `alerts.yml` versioniert.
|
||||||
|
- Family-View, Host-Overview, Containers+Logs, Traefik-Standalone, Backup-Frische, Cert-Tage. Sechs Dashboards mehr braucht keine Familie.
|
||||||
|
- Loki sammelt 30 Tage Logs aus allen Containern via Promtail. cAdvisor + node-exporter liefern Container- und Host-Metriken. Blackbox testet oeffentliche Endpoints alle 60 s.
|
||||||
|
|
||||||
|
**Security**
|
||||||
|
|
||||||
|
- Authelia OIDC fuer Nextcloud, Immich, Grafana, Mealie. ForwardAuth fuer Operator-UIs mit 2FA-Pflicht ab Tier-2.
|
||||||
|
- CrowdSec sperrt Brute-Force-IPs auf Traefik-Ebene bevor sie die Apps treffen.
|
||||||
|
- AdGuard-Admin nur via Tailscale. Operator-Pfad ausschliesslich Tailscale.
|
||||||
|
- Authelia-Repo-Baseline und Host-Config sind per Diff-Check im posture-check abgesichert.
|
||||||
|
- Secrets-Mounts mode 600, Borg-Passphrase analog.
|
||||||
|
|
||||||
|
**Dokumentation**
|
||||||
|
|
||||||
|
- SERVICE_CATALOG, RESTORE_MATRIX, DISASTER_RECOVERY, STORAGE_LAYOUT, SECRETS_MAP, WORKFLOW, GITOPS_DRIFT_RUNBOOK, ALERTING_MAP, HOMELAB_ARCHITECTURE_MASTER bleiben aktuelle Single-Source-of-Truth.
|
||||||
|
- `services/komodo-bootstrap/` loest das Henne-Ei. `SERVICES_RECOVERY.md` ist final, nicht Draft.
|
||||||
|
- Familien-Onboarding-Doku als Markdown: "So nutzt du Nextcloud-Web", "So aktivierst du Immich-Foto-Backup auf dem Handy", "So loggst du dich neu per 2FA ein".
|
||||||
|
|
||||||
|
**Taegliche Nutzung**
|
||||||
|
|
||||||
|
- Familie scannt Briefe per ASN-Barcode in `scans_inbox/`, Paperless tagged via paperless-gpt automatisch, alles durchsuchbar.
|
||||||
|
- Immich erfasst Smartphone-Fotos aller Familienmitglieder automatisch, Familie blaettert per Web/App, Tagging per ML.
|
||||||
|
- Nextcloud traegt Kalender, Kontakte, geteilte Familienordner per WebDAV/CardDAV — kein Google/Apple-Lock-In.
|
||||||
|
- Mealie speichert Rezepte, Einkaufsliste auf dem Handy.
|
||||||
|
- Vaultwarden ist der einzige Passwort-Tresor der Familie; Familien-Organisation aktiv.
|
||||||
|
- Plex streamt Heim-Medien an alle Endgeraete.
|
||||||
|
- ntfy schickt dir Vorfaelle aufs Handy — und sonst nichts.
|
||||||
|
- Optional: Firefly III fuer die Familien-Finanzen, Ecowitt-Wetter-Dashboard, Home-Assistant-Automationen fuer Strom-Eigenverbrauch.
|
||||||
|
|
||||||
|
**Was bewusst weggelassen ist**
|
||||||
|
|
||||||
|
- Kein Kubernetes. Komodo + Compose reicht.
|
||||||
|
- Kein zweiter Medienserver neben Plex (Jellyfin-Entscheidung 2026-05-25).
|
||||||
|
- Kein zweites Dashboard neben Glance (Homepage-Entscheidung 2026-05-25).
|
||||||
|
- Kein Uptime-Kuma neben Blackbox (Entscheidung 2026-05-25).
|
||||||
|
- Kein Hermes-Agent, wenn er bis 2026-07-25 keinen klaren Alltagsnutzen liefert.
|
||||||
|
- Kein BentoPDF/paperless-gpt 24/7, wenn nicht aktiv genutzt.
|
||||||
|
- Kein Self-Stack-Komodo (durch `services/komodo-bootstrap/` ersetzt).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schlussbemerkung
|
||||||
|
|
||||||
|
Das Setup ist naeher an Senior-Reife als an Bastel-Niveau. Der groesste Hebel der naechsten drei Monate ist **Konsolidieren statt erweitern** (Hermes-Entscheidung, Altstaende raus, Auth-SSO, Off-Site-Diversitaet), kombiniert mit der einen Aktivierung, die das Setup vom Operator-Tool zum **Familien-Tool** macht: Immich-Smartphone-Backup fuer alle.
|
||||||
|
|
||||||
|
Das vorhandene 2026-05-23-Audit hat die richtigen Sprintziele bereits identifiziert. Diese externe Audit-Sicht ergaenzt:
|
||||||
|
|
||||||
|
- **2FA-Pflicht auf Tier-1-Operator-UIs** (F-04) — fehlt in der Bewertung 2026-05-23 in dieser Klarheit.
|
||||||
|
- **Healthcheck-Luecke** (F-15) und **fehlende Mem-Limits** (F-19) — operative Detail-Findings, die in der strategischen Bewertung nicht auftauchen.
|
||||||
|
- **Komodo-Self-Bootstrap als konkreter Code-Vorschlag** (F-09) statt nur als Risiko-Erwaehnung.
|
||||||
|
- **Authelia-Drift-Detection automatisieren** (F-10) statt nur "manuell merge".
|
||||||
|
- **Monitoring-Stack ohne Digest-Pin** (F-07) — Inkonsistenz mit der eigenen Image-Pinning-Disziplin.
|
||||||
|
- **`infra/redis` ist faktisch nicht shared** (F-16) — Etikett-Realitaet-Drift.
|
||||||
|
- **Alert-Regeln deutlich zu duenn** (F-08) — Sichtbarkeitsluecken bei Cert/Borg/Container-Down.
|
||||||
|
|
||||||
|
Wenn Sprint 1–3 in 4–6 Wochen sitzen, bist du auf einer 1-Note. Wenn dann Sprint 4–5 in weiteren 6–8 Wochen kommen, hat die Familie ein echtes Self-Hosting-System, kein "Container-Sammlung im Keller". Das ist der Unterschied, den der Audit-Auftrag adressiert.
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Audit TODO 2026-05-25
|
||||||
|
|
||||||
|
Quelle: `docs/AUDIT_2026-05-25.md`
|
||||||
|
|
||||||
|
Status: Arbeitsliste fuer die Umsetzung. Authelia-2FA/OIDC bleibt bewusst spaet, weil die Ziel-Policy noch nicht final entschieden ist.
|
||||||
|
|
||||||
|
## Leitplanken
|
||||||
|
|
||||||
|
- Keine Authelia-2FA-ACL-Aenderungen in den ersten Sprints.
|
||||||
|
- Keine Live-riskanten Bind-/Port-Aenderungen ohne vorher erfasste Host-Werte, insbesondere Tailscale-IP.
|
||||||
|
- Erst Inventar und Baseline, dann Aenderungen.
|
||||||
|
- Jede produktive Aenderung bekommt Validierung und Rollback-Hinweis.
|
||||||
|
|
||||||
|
## Naechster Startpunkt 2026-05-26
|
||||||
|
|
||||||
|
Kontext bewusst gesichert, bevor weitere Live-Aenderungen passieren:
|
||||||
|
|
||||||
|
1. USV-Entscheidung treffen: aktuell ist keine funktionierende USV-Abschaltung nachgewiesen.
|
||||||
|
2. Gitea-Bundle-/Mirror-Mechanik und Borg-Passphrase-Offsite-Sicherung entscheiden.
|
||||||
|
3. Authelia 2FA/OIDC weiterhin nicht anfassen; das bleibt bewusst der letzte Block.
|
||||||
|
|
||||||
|
## Sprint 0 - Inventar und Baseline
|
||||||
|
|
||||||
|
| Status | Aufgabe | Ergebnis |
|
||||||
|
|---|---|---|
|
||||||
|
| erledigt | Hardware-Inventar ausfuellen | CPU, RAM, Mainboard, BIOS, NIC, Controller, Disks, SMART und Capacity-Baseline erfasst; USV ist als nicht validiert dokumentiert |
|
||||||
|
| in Arbeit | Netzwerk-Inventar ausfuellen | Host-IP, Gateway, Tailscale-IP und AdGuard-Bind erfasst; Router-/VLAN-Details offen |
|
||||||
|
| erledigt (Baseline) | Externe Abhaengigkeiten dokumentieren | `docs/EXTERNAL_DEPENDENCIES.md` enthaelt bekannte Provider, Kritikalitaet, Ausfallplaene; Account-Recovery-Codes/Zahlungswege bleiben Off-Repo-Operatorcheck |
|
||||||
|
| erledigt (Baseline) | Services-Recovery-Pfade beschreiben | `docs/SERVICES_RECOVERY.md` enthaelt Gitea-/Komodo-/Secrets-Sonderpfade; Gitea-Bundle-/Mirror-Mechanik bleibt als Umsetzungsentscheidung offen |
|
||||||
|
| erledigt | Baseline-Tag setzen | `audit-2026-05-25-baseline` ist lokal und remote vorhanden |
|
||||||
|
| erledigt | Policy-Check neu ausfuehren | SEC001-Warnings aus altem Report sind nicht mehr aktuell |
|
||||||
|
|
||||||
|
## Sprint 1 - Nicht-kontroverse Sicherheits- und Repo-Hygiene
|
||||||
|
|
||||||
|
| Status | Aufgabe | Ergebnis |
|
||||||
|
|---|---|---|
|
||||||
|
| offen | Borg-Passphrase analog sichern | Passphrase ist ohne Host/Vaultwarden wiederherstellbar |
|
||||||
|
| erledigt (repo) | AdGuard Admin-Bind vorbereiten | Tailscale-IP `100.80.98.33` erfasst, Compose-Soll geaendert |
|
||||||
|
| erledigt | AdGuard Admin-Port auf Tailscale-IP binden | Live validiert: `ss -ltnp` zeigt `100.80.98.33:8082`, DNS auf Port 53 funktioniert, LAN-Zugriff auf `192.168.178.58:8082` schlaegt fehl |
|
||||||
|
| erledigt | Alte Monitoring-Verzeichnisse entfernen | `ops/grafana-influxdb/` und `ops/loki/` sind aus dem aktiven Repo entfernt; Rollback erfolgt ueber Git-Historie |
|
||||||
|
| erledigt | Komodo/Gitea-Restdrift bereinigen | alter Komodo-Stack `grafana` ist inert und ohne Repo-Pfad/Webhook; Gitea-Hook `35` und `komodo`-Self-Hook `11` sind inaktiv; aktive Gitea-Hooks haben keine Fehlstatus |
|
||||||
|
| erledigt | Policy-Warnings triagieren | Plex Host-Netz und digest-gepinnte mutable Tags sind dokumentierte Info-Ausnahmen; `monitoring-influxdb3-core` als Root-Ausnahme bleibt bewusst als Warning sichtbar |
|
||||||
|
|
||||||
|
## Sprint 2 - Storage und Recovery verbindlich machen
|
||||||
|
|
||||||
|
| Status | Aufgabe | Ergebnis |
|
||||||
|
|---|---|---|
|
||||||
|
| offen | `docs/STORAGE_LAYOUT.draft.md` finalisieren | Datei wird als `docs/STORAGE_LAYOUT.md` Active gefuehrt |
|
||||||
|
| offen | Disk- und Share-TBDs eintragen | Modelle, Groessen, Seriennummern, Filesysteme und Cache-Settings sind dokumentiert |
|
||||||
|
| offen | Gitea-Repo-Mirror-Mechanik definieren | Mirror fuer `/mnt/user/services/gitea/git/repositories/` mit Frequenz <= 6 h ist spezifiziert |
|
||||||
|
| offen | Komodo-Bootstrap-Pfad beschreiben | Kaltstart ohne laufendes Komodo ist dokumentiert |
|
||||||
|
| offen | Immich-Restore-Test planen | Testumfang, Datenpfade und Smoke-Test-Kriterium stehen fest |
|
||||||
|
|
||||||
|
## Sprint 3 - Restore und Monitoring
|
||||||
|
|
||||||
|
| Status | Aufgabe | Ergebnis |
|
||||||
|
|---|---|---|
|
||||||
|
| offen | Immich-Restore-Test implementieren | Restore-Report landet unter `/mnt/user/backups/restore-reports/` |
|
||||||
|
| offen | Borg-Stale-Alert bauen | Alarm feuert, wenn Borg-Archiv zu alt ist |
|
||||||
|
| offen | TLS-Cert-Expiry-Alert bauen | Alarm feuert bei Restlaufzeit unter Schwellwert |
|
||||||
|
| offen | Container-Down-Alert bauen | Unerwartet fehlende Container werden sichtbar |
|
||||||
|
| offen | Family-View Dashboard definieren | Uptime, Backup-Frische, Cert-Tage, Disk-Fuellung auf einer Seite |
|
||||||
|
|
||||||
|
## Sprint 4 - Familien- und Betriebsdoku
|
||||||
|
|
||||||
|
| Status | Aufgabe | Ergebnis |
|
||||||
|
|---|---|---|
|
||||||
|
| offen | Familien-Onboarding schreiben | Nextcloud, Immich, Vaultwarden, 2FA-Verlust, Ausfallverhalten kurz erklaert |
|
||||||
|
| erledigt (Baseline) | Capacity-/Lifecycle-Review erstellen | Cache 6 %, Array/User-Shares 33 %, lokale Backups 2.2G; externe Backup-/Cold-Storage-Groessen bleiben offen |
|
||||||
|
| offen | USV-Test oder USV-Entscheidung | Power-Loss-Verhalten ist bekannt und dokumentiert |
|
||||||
|
|
||||||
|
## Sprint 5 - Auth und Frontdoor, bewusst zuletzt
|
||||||
|
|
||||||
|
| Status | Aufgabe | Ergebnis |
|
||||||
|
|---|---|---|
|
||||||
|
| geparkt | Authelia 2FA fuer Operator-UIs erweitern | Erst nach finaler Policy-Entscheidung |
|
||||||
|
| geparkt | Authelia OIDC fuer Apps pruefen | Erst nach Familien-/Client-Auswirkungsanalyse |
|
||||||
|
| geparkt | CrowdSec vor Traefik pruefen | Nach stabiler Auth-/Monitoring-Basis |
|
||||||
|
|
||||||
|
## Offene Host-Werte
|
||||||
|
|
||||||
|
Diese Werte muessen am Unraid-Host erhoben werden, bevor die entsprechenden Aenderungen sauber umgesetzt werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hostname
|
||||||
|
cat /proc/cpuinfo | awk '/model name|flags/ {print; if(/flags/) exit}'
|
||||||
|
free -h
|
||||||
|
dmidecode -t baseboard | head -30
|
||||||
|
ip -br link
|
||||||
|
tailscale ip -4
|
||||||
|
lsblk -o NAME,SIZE,MODEL,SERIAL,FSTYPE,MOUNTPOINT,VENDOR
|
||||||
|
df -h /mnt/cache /mnt/disk1 /mnt/user
|
||||||
|
smartctl -a /dev/nvme0n1 | head -80
|
||||||
|
smartctl -a /dev/sdb | head -80
|
||||||
|
```
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Audit Report - KalliLab CORE
|
||||||
|
Datum: 2026-05-16
|
||||||
|
Gepruefte Compose-Dateien: 30
|
||||||
|
|
||||||
|
## Kritische Befunde
|
||||||
|
|
||||||
|
Keine kritischen Befunde im Repo-Sollzustand gefunden.
|
||||||
|
|
||||||
|
- Keine Datenbank und kein Cache haengt in `frontend_net`.
|
||||||
|
- Keine produktive Compose-Datei ohne explizites `networks:`-Feld gefunden.
|
||||||
|
- Keine produktive `.env`- oder `stack.env`-Datei ohne `.example`-Suffix im Repository gefunden.
|
||||||
|
- Keine Klartext-Passwoerter, Tokens oder API-Keys in Compose-`environment:`-Bloecken gefunden.
|
||||||
|
|
||||||
|
## Mittlere Befunde
|
||||||
|
|
||||||
|
- Docker-Socket ausserhalb der im Audit-Prompt genannten Allowlist: `traefik` mountet `/var/run/docker.sock:/var/run/docker.sock:ro` in `traefik/docker-compose.yml:34`. Der Socket ist fuer den Docker-Provider fachlich nachvollziehbar, aber in `HOMELAB_ARCHITECTURE_MASTER_V2.md` Abschnitt 10 nicht explizit als Docker-Socket-Ausnahme aufgefuehrt.
|
||||||
|
- Docker-Socket ausserhalb der im Audit-Prompt genannten Allowlist: `alloy` mountet `/var/run/docker.sock:/var/run/docker.sock:ro` in `ops/loki/docker-compose.yml:26`. [AUSNAHME - dokumentiert] in `HOMELAB_ARCHITECTURE_MASTER_V2.md` Abschnitt 10 als `alloy` Docker-Socket read-only, aber nicht in der Prompt-Allowlist genannt.
|
||||||
|
- `komodo-periphery` haengt ohne Web-UI im `frontend_net` in `ops/komodo/docker-compose.yml:92-94`. Die Compose-Datei dokumentiert an `ops/komodo/docker-compose.yml:77-79` den Grund als Git-Zugriff auf internes Gitea und Docker-Agent-Sonderfall. [AUSNAHME - lokal in Compose dokumentiert]
|
||||||
|
- `hermes_net` ist ein app-internes Netz ohne `internal: true`: `ops/hermes-agent/docker-compose.yml:91-93`. Das ist nicht Teil der explizit geforderten `internal: true`-Liste, aber strukturell auffaellig, weil Gateway und Dashboard intern ueber dieses Netz sprechen.
|
||||||
|
- `grafana` nutzt zusaetzlich `backend_net` in `ops/grafana-influxdb/docker-compose.yml:27-30`, obwohl die Architektur das Grafana/Influx-Paar primaer als `frontend_net` + `grafana_influx_internal` beschreibt. Grund ist vermutlich Loki-Datasource-Zugriff; `docs/REPO_MAP.md` nennt `backend_net` fuer Grafana-Loki-Datasource. [AUSNAHME - dokumentiert]
|
||||||
|
- `paperless-gpt` verwendet `PAPERLESS_API_TOKEN` als Stack-ENV in `apps/paperless-gpt/docker-compose.yml:15`, taucht aber in `docs/SECRETS_MAP.md` nicht als eigener aktiver Secret-Eintrag auf.
|
||||||
|
|
||||||
|
## Hinweise
|
||||||
|
|
||||||
|
- Direkte Host-Ports sind nur bei dokumentierten Ausnahmen vorhanden:
|
||||||
|
- [AUSNAHME - dokumentiert] `traefik`: `80:80`, `443:443` in `traefik/docker-compose.yml:30-32`.
|
||||||
|
- [AUSNAHME - dokumentiert] `gitea`: `222:22` in `core/gitea/docker-compose.yml:17-18`.
|
||||||
|
- [AUSNAHME - dokumentiert] `adguard`: `53:53/tcp`, `53:53/udp`, `8082:80` in `host-services/Adguard/docker-compose.yml:13-16`.
|
||||||
|
- [AUSNAHME - dokumentiert] `influxdb3-core`: `${INFLUXDB_BIND_IP:-127.0.0.1}:8181:8181` in `ops/grafana-influxdb/docker-compose.yml:54-55`.
|
||||||
|
- `backend_net` wird in allen Compose-Dateien als `external: true` referenziert, z. B. `infra/postgresql17/docker-compose.yml:25-26`, `infra/redis/docker-compose.yml:22-23`, `traefik/docker-compose.yml:59-60`. Ob das externe Docker-Netz live wirklich `internal: true` ist, ist aus dem Repo allein nicht verifizierbar.
|
||||||
|
- `grafana_influx_lan` hat bewusst kein `internal: true`: `ops/grafana-influxdb/docker-compose.yml:88-89`. [AUSNAHME - dokumentiert]
|
||||||
|
- Mutable Tags sind mit Digests gepinnt, aber semantisch weiterhin mutable:
|
||||||
|
- `immich-server`: `release@sha256` in `apps/immich/docker-compose.yml:4`.
|
||||||
|
- `immich-machine-learning`: `release@sha256` in `apps/immich/docker-compose.yml:35`.
|
||||||
|
- `tailscale`: `stable@sha256` in `host-services/tailscale/docker-compose.yml:3`.
|
||||||
|
- `ddns-updater`: `latest@sha256` in `infra/ddns-updater/docker-compose.yml:3`.
|
||||||
|
- `komodo-core`: Major-Tag `2@sha256` in `ops/komodo/docker-compose.yml:36`.
|
||||||
|
- `komodo-periphery`: Major-Tag `2@sha256` in `ops/komodo/docker-compose.yml:82`.
|
||||||
|
- `uptime-kuma`: Major-Tag `1@sha256` in `ops/uptime-kuma/docker-compose.yml:3`.
|
||||||
|
- Reines `image: name:latest` ohne Digest wurde in produktiven Compose-Dateien nicht gefunden.
|
||||||
|
- Alle Services haben `restart: unless-stopped`.
|
||||||
|
- Alle Services haben `security_opt: no-new-privileges:true`.
|
||||||
|
- [AUSNAHME - dokumentiert] `scrutiny` nutzt `privileged: true` in `ops/scrutiny/docker-compose.yml:6`.
|
||||||
|
- [AUSNAHME - dokumentiert] `tailscale` nutzt `network_mode: host` in `host-services/tailscale/docker-compose.yml:6`.
|
||||||
|
|
||||||
|
## Dokumentations-Abweichungen
|
||||||
|
|
||||||
|
- Service-Namen in Compose vs. `docs/SERVICE_CATALOG.md` weichen bei Immich ab: Compose nutzt `immich-server`, `immich-machine-learning`, `database`, `redis` in `apps/immich/docker-compose.yml:2`, `:33`, `:44`, `:53`; der Katalog dokumentiert `immich_server`, `immich_machine_learning`, `immich_postgres`, `immich_redis`.
|
||||||
|
- Service-Name in Compose vs. `docs/SERVICE_CATALOG.md` weicht bei Paperless ab: Compose nutzt `paperless` in `apps/paperless/docker-compose.yml:2`; der Katalog dokumentiert `paperless-ngx`.
|
||||||
|
- Service-Name in Compose vs. `docs/SERVICE_CATALOG.md` weicht bei Redis ab: Compose nutzt `redis` in `infra/redis/docker-compose.yml:2`; der Katalog dokumentiert `Redis`.
|
||||||
|
- Traefik-Host bei Hermes ist im Compose variabel: `Host(`${HERMES_DASHBOARD_HOST}`)` in `ops/hermes-agent/docker-compose.yml:81`; `docs/REPO_MAP.md` dokumentiert konkret `hermes.kaleschke.info`. Das ist plausibel, aber maschinell nicht eindeutig abgleichbar.
|
||||||
|
- `docs/REPO_MAP.md` Volumes-Tabelle ist fuer mehrere Mounts nur zusammenfassend, nicht pfadgenau. Beispiele mit Compose-Pfaden, die dort nicht wortgleich auftauchen:
|
||||||
|
- `/mnt/user/appdata/unbound/config` in `apps/unbound/docker-compose.yml:7`.
|
||||||
|
- `/mnt/user/appdata/ddns-updater` in `infra/ddns-updater/docker-compose.yml:17`.
|
||||||
|
- `/mnt/user/appdata/filebrowser/database` und `/mnt/user/appdata/filebrowser/config` in `ops/filebrowser/docker-compose.yml:15-16`.
|
||||||
|
- `/mnt/user/appdata/borg-ui/restore` in `ops/borg-ui/docker-compose.yml:27`.
|
||||||
|
- Netzwerke in Compose sind alle in `docs/REPO_MAP.md` aufgefuehrt.
|
||||||
|
- Traefik-Hosts aus Compose sind in `docs/REPO_MAP.md` aufgefuehrt; einzige Besonderheit ist der variable Hermes-Host.
|
||||||
|
- `.example`-Dateien haben passende Gegenspieler in `.gitignore`: `.env`, `*.env`, `!*.env.example`, `**/stack.env`, `!**/stack.env.example`.
|
||||||
|
- Keine `.keep`-Platzhalter-Dateien gefunden.
|
||||||
|
|
||||||
|
## Offene Architektur-Punkte: aktueller Stand
|
||||||
|
|
||||||
|
- `immich_redis`: weiterhin kein named volume. Compose-Service `redis` in `apps/immich/docker-compose.yml:44-50` hat aktuell gar keinen `volumes:`-Block. Architekturpunkt bleibt offen.
|
||||||
|
- `AdGuard Home`: Admin-Port ist weiterhin direkt veroeffentlicht: `8082:80` in `host-services/Adguard/docker-compose.yml:15`; keine Traefik-Labels vorhanden. Block F bleibt offen.
|
||||||
|
- `filebrowser`: Appdata-Breitmount ist entfernt; aktuelle Mounts sind `/mnt/user/documents`, `/mnt/user/photos`, `/mnt/user/projekte` plus eigene DB/Config in `ops/filebrowser/docker-compose.yml:12-16`. Langfristiges Hardening bleibt moeglich, aber der groesste alte Breitmount ist erledigt.
|
||||||
|
- `bentopdf`: Compose ist vorhanden und Traefik-abgesichert in `apps/bentopdf/docker-compose.yml:2-27`. Runtime-Deploy und fachliche Abnahme koennen aus dem Repo allein nicht bestaetigt werden; Architekturpunkt bleibt als Live-Pruefung offen.
|
||||||
|
- `grafana` und `influxdb3-core`: laufen weiterhin als `user: "0"` in `ops/grafana-influxdb/docker-compose.yml:6` und `ops/grafana-influxdb/docker-compose.yml:53`. [AUSNAHME - dokumentiert]
|
||||||
|
- `plex-media-server`: keine Compose-Datei im Repo gefunden; bleibt Dockerman-/Host-Sonderfall laut Architektur.
|
||||||
|
|
||||||
|
## Bestanden
|
||||||
|
|
||||||
|
- 30 produktive `docker-compose.yml` wurden geprueft.
|
||||||
|
- Keine Datenbanken oder Caches im `frontend_net`: `postgresql17` nur `backend_net` in `infra/postgresql17/docker-compose.yml:18`; shared `redis` nur `backend_net` in `infra/redis/docker-compose.yml:15`; `mealie-postgres` nur `mealie_internal` in `apps/mealie/docker-compose.yml:56`; Immich `database` und `redis` nur `immich_default` in `apps/immich/docker-compose.yml:48` und `:64`; Nextcloud DB/Redis nur `nextcloud_internal` in `apps/nextcloud/docker-compose.yml:61` und `:73`; `komodo-mongo` nur `komodo_net` in `ops/komodo/docker-compose.yml:16`.
|
||||||
|
- App-interne Pflichtnetze sind korrekt `internal: true`: `immich_default` in `apps/immich/docker-compose.yml:73-75`, `mealie_internal` in `apps/mealie/docker-compose.yml:66-68`, `nextcloud_internal` in `apps/nextcloud/docker-compose.yml:82-84`, `grafana_influx_internal` in `ops/grafana-influxdb/docker-compose.yml:90-91`.
|
||||||
|
- `frontend_net` und `backend_net` werden in Compose-Dateien als `external: true` referenziert.
|
||||||
|
- Alle Services mit `traefik.enable=true` setzen explizit `traefik.docker.network=frontend_net`, `entrypoints=websecure`, `tls=true` und `tls.certresolver=le`.
|
||||||
|
- Kein Traefik-Label mit `yourdomain.tld` gefunden.
|
||||||
|
- Admin-/Ops-Router haben Middleware `authelia@file,secure-headers@file`, u. a. `homepage` in `apps/homepage/docker-compose.yml:31`, `filebrowser` in `ops/filebrowser/docker-compose.yml:27`, `scrutiny` in `ops/scrutiny/docker-compose.yml:32`, `grafana` in `ops/grafana-influxdb/docker-compose.yml:46`. [AUSNAHME - dokumentiert] `komodo-core` hat keine ForwardAuth-Middleware in `ops/komodo/docker-compose.yml:64-71`; [AUSNAHME - dokumentiert] `nextcloud` nutzt native Auth und nur Redirect-Middleware in `apps/nextcloud/docker-compose.yml:42-46`.
|
||||||
|
- Web-UIs ohne Traefik-Labels wurden nur als dokumentierte Sonderfaelle gefunden: `adguard` mit LAN-Admin-Port in `host-services/Adguard/docker-compose.yml:13-16`; `ddns-updater` hat keine Web-UI und braucht Internet in `infra/ddns-updater/docker-compose.yml:8`; `komodo-periphery` ist Agent ohne Traefik-Route in `ops/komodo/docker-compose.yml:81-103`.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Capacity and Lifecycle - KalliLab CORE
|
||||||
|
|
||||||
|
Status: Initiale Capacity-Baseline 2026-05-26; externe Backup-/Cold-Storage-Groessen offen.
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
|
||||||
|
Dieses Dokument haelt Wachstum, Schwellenwerte und Upgrade-Trigger fest. Es verhindert, dass Storage-, RAM- oder Backup-Entscheidungen erst dann getroffen werden, wenn der Host bereits unter Druck steht.
|
||||||
|
|
||||||
|
## Aktuelle Kapazitaet
|
||||||
|
|
||||||
|
| Bereich | Groesse | Belegt | Frei | Schwellwert | Bewertung |
|
||||||
|
|---|---:|---:|---:|---:|---|
|
||||||
|
| Cache | 1.9T | 97G | 1.8T | 70 % Planung / 85 % Aktion | gruen, 6 % belegt |
|
||||||
|
| Disk1 / Array | 5.5T | 1.8T | 3.7T | 80 % Planung / 90 % Aktion | gruen, 33 % belegt |
|
||||||
|
| User Shares gesamt | 5.5T | 1.8T | 3.7T | 80 % Planung / 90 % Aktion | gruen, entspricht aktuell Disk1 |
|
||||||
|
| Backups lokal | 5.5T geteilter Array-Space | 2.2G unter `/mnt/user/backups` | 3.7T Share-frei | Review bei Borg-/Dump-Wachstum | lokal nicht unabhaengig vom Array |
|
||||||
|
| Hetzner Borg | TBD | TBD | TBD | TBD | TBD |
|
||||||
|
| Externe Cold-Platte | TBD | TBD | TBD | TBD | TBD |
|
||||||
|
|
||||||
|
Pruefkommando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
df -h /mnt/cache /mnt/disk1 /mnt/user
|
||||||
|
du -sh /mnt/user/appdata/* | sort -hr | head -30
|
||||||
|
du -sh /mnt/user/documents /mnt/user/photos /mnt/user/media /mnt/user/backups 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wachstumsbereiche
|
||||||
|
|
||||||
|
| Bereich | Erwartetes Wachstum | Risiko | Naechste Aktion |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Medien | aktuell ca. 1.7T | groesster Speicherblock | Array-Erweiterung vor 80 % planen |
|
||||||
|
| Immich Fotos/Videos | aktuell ca. 23G | hoechster privater Datentopf | Restore-Test priorisieren |
|
||||||
|
| Paperless/Dokumente | aktuell ca. 199M im Documents-Share | wichtig, moderates Wachstum | Restore-Test existiert, Share-Wachstum beobachten |
|
||||||
|
| Nextcloud | TBD | Familiennutzung kann stark wachsen | Quota/Backup pruefen |
|
||||||
|
| Monitoring/Loki | begrenzt durch Retention | Retention kann Disk fuellen | Retention und Volume-Groesse bei Reviews pruefen |
|
||||||
|
| Borg Dumps | aktuell ca. 2.2G lokale Backups | Retention und Excludes pruefen | Borg-Stale + Groessenprofil |
|
||||||
|
|
||||||
|
## Upgrade-Trigger
|
||||||
|
|
||||||
|
| Trigger | Massnahme |
|
||||||
|
|---|---|
|
||||||
|
| Cache dauerhaft >70 % | Zweite NVMe oder Appdata-Verteilung planen |
|
||||||
|
| Cache >85 % | Sofortmassnahme, keine grossen Deployments |
|
||||||
|
| Disk1 >80 % | Array-Erweiterung planen |
|
||||||
|
| Disk1 >90 % | Keine neuen grossen Datenimporte, Erweiterung priorisieren |
|
||||||
|
| RAM >90 % ueber 10 Minuten regelmaessig | RAM-Ausbau oder Service-Limits pruefen |
|
||||||
|
| Borg-Laufzeit deutlich steigend | Scope, Netzwerk und Ziel pruefen |
|
||||||
|
| SMART-Warnung | Ersatz planen, Restore-/Backup-Frische pruefen |
|
||||||
|
| Keine USV-Abschaltung | USV anschaffen/anschliessen oder Power-Loss-Risiko bewusst akzeptieren |
|
||||||
|
|
||||||
|
## Restore-Zeitziele
|
||||||
|
|
||||||
|
| Tier | Beispiel | Zielzeit | Status |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| Tier 0 | Repo, Secrets, Traefik, DNS | TBD | offen |
|
||||||
|
| Tier 1 | Gitea, Vaultwarden, Paperless, Immich | TBD | offen |
|
||||||
|
| Tier 2 | Nextcloud, Mealie, Monitoring | TBD | offen |
|
||||||
|
| Tier 3 | Komfort-/Ops-Tools | TBD | offen |
|
||||||
|
|
||||||
|
## Review-Log
|
||||||
|
|
||||||
|
| Datum | Befund | Entscheidung |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-05-26 | Cache 6 %, Array/User-Shares 33 %, lokale Backups 2.2G; keine validierte USV-Abschaltung | Capacity gruen; naechste operative Risiken sind USV-Entscheidung und externe Backup-/Cold-Storage-Groessen |
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Codex-Prompt: KalliLab Endstufe
|
||||||
|
|
||||||
|
Du hast Vollzugriff auf `G:\Gitea_Clone\homelab-infra`, Gitea-Push, Komodo, und SSH auf Unraid `Kallilabcore`.
|
||||||
|
|
||||||
|
## Lies zuerst
|
||||||
|
1. `CLAUDE.md`
|
||||||
|
2. `docs/AUDIT_2026-05-23.md` — dort steht die komplette Restliste
|
||||||
|
|
||||||
|
## Auftrag
|
||||||
|
Den Audit von oben verifizieren und die offenen Punkte abarbeiten, bis das Homelab in der Endstufe ist. Reihenfolge:
|
||||||
|
|
||||||
|
1. **P0** — Lokalen Commit `cd650b1` nach Gitea pushen, danach Komodo-Reaktion fuer `gitea` und `borg-ui` pruefen.
|
||||||
|
2. **P0** — Live-Daten aus Audit-Abschnitt 9 messen und in `docs/AUDIT_2026-05-23_LIVE.md` ablegen (Secrets redacten).
|
||||||
|
3. **P1** — Monitoring-Stack (`monitoring/`) live deployen, alte `ops/grafana-influxdb` und `ops/loki` `down` (nicht loeschen).
|
||||||
|
4. **P1** — Jellyfin und Plex in `HOMELAB_ARCHITECTURE_MASTER_V2.md`, `docs/SERVICE_CATALOG.md`, `docs/REPO_MAP.md` nachtragen. Plex-Eintrag "nicht als Repo-Stack enthalten" korrigieren.
|
||||||
|
5. **P2** — Borg-Lauf-Frische pruefen, ggf. neuen Lauf ausloesen, alle 14 Dump-Artefakte juenger als 24 h.
|
||||||
|
6. **P3** — Repo-Hygiene: 8 leere Verzeichnisse weg, `.serena/` in `.gitignore`, Entscheidung zu `ops/windows-reinstall/*.ps1`.
|
||||||
|
|
||||||
|
## Regeln (aus CLAUDE.md, nicht verhandelbar)
|
||||||
|
- Secrets nie im Klartext ausgeben.
|
||||||
|
- Keine Aenderungen direkt in Komodo, nur ueber Git → Push → Komodo.
|
||||||
|
- Kein `push --force`, kein blindes Loeschen von `/mnt/user/{appdata,documents,photos,services,backups}`.
|
||||||
|
- Working-Tree-Status nur aus `git status --short` ableiten, nie aus `git diff` ueber Linux-Mount.
|
||||||
|
- Traefik dynamic config wird nicht von Komodo deployed — Aenderungen dort manuell auf `/mnt/user/appdata/traefik/dynamic/` syncen.
|
||||||
|
- Nicht anfassen: Hermes, Disk1 NTFS Phase 2, Komodo-Auth, Grafana/InfluxDB `user: "0"`, Image-Pinning ddns/glances/scrutiny.
|
||||||
|
- Wenn zwei Reparaturversuche scheitern: stoppen, Drift-Runbook Pflichtmatrix, Operator fragen.
|
||||||
|
|
||||||
|
## Arbeitsmodus pro Block
|
||||||
|
Lesen → minimal aendern → `ops/policy-checks/check_repo.ps1` lokal → Commit → Push → Komodo-Reaktion + Smoke-Test → eine Zeile in `docs/MIGRATION_LOG.md`.
|
||||||
|
|
||||||
|
## Fertig
|
||||||
|
Wenn alles abgearbeitet ist (oder ein Punkt bewusst offen bleibt): `docs/AUDIT_2026-05-23_FINAL.md` schreiben mit Ampel + konkretem Beleg pro Punkt, committen, pushen, kurz an mich melden.
|
||||||
+37
-27
@@ -8,6 +8,9 @@ Verwandte Dokumente:
|
|||||||
|
|
||||||
- `docs/ROLLBACK.md` - Rueckweg bei Fehlern im laufenden GitOps-Betrieb
|
- `docs/ROLLBACK.md` - Rueckweg bei Fehlern im laufenden GitOps-Betrieb
|
||||||
- `docs/RESTORE_MATRIX.md` - Restore-Quellen und Verifikationsregeln pro Dienst
|
- `docs/RESTORE_MATRIX.md` - Restore-Quellen und Verifikationsregeln pro Dienst
|
||||||
|
- `docs/RESTORE_HANDBOOK.md` - praktische Restore-Betriebsanleitung
|
||||||
|
- `docs/SERVICES_RECOVERY.md` - Recovery-kritische `/mnt/user/services`-Pfade, Gitea-Mirror und Komodo-Bootstrap
|
||||||
|
- `docs/EXTERNAL_DEPENDENCIES.md` - externe Provider/Konten und Ausfall-Szenarien
|
||||||
- `ops/borg-ui/BACKUP_SCOPE.md` - Zielbild des Borg-Scopes
|
- `ops/borg-ui/BACKUP_SCOPE.md` - Zielbild des Borg-Scopes
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -59,12 +62,14 @@ Diese Punkte sollten **vor** einem echten Ausfall geklaert sein:
|
|||||||
|
|
||||||
| Thema | Sollzustand |
|
| Thema | Sollzustand |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Repo-Zugang ausserhalb von Gitea | externer Mirror oder lokaler aktueller Clone vorhanden |
|
| Repo-Zugang ausserhalb von Gitea | privater GitHub-Push-Mirror `michaelkaleschke-spec/homelab-infra` und lokaler aktueller Clone vorhanden |
|
||||||
| Unraid USB-/Flash-Backup | eingerichtet und wiederherstellbar |
|
| Unraid USB-/Flash-Backup | `unraid-flash-config.tar.gz` wird vor Borg unter `/mnt/user/backups/borg/dumps/latest` erzeugt und nach Hetzner/Borg gesichert; Unraid-Connect-Cloud-Backup optional zusaetzlich |
|
||||||
| Borg-Ziel | nicht nur lokal auf demselben Ausfallpfad |
|
| Borg-Ziel | nicht nur lokal auf demselben Ausfallpfad |
|
||||||
| Borg-Passphrase | extern sicher hinterlegt |
|
| Borg-Passphrase | Host-Secret-Datei vorhanden und fuer Borg-Zugriff verifiziert; externe analoge Hinterlegung bleibt Operator-Aufgabe |
|
||||||
| Secrets-Dateien | ueber Borg bzw. Restore-Quellen abgedeckt |
|
| Secrets-Dateien | ueber Borg bzw. Restore-Quellen abgedeckt |
|
||||||
| Komodo Stack ENV-Werte | extern dokumentiert, z. B. Vaultwarden |
|
| Komodo Stack ENV-Werte | extern dokumentiert, z. B. Vaultwarden |
|
||||||
|
| Services-Recovery | `docs/SERVICES_RECOVERY.md` gepflegt, insbesondere Gitea-Repo-Mirror und Komodo-Bootstrap |
|
||||||
|
| Hardware-/Netzwerkdaten | `docs/HARDWARE_INVENTORY.md` und `docs/NETWORK_INVENTORY.md` mit echten Werten gefuellt |
|
||||||
| Restore-Smoke-Tests | fuer mindestens 1-2 kritische Dienste nachgewiesen |
|
| Restore-Smoke-Tests | fuer mindestens 1-2 kritische Dienste nachgewiesen |
|
||||||
|
|
||||||
**Wichtig:** Dieses Dokument ist nur so gut wie die Vorbereitung ausserhalb des Repos.
|
**Wichtig:** Dieses Dokument ist nur so gut wie die Vorbereitung ausserhalb des Repos.
|
||||||
@@ -80,13 +85,13 @@ Deshalb gilt:
|
|||||||
1. Wenn moeglich, Repo ueber einen externen Mirror oder einen lokalen aktuellen Clone holen.
|
1. Wenn moeglich, Repo ueber einen externen Mirror oder einen lokalen aktuellen Clone holen.
|
||||||
2. Nur wenn `gitea` bereits wieder verfuegbar ist, direkt aus `git.kaleschke.info` klonen.
|
2. Nur wenn `gitea` bereits wieder verfuegbar ist, direkt aus `git.kaleschke.info` klonen.
|
||||||
|
|
||||||
Empfohlene Wege:
|
Verfuegbare Wege:
|
||||||
|
|
||||||
- externer Push-Mirror
|
- externer Push-Mirror: `https://github.com/michaelkaleschke-spec/homelab-infra`
|
||||||
- lokaler Bare-Clone auf dem PC
|
- lokaler Bare-Clone auf dem PC
|
||||||
- normaler lokaler Arbeits-Clone auf dem PC
|
- normaler lokaler Arbeits-Clone auf dem PC
|
||||||
|
|
||||||
Wenn **kein externer Repo-Zugang** besteht, ist `services/gitea/data` selbst ein kritischer Restore-Pfad.
|
Wenn **weder GitHub-Mirror noch lokaler Repo-Clone** verfuegbar sind, ist `services/gitea/data` selbst ein kritischer Restore-Pfad.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -99,6 +104,8 @@ Wenn **kein externer Repo-Zugang** besteht, ist `services/gitea/data` selbst ein
|
|||||||
3. Array wieder zuweisen und starten
|
3. Array wieder zuweisen und starten
|
||||||
4. Grundlegende Shares pruefen
|
4. Grundlegende Shares pruefen
|
||||||
|
|
||||||
|
Primaere lokale/off-site Restore-Quelle fuer die bestehende Flash-Konfiguration ist das Borg-Artefakt `unraid-flash-config.tar.gz` aus `/mnt/user/backups/borg/dumps/latest`. Dieses Archiv enthaelt `/boot/config` und muss wie Secret-Material behandelt werden.
|
||||||
|
|
||||||
### 5.2 Erwartete Shares / Pfade
|
### 5.2 Erwartete Shares / Pfade
|
||||||
|
|
||||||
Mindestens diese Pfade muessen wieder verfuegbar sein:
|
Mindestens diese Pfade muessen wieder verfuegbar sein:
|
||||||
@@ -139,6 +146,7 @@ Erwartete Basis unter `/mnt/user/appdata/secrets/`:
|
|||||||
- `nextcloud_postgres_password.txt`
|
- `nextcloud_postgres_password.txt`
|
||||||
- `postgres_password.txt`
|
- `postgres_password.txt`
|
||||||
- `redis_password.txt`
|
- `redis_password.txt`
|
||||||
|
- `borg_repo_passphrase.txt`
|
||||||
- `vaultwarden_admin_token.txt`
|
- `vaultwarden_admin_token.txt`
|
||||||
- `hermes_runner_id_ed25519`
|
- `hermes_runner_id_ed25519`
|
||||||
|
|
||||||
@@ -163,7 +171,6 @@ Diese Werte sind vor dem Start der betroffenen Dienste zu pruefen bzw. wieder in
|
|||||||
- `KOMODO_JWT_SECRET`
|
- `KOMODO_JWT_SECRET`
|
||||||
- `KOMODO_MONGO_PASSWORD`
|
- `KOMODO_MONGO_PASSWORD`
|
||||||
- `KOMODO_PERIPHERY_PASSKEY`
|
- `KOMODO_PERIPHERY_PASSKEY`
|
||||||
- relevante `HOMEPAGE_VAR_*`
|
|
||||||
- `APP_KEY` und `ADMIN_PASSWORD` fuer `speedtest-tracker`
|
- `APP_KEY` und `ADMIN_PASSWORD` fuer `speedtest-tracker`
|
||||||
|
|
||||||
### 6.3 Rechte
|
### 6.3 Rechte
|
||||||
@@ -198,11 +205,16 @@ Besonders kritisch:
|
|||||||
|
|
||||||
- `/mnt/user/appdata/secrets`
|
- `/mnt/user/appdata/secrets`
|
||||||
- `/mnt/user/appdata/traefik`
|
- `/mnt/user/appdata/traefik`
|
||||||
|
- `/mnt/user/services/homelab-infra`
|
||||||
|
- `/mnt/user/services/stacks`
|
||||||
|
- `/mnt/user/services/posture-check`
|
||||||
|
- Details zu `/mnt/user/services/` und Komodo/Gitea-Bootstrap stehen in `docs/SERVICES_RECOVERY.md`
|
||||||
- `/mnt/user/services/gitea/data`
|
- `/mnt/user/services/gitea/data`
|
||||||
- `/mnt/user/appdata/authelia/config`
|
- `/mnt/user/appdata/authelia/config`
|
||||||
- `/mnt/user/appdata/komodo/core`
|
- `/mnt/user/appdata/komodo/core`
|
||||||
- `/mnt/user/appdata/komodo/periphery`
|
- `/mnt/user/appdata/komodo/periphery`
|
||||||
- `/mnt/user/backups/borg/dumps/latest`
|
- `/mnt/user/backups/borg/dumps/latest`
|
||||||
|
- `/mnt/user/backups/borg/dumps/latest/unraid-flash-config.tar.gz`
|
||||||
- dienstspezifische App- und Nutzdatenpfade
|
- dienstspezifische App- und Nutzdatenpfade
|
||||||
|
|
||||||
**Nicht blind alles extrahieren**, wenn nur einzelne Pfade oder Dienste betroffen sind.
|
**Nicht blind alles extrahieren**, wenn nur einzelne Pfade oder Dienste betroffen sind.
|
||||||
@@ -261,20 +273,18 @@ Ziel:
|
|||||||
|
|
||||||
### Stufe 5 - Restliche Apps und Ops
|
### Stufe 5 - Restliche Apps und Ops
|
||||||
|
|
||||||
15. `apps/homepage/`
|
15. `apps/ntfy/`
|
||||||
16. `apps/ntfy/`
|
16. `apps/paperless-gpt/`
|
||||||
17. `apps/paperless-gpt/`
|
17. `apps/bentopdf/`
|
||||||
18. `apps/bentopdf/`
|
18. `ops/glance/`
|
||||||
19. `ops/uptime-kuma/`
|
19. `ops/borg-ui/`
|
||||||
20. `ops/borg-ui/`
|
20. `ops/filebrowser/`
|
||||||
21. `ops/backrest/`
|
21. `ops/glances/`
|
||||||
22. `ops/filebrowser/`
|
22. `ops/scrutiny/`
|
||||||
23. `ops/glances/`
|
23. `ops/speedtest/`
|
||||||
24. `ops/scrutiny/`
|
24. `monitoring/`
|
||||||
25. `ops/speedtest/`
|
25. `ops/hermes-agent/`
|
||||||
26. `ops/grafana-influxdb/`
|
26. `infra/ddns-updater/`
|
||||||
27. `ops/hermes-agent/`
|
|
||||||
28. `infra/ddns-updater/`
|
|
||||||
|
|
||||||
**Regel:** Nach jeder Stufe kurz pruefen, bevor die naechste beginnt.
|
**Regel:** Nach jeder Stufe kurz pruefen, bevor die naechste beginnt.
|
||||||
|
|
||||||
@@ -309,7 +319,7 @@ Ziel:
|
|||||||
|
|
||||||
- Borg UI startet und kennt sein Repo noch
|
- Borg UI startet und kennt sein Repo noch
|
||||||
- aktuelle Dump-Artefakte sind vorhanden
|
- aktuelle Dump-Artefakte sind vorhanden
|
||||||
- Uptime Kuma / Homepage / ntfy sind wieder da
|
- Glance / Monitoring / ntfy sind wieder da
|
||||||
- Hermes Gateway und Dashboard starten; `hermes.kaleschke.info` leitet anonym zu Authelia weiter
|
- Hermes Gateway und Dashboard starten; `hermes.kaleschke.info` leitet anonym zu Authelia weiter
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -366,6 +376,7 @@ Relevant:
|
|||||||
|
|
||||||
- Dump-Ziel: `/mnt/user/backups/borg/dumps/latest`
|
- Dump-Ziel: `/mnt/user/backups/borg/dumps/latest`
|
||||||
- Skript: `ops/borg-ui/scripts/pre-backup-dumps.sh`
|
- Skript: `ops/borg-ui/scripts/pre-backup-dumps.sh`
|
||||||
|
- Unraid-Flash-Artefakt: `unraid-flash-config.tar.gz` plus `.sha256` und Manifest im selben Zielpfad
|
||||||
|
|
||||||
### Hermes Agent
|
### Hermes Agent
|
||||||
|
|
||||||
@@ -382,17 +393,16 @@ Smoke-Test: `hermes-gateway` healthcheck ist gruen, `hermes.kaleschke.info` leit
|
|||||||
|
|
||||||
### Gitea
|
### Gitea
|
||||||
|
|
||||||
Wenn weder externer Mirror noch lokaler Clone verfuegbar sind, ist `services/gitea/data` selbst Teil des kritischen Wiederanlaufs.
|
`Micha/homelab-infra` wird als privater GitHub-Push-Mirror gespiegelt. Dieser Mirror ist der bevorzugte Repo-Bootstrap, falls Gitea selbst nach einem Ausfall noch nicht laeuft. Wenn weder GitHub-Mirror noch lokaler Clone verfuegbar sind, ist `services/gitea/data` selbst Teil des kritischen Wiederanlaufs.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Offene Vorbereitungs-To-dos
|
## 11. Offene Vorbereitungs-To-dos
|
||||||
|
|
||||||
- externer Repo-Zugang fuer den Ernstfall absichern
|
- Unraid-USB-/Flash-Backup regelmaessig ueber `unraid-flash-config.tar.gz` und optional Unraid Connect pruefen
|
||||||
- Unraid-USB-/Flash-Backup pruefen
|
- Borg-Passphrase aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` extern analog sicher hinterlegen
|
||||||
- Borg-Passphrase extern sicher hinterlegen
|
|
||||||
- Komodo Stack-ENV-Werte zentral ausserhalb von Komodo dokumentieren
|
- Komodo Stack-ENV-Werte zentral ausserhalb von Komodo dokumentieren
|
||||||
- Restore-Smoke-Test fuer mindestens einen weiteren kritischen Dienst dokumentieren
|
- regelmaessige automatisierte Restore-Smoke-Tests fuer Vaultwarden, Gitea und Paperless etablieren
|
||||||
- `komodo-mongo`-Dump nach Major-Upgrades gezielt kontrollieren
|
- `komodo-mongo`-Dump nach Major-Upgrades gezielt kontrollieren
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Disk1 Phase 2 - NTFS to XFS Migration
|
||||||
|
|
||||||
|
Stand: 2026-05-25 06:15 CEST. Ziel erreicht: `/mnt/disk1` wurde von `ntfs3` auf XFS migriert, ohne produktive Compose-Pfade zu aendern. Container nutzen weiter `/mnt/user/...`.
|
||||||
|
|
||||||
|
## Preflight
|
||||||
|
|
||||||
|
| Check | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| Disk1 Mount | `/dev/md1p1` auf `/mnt/disk1`, `ntfs3`, 5.5T, 1.7T genutzt, 3.8T frei |
|
||||||
|
| Cache Mount | `/dev/nvme0n1p1` auf `/mnt/cache`, `xfs`, 1.9T, 100G genutzt |
|
||||||
|
| H: Backup-Ziel | `H:\`, Label `Externe HDD`, NTFS, 8T, 5.96T frei, healthy |
|
||||||
|
| Compose-Binds | Keine Treffer fuer direkte `/mnt/disk1`, `/mnt/cache`, `/mnt/disks`, `/mnt/remotes` Binds |
|
||||||
|
| SMB-Zugriff | `\\Kallilabcore\services`, `documents`, `photos`, `media` erreichbar |
|
||||||
|
|
||||||
|
## Zu sichernde Shares
|
||||||
|
|
||||||
|
| Share | Groesse laut Preflight |
|
||||||
|
|---|---:|
|
||||||
|
| `services` | 451M |
|
||||||
|
| `documents` | 196M |
|
||||||
|
| `photos` | 23G |
|
||||||
|
| `backups` | 2.2G |
|
||||||
|
| `media` | 1.7T |
|
||||||
|
| `finance` | 0 |
|
||||||
|
| `projekte` | 92K |
|
||||||
|
|
||||||
|
Zusaetzliche Disk1-Top-Level-Pfade: `scripts` 3.3M, `isos` 0, `System Volume Information` 0.
|
||||||
|
|
||||||
|
## Backup-Strategie
|
||||||
|
|
||||||
|
- Zielroot: `H:\kallilab-recovery\disk1-phase2-2026-05-23`.
|
||||||
|
- Kritischer Linux-/GitOps-Pfad `services` wird zusaetzlich als Tar-Archiv ueber SSH gesichert, damit Linux-Metadaten erhalten bleiben.
|
||||||
|
- User-Shares werden per SMB/Robocopy kopiert, mit Logs und anschliessender Zaehler-/Groessenverifikation.
|
||||||
|
- Keine produktiven Datenpfade werden geloescht.
|
||||||
|
|
||||||
|
## Gates
|
||||||
|
|
||||||
|
1. Backup komplett und verifiziert.
|
||||||
|
2. Dienste-Freeze vorbereitet und letzte Dumps frisch.
|
||||||
|
3. Direkt vor Format/Array-Prozedur: Operator-Bestaetigung im konkreten Moment.
|
||||||
|
|
||||||
|
## Backup-Ergebnis
|
||||||
|
|
||||||
|
Stand: 2026-05-24 13:07 CEST.
|
||||||
|
|
||||||
|
| Bereich | Sicherungsart | Ergebnis |
|
||||||
|
|---|---|---|
|
||||||
|
| `media` | Robocopy/SMB nach `H:\kallilab-recovery\disk1-phase2-2026-05-23\shares\media` | 2722 Dateien, 1677.11 GiB, Manifestvergleich: 0 missing, 0 size mismatch, 0 extra |
|
||||||
|
| `services` | Host-Tar auf Unraid Cache, danach binaer per `scp` nach H: | `services.tar`, 0.441 GiB, `tar -tf` gueltig |
|
||||||
|
| `documents` | Host-Tar auf Unraid Cache, danach binaer per `scp` nach H: | `documents.tar`, 0.192 GiB, `tar -tf` gueltig |
|
||||||
|
| `photos` | Host-Tar auf Unraid Cache, danach binaer per `scp` nach H: | `photos.tar`, 22.876 GiB, `tar -tf` gueltig |
|
||||||
|
| `backups` | Host-Tar auf Unraid Cache, danach binaer per `scp` nach H: | `backups.tar`, 2.099 GiB, `tar -tf` gueltig |
|
||||||
|
| `finance` | Host-Tar auf Unraid Cache, danach binaer per `scp` nach H: | `finance.tar`, leerer Share, `tar -tf` gueltig |
|
||||||
|
| `projekte` | Host-Tar auf Unraid Cache, danach binaer per `scp` nach H: | `projekte.tar`, klein, `tar -tf` gueltig |
|
||||||
|
| Disk1-Extras `scripts`, `isos` | Host-Tar auf Unraid Cache, danach binaer per `scp` nach H: | `disk1-extra.tar`, 0.003 GiB, `tar -tf` gueltig |
|
||||||
|
|
||||||
|
Hinweis: Erste Tar-Versuche per PowerShell-Redirect wurden verworfen, weil PowerShell den binaeren SSH-Stream als UTF-16 geschrieben hatte. Die ungueltige `media.tar` und unvollstaendige SMB-Teilkopien fuer `services`/`documents` wurden vom Backup-Ziel entfernt, damit nur verwertbare Sicherungen uebrig bleiben.
|
||||||
|
|
||||||
|
## Abschluss
|
||||||
|
|
||||||
|
Stand: 2026-05-25 06:15 CEST.
|
||||||
|
|
||||||
|
| Check | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| Freeze-Dumps | `pre-backup-dumps.sh` vor Format ausgefuehrt; nach Wiederanlauf erneut erfolgreich, 15 kanonische Dump-Artefakte juenger als 24 h |
|
||||||
|
| Disk1 Filesystem | `/dev/md1p1` auf `/mnt/disk1`, `xfs`, 5.5T, 1.8T genutzt, 3.7T frei |
|
||||||
|
| Restore `media` | 2722 Dateien, 1,800,782,188,226 Bytes; finaler Manifestvergleich: 0 missing, 0 size mismatch, 0 extra |
|
||||||
|
| Restore Tar-Shares | `services`, `documents`, `photos`, `backups`, `finance`, `projekte` und Disk1-Extras aus H:-Freeze-Archiven nach `/mnt/disk1` extrahiert |
|
||||||
|
| Docker/Services | 49 Container laufend, 0 stopped, 0 unhealthy, 0 starting |
|
||||||
|
| Smoke-Tests | `git.kaleschke.info` 200, `komodo.kaleschke.info` 200, `borg.kaleschke.info` 302, `jellyfin.kaleschke.info` 302, `monitoring.kaleschke.info` 302 |
|
||||||
|
| Service-Mounts | Gitea SSH `:222` offen; Jellyfin und Plex sehen `/media`; Prometheus readiness ok |
|
||||||
|
| Backup-Lauf | Borg-UI Repository `appdata-critical`, letzter Job `completed`, Archiv `Taegliche-Sicherung-2026-05-25T05:52:44.157`, `nfiles=100221` |
|
||||||
|
| Temp-Cleanup | `/mnt/cache/disk1-phase2-tmp/*.tar` nach H:-Verifikation geloescht; Cache weiter XFS mit ca. 1.7T frei |
|
||||||
|
|
||||||
|
Hinweis zum Docker-Wiederanlauf: Nach dem manuellen Docker-Start liefen die Container, aber Healthcheck-Execs scheiterten wegen `dockerd` mit `XDG_RUNTIME_DIR=/run/user/0`. Ein gezielter Docker-Neustart mit unsetztem `XDG_RUNTIME_DIR` behob den Runtime-Fehler; danach wurden alle Healthchecks gruen. `monitoring-prometheus` war durch den geplanten Docker-Stop sauber beendet und wurde als bestehender Container wieder gestartet.
|
||||||
|
|
||||||
|
Offener Nachlauf: Die Array-Parity-Anzeige zeigte nach Abschluss weiter `mdNumDisabled=1`, `mdNumInvalid=1` und `mdResyncAction=check P`, waehrend beide beteiligten Devices `rdevStatus=DISK_OK` und `rdevNumErrors=0` melden. Parity-Zustand separat in Unraid pruefen und keinen Parity-/Disk-Slot ohne Operator-Entscheid aendern.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# External Dependencies - KalliLab CORE
|
||||||
|
|
||||||
|
Status: Initiale Betreiber-Baseline 2026-05-26; konkrete Account-Recovery-Codes und Besitznachweise muessen ausserhalb des Repos bestaetigt werden.
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt externe Anbieter und Konten, von denen Betrieb, Recovery oder Zugriff abhaengen. Ziel ist, im Ausfallfall nicht erst suchen zu muessen, welcher Provider welches Teilproblem verursacht.
|
||||||
|
|
||||||
|
## Abhaengigkeiten
|
||||||
|
|
||||||
|
| Anbieter / System | Zweck | Kritikalitaet | Recovery-Auswirkung | Zugang / Besitz | Notfallplan |
|
||||||
|
|---|---|---:|---|---|---|
|
||||||
|
| Domain-Registrar | Besitz `kaleschke.info` | hoch | Ohne Domain brechen Public URLs/TLS-Erneuerung | Operator-Konto ausserhalb Repo, konkreten Registrar im Account pruefen | Registrar-Zugang, 2FA-Recovery und Zahlungsweg analog/off-system sichern |
|
||||||
|
| Cloudflare DNS | Authoritative DNS, ACME DNS-Challenge, DDNS | hoch | Neue Zertifikate/DNS-Aenderungen blockiert | Cloudflare-Konto; API-Token liegt als Host-Secret | API-Token rotierbar halten, Account-Recovery und Zone-Besitz pruefen |
|
||||||
|
| Hetzner Storage Box | Off-site Borg Backup | kritisch | Restore aus Off-site ggf. nicht moeglich | Hetzner-Konto / Storage-Box-Zugang ausserhalb Repo | Zweites Off-site-Ziel oder Cold-Platte etablieren; Borg-Passphrase extern sichern |
|
||||||
|
| GitHub Mirror | Externer Repo-Mirror `michaelkaleschke-spec/homelab-infra` | mittel/hoch | Gitea-Verlust abfederbar, Repo-Bootstrap bleibt moeglich | GitHub-Konto; PAT liegt in Gitea-Mirror-Settings, nicht im Repo | Mirror-Status regelmaessig pruefen; lokalen Clone als zweite Kopie behalten |
|
||||||
|
| Tailscale | Remote-/Operator-Zugang | hoch | Remote-Zugriff erschwert, lokale Bedienung bleibt | Tailnet-Konto; Node `Kallilabcore`, IPv4 `100.80.98.33` | Break-glass per LAN und physischem Zugriff; Tailnet-Recovery-Codes sichern |
|
||||||
|
| GMX SMTP | Authelia Notifier | mittel | Mail-Notifier faellt aus, Login selbst nicht zwingend | GMX-Konto; SMTP-Secret liegt hostseitig | ntfy/zweiter SMTP als Fallback pruefen |
|
||||||
|
| Let's Encrypt | TLS-Zertifikate | hoch | Cert-Erneuerung faellt aus | automatisch via Traefik und Cloudflare DNS-Challenge | Cert-Expiry Alert einrichten; Cloudflare-Token und Traefik-Storage pruefen |
|
||||||
|
| Container Registries | Image Pulls von Docker Hub, GHCR, LSCR, Gitea Registry u. a. | mittel | Redeploy/Update blockiert | ueberwiegend oeffentlich; keine produktiven Registry-Tokens im Repo | Gepinnte Digests und lokale Runtime helfen kurzfristig; Updates geplant und einzeln deployen |
|
||||||
|
| Plex Konto/Remote Access | Plex native Auth, ggf. Remote Access und Claim | mittel | Plex-Clients/Remote-Funktionen koennen ausfallen | Plex-Konto ausserhalb Repo; `PLEX_CLAIM` nur fuer Setup | LAN-Medienpfade bleiben lokal; Konto-Recovery separat sichern |
|
||||||
|
| Mobile Push | ntfy und ggf. mobile Plattform-Pushes | niedrig/mittel | Alerts erreichen Mobilgeraete ggf. nicht | App-/Device-seitig | Kritische Alerts zusaetzlich in Grafana/Glance sichtbar halten |
|
||||||
|
|
||||||
|
## Kritische Secrets ausserhalb des Repos
|
||||||
|
|
||||||
|
Authoritativ ist `docs/SECRETS_MAP.md`. Diese Liste markiert nur externe Abhaengigkeiten.
|
||||||
|
|
||||||
|
| Secret | Zweck | Recovery-Hinweis |
|
||||||
|
|---|---|---|
|
||||||
|
| Borg Passphrase | Entschluesselung Borg-Repos | Muss analog/off-system vorhanden sein |
|
||||||
|
| Cloudflare DNS API Token | ACME DNS-Challenge | Token-Rotation und Scope pruefen |
|
||||||
|
| GitHub Mirror Token | Push-Mirror | In Gitea/GitHub verwaltet, nicht im Repo |
|
||||||
|
| Tailscale Account Recovery | Tailnet-Zugang | Account-2FA/Recovery Codes sichern |
|
||||||
|
| SMTP Passwort | Authelia Mail | In Host-Secret, Fallback pruefen |
|
||||||
|
| Domain-Registrar Recovery | Domain-Besitz und Zahlung | Account, 2FA und Zahlungsweg ausserhalb des Homelabs sichern |
|
||||||
|
| Hetzner Storage Box Zugang | Off-site Backup-Ziel | SSH-/Web-Zugang und Zahlungsweg extern sichern |
|
||||||
|
|
||||||
|
## Ausfall-Szenarien
|
||||||
|
|
||||||
|
### Hetzner Storage Box nicht erreichbar
|
||||||
|
|
||||||
|
- Lokales Borg-Repo und aktuelle Dumps pruefen.
|
||||||
|
- Keine destruktiven Host-Aenderungen starten, solange Off-site unklar ist.
|
||||||
|
- Zweites Off-site-Ziel oder Cold-Platte als Folgeaufgabe umsetzen.
|
||||||
|
|
||||||
|
### Cloudflare Account/DNS gestoert
|
||||||
|
|
||||||
|
- Bestehende Zertifikate laufen bis Ablauf weiter.
|
||||||
|
- Keine Domain-/ACME-Aenderungen moeglich.
|
||||||
|
- Tailscale/LAN-Zugang als Break-glass nutzen.
|
||||||
|
|
||||||
|
### Tailscale gestoert
|
||||||
|
|
||||||
|
- Lokalen LAN-Zugang nutzen.
|
||||||
|
- Direkte Admin-Ports nur gemaess dokumentierten Ausnahmen verwenden.
|
||||||
|
- AdGuard-Admin-Bind muss so geplant werden, dass ein lokaler Break-glass-Weg bekannt ist.
|
||||||
|
- Seit 2026-05-26 ist AdGuard Admin nur ueber `100.80.98.33:8082` gebunden; bei Tailnet-Ausfall ist lokaler Host-/Compose-Zugriff der Break-glass-Weg.
|
||||||
|
|
||||||
|
### Domain verloren oder Registrar-Zugriff verloren
|
||||||
|
|
||||||
|
- Gitea/GitHub Mirror und lokale IP/Tailscale-Pfade fuer Recovery nutzen.
|
||||||
|
- Neue Domain waere separater Migrationsfall fuer Traefik, Authelia, App-URLs und Clients.
|
||||||
|
|
||||||
|
## Review
|
||||||
|
|
||||||
|
| Datum | Ergebnis | Naechste Aktion |
|
||||||
|
|---|---|---|
|
||||||
|
| 2026-05-26 | Bekannte externe Abhaengigkeiten aus Repo-/Betriebsdoku dokumentiert; keine Secret-Werte aufgenommen | Account-Besitz, 2FA-Recovery-Codes, Zahlungswege und Borg-Passphrase extern bestaetigen |
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Family Onboarding - KalliLab CORE
|
||||||
|
|
||||||
|
Status: Entwurf. Zielgruppe sind Familienmitglieder, nicht Operatoren.
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
|
||||||
|
Diese Datei soll spaeter kurz und alltagstauglich erklaeren, wie die wichtigsten Dienste genutzt werden und was bei Problemen zu tun ist. Keine Restore-Matrix, keine Docker-Begriffe.
|
||||||
|
|
||||||
|
## Dienste
|
||||||
|
|
||||||
|
| Dienst | URL | Zweck | Konto / Login | Notiz |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Nextcloud | `https://cloud.kaleschke.info` | Dateien, Kalender, Kontakte | TBD | Mobile App/WebDAV/CardDAV |
|
||||||
|
| Immich | `https://immich.kaleschke.info` | Fotos und Smartphone-Backup | TBD | Backup-App pro Handy |
|
||||||
|
| Vaultwarden | `https://vault.kaleschke.info` | Passwoerter | TBD | Familien-Organisation pruefen |
|
||||||
|
| Mealie | `https://mealie.kaleschke.info` | Rezepte und Einkauf | TBD | TBD |
|
||||||
|
| Paperless | `https://paperless.kaleschke.info` | Dokumente | TBD | Scan-/Inbox-Prozess beschreiben |
|
||||||
|
| Plex | intern/App | Medien | TBD | TBD |
|
||||||
|
|
||||||
|
## Was tun bei Problemen?
|
||||||
|
|
||||||
|
| Situation | Verhalten |
|
||||||
|
|---|---|
|
||||||
|
| Webseite nicht erreichbar | 10 Minuten warten, dann Operator informieren |
|
||||||
|
| Passwort vergessen | Operator informieren, nicht selbst neue Konten anlegen |
|
||||||
|
| Handy-Foto-Backup stoppt | App oeffnen, WLAN/Batteriesparmodus pruefen, Operator informieren |
|
||||||
|
| 2FA verloren | Operator informieren; Recovery-Prozess wird separat festgelegt |
|
||||||
|
| Warnmeldung vom Browser | Nicht weiterklicken, Screenshot machen, Operator informieren |
|
||||||
|
|
||||||
|
## Offene Inhalte
|
||||||
|
|
||||||
|
| Status | Aufgabe |
|
||||||
|
|---|---|
|
||||||
|
| offen | Pro Dienst kurze Schritt-fuer-Schritt-Anleitung schreiben |
|
||||||
|
| offen | Konto-/2FA-Policy final entscheiden |
|
||||||
|
| offen | Immich Mobile Backup fuer alle Geraete testen |
|
||||||
|
| offen | Vaultwarden Familienorganisation pruefen |
|
||||||
|
|
||||||
@@ -90,18 +90,18 @@ Sollzustand:
|
|||||||
Soll fuer Home Assistant:
|
Soll fuer Home Assistant:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /mnt/user/services/stacks/grafana
|
cd /mnt/user/services/stacks/monitoring
|
||||||
git fetch --all --prune
|
git fetch --all --prune
|
||||||
git reset --hard origin/master
|
git reset --hard origin/master
|
||||||
docker compose --env-file .env -p grafana -f ops/grafana-influxdb/docker-compose.yml up -d --force-recreate --no-deps influxdb3-core
|
docker compose --env-file .env -p monitoring -f monitoring/docker-compose.yml up -d --force-recreate --no-deps influxdb3-core
|
||||||
```
|
```
|
||||||
|
|
||||||
Danach pruefen:
|
Danach pruefen:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker network ls | grep -E "grafana|influx"
|
docker network ls | grep -E "monitoring|influx"
|
||||||
docker inspect influxdb3-core --format '{{json .NetworkSettings.Networks}}'
|
docker inspect monitoring-influxdb3-core --format '{{json .NetworkSettings.Networks}}'
|
||||||
docker inspect influxdb3-core --format '{{json .NetworkSettings.Ports}}'
|
docker inspect monitoring-influxdb3-core --format '{{json .NetworkSettings.Ports}}'
|
||||||
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep influx
|
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep influx
|
||||||
ss -ltnp | grep 8181
|
ss -ltnp | grep 8181
|
||||||
curl -i --max-time 5 http://192.168.178.58:8181/
|
curl -i --max-time 5 http://192.168.178.58:8181/
|
||||||
@@ -110,12 +110,12 @@ curl -i --max-time 5 http://192.168.178.58:8181/
|
|||||||
Erwartung:
|
Erwartung:
|
||||||
|
|
||||||
- Komodo Workspace `HEAD` entspricht `origin/master`.
|
- Komodo Workspace `HEAD` entspricht `origin/master`.
|
||||||
- `influxdb3-core` haengt an `grafana_grafana_influx_internal` und `grafana_grafana_influx_lan`.
|
- `monitoring-influxdb3-core` haengt an `monitoring_monitoring_net` und `monitoring_monitoring_influx_lan`.
|
||||||
- Docker zeigt `192.168.178.58:8181->8181/tcp`.
|
- Docker zeigt `192.168.178.58:8181->8181/tcp`.
|
||||||
- `ss` zeigt `docker-proxy` auf `192.168.178.58:8181`.
|
- `ss` zeigt `docker-proxy` auf `192.168.178.58:8181`.
|
||||||
- `curl` bekommt `401 Unauthorized` von InfluxDB.
|
- `curl` bekommt `401 Unauthorized` von InfluxDB.
|
||||||
|
|
||||||
Hinweis: Im Compose-File heissen die Netze `grafana_influx_internal` und `grafana_influx_lan`. Durch den Compose-Projektnamen `grafana` werden daraus zur Laufzeit die Docker-Netze `grafana_grafana_influx_internal` und `grafana_grafana_influx_lan`.
|
Hinweis: Im Compose-File heissen die Netze `monitoring_net` und `monitoring_influx_lan`. Durch den Compose-Projektnamen `monitoring` koennen daraus zur Laufzeit Docker-Netze mit Projektpraefix werden.
|
||||||
|
|
||||||
## Stop-Regel
|
## Stop-Regel
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# Hardware Inventory - KalliLab CORE
|
||||||
|
|
||||||
|
Status: Hardware-Baseline erfasst; USV/Power-Loss bleibt offene Betreiberentscheidung.
|
||||||
|
Host: `Kallilabcore`
|
||||||
|
Letzte Pruefung: 2026-05-26
|
||||||
|
Naechster Review: 2026-08-26
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt die physische Basis des Homelabs. Es ist die Grundlage fuer Capacity Planning, Restore-Zeit, Ersatzteilplanung, USV-Verhalten und Entscheidungen wie Immich-ML, Plex-Transcoding oder Storage-Erweiterung.
|
||||||
|
|
||||||
|
## Host
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Hostname | Kallilabcore |
|
||||||
|
| Standort | Heim-LAN, physischer Standort TBD |
|
||||||
|
| Betriebssystem | Unraid |
|
||||||
|
| Unraid-Version | 7.2.4 |
|
||||||
|
| Rolle | Single-Host Homelab, Docker Compose via Komodo |
|
||||||
|
| Boot-Medium | Samsung Flash Drive, 59.8G, FAT32 |
|
||||||
|
| Flash-Backup | In Borg-Scope aufgenommen, siehe `docs/MIGRATION_LOG.md` |
|
||||||
|
|
||||||
|
## CPU
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Modell | 12th Gen Intel(R) Core(TM) i5-12400F |
|
||||||
|
| Kerne / Threads | 6 Kerne / 12 Threads |
|
||||||
|
| Architektur | x86_64 |
|
||||||
|
| Relevante Flags | AVX, AVX2, FMA, AES, VT-x vorhanden; kein AVX-512 |
|
||||||
|
| iGPU / Quick Sync | Nein, `F`-CPU ohne iGPU |
|
||||||
|
|
||||||
|
Pruefkommando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat /proc/cpuinfo | awk '/model name|flags/ {print; if(/flags/) exit}'
|
||||||
|
lscpu
|
||||||
|
```
|
||||||
|
|
||||||
|
## RAM
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Gesamt | 31 GiB |
|
||||||
|
| Belegt im Normalbetrieb | ca. 7.9 GiB genutzt, ca. 23 GiB verfuegbar |
|
||||||
|
| Slots / Ausbau | 4x 8 GB DDR4 belegt, gemischte Module |
|
||||||
|
| Module | Crucial CT8G4DFS8266.C8FE, Crucial CT8G4DFS8213.C8FDD1, 2x G.Skill F4-3600C17-8GVK |
|
||||||
|
| Konfigurierter Takt | 2133 MT/s |
|
||||||
|
| ECC | Nein |
|
||||||
|
|
||||||
|
Pruefkommando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
free -h
|
||||||
|
dmidecode -t memory | grep -E "Size|Speed|Locator|Type" | head -40
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mainboard und Controller
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Mainboard | Gigabyte Technology Co., Ltd. B760M DS3H DDR4 |
|
||||||
|
| BIOS/Firmware | American Megatrends International F21, Release 2025-06-19 |
|
||||||
|
| SATA/HBA Controller | Intel Raptor Lake SATA AHCI Controller, onboard |
|
||||||
|
| NVMe Controller | Samsung SM981/PM981/PM983 NVMe Controller |
|
||||||
|
| NVMe Slots | mindestens 1 belegt |
|
||||||
|
|
||||||
|
Pruefkommando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dmidecode -t baseboard | head -30
|
||||||
|
lspci
|
||||||
|
```
|
||||||
|
|
||||||
|
## Netzwerk-Hardware
|
||||||
|
|
||||||
|
| Interface | Speed | Rolle | Bemerkung |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| eth0 / bond0 / br0 | 1 Gbit/s full duplex | LAN | Realtek RTL8125 2.5GbE Controller, Link aktuell 1G; Host-IP `192.168.178.58/24`, Gateway `192.168.178.1` |
|
||||||
|
| tailscale1 | virtuell | VPN | Tailscale IPv4 `100.80.98.33` |
|
||||||
|
|
||||||
|
Pruefkommando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ip -br link
|
||||||
|
ethtool <interface>
|
||||||
|
tailscale ip -4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
| Slot | Device | Modell | Seriennummer | Groesse | Filesystem | Rolle | Health |
|
||||||
|
|---|---|---|---|---:|---|---|---|
|
||||||
|
| Cache | `nvme0n1p1` | Samsung SSD 970 EVO Plus 2TB | `S4J4NM0W609649H` | 1.8T | XFS | Appdata/system/domains | SMART passed |
|
||||||
|
| Disk1 | `md1p1` / physisch `sdc` | WDC WD60EFAX-68JH4N1 | `WD-WX32D90PC0V0` | 5.5T | XFS auf md1p1 | Array-Daten | SMART passed |
|
||||||
|
| Parity | physisch `sdb` | TOSHIBA HDWG480 | `2460A03VFA3H` | 7.3T | n/a | Parity | SMART passed |
|
||||||
|
| Boot | `sda1` | Samsung Flash Drive | `0375125090000587` | 59.8G | FAT32 | Unraid Boot | aktiv |
|
||||||
|
| Cold Backup | TBD | TBD | TBD | TBD | TBD | Externe Rotation | offen |
|
||||||
|
|
||||||
|
Pruefkommando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lsblk -o NAME,SIZE,MODEL,SERIAL,FSTYPE,MOUNTPOINT,VENDOR
|
||||||
|
findmnt -no FSTYPE /mnt/cache /mnt/disk1 /boot
|
||||||
|
df -h /mnt/cache /mnt/disk1 /mnt/user
|
||||||
|
```
|
||||||
|
|
||||||
|
## SMART / Health
|
||||||
|
|
||||||
|
| Device | Letzter Check | Kritische Werte | Bewertung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| /dev/nvme0n1 | 2026-05-26 | Critical Warning `0x00`, Percentage Used `0%`, Media Errors `0`, Power On Hours `370`, Written `5.87 TB` | gut |
|
||||||
|
| /dev/sdb | 2026-05-26 | Reallocated `0`, Pending `0`, Uncorrectable `0`, CRC `1`, Power On Hours `8971` | gut, CRC-Wert beobachten |
|
||||||
|
| /dev/sdc | 2026-05-26 | Reallocated `0`, Pending `0`, Uncorrectable `0`, CRC `0`, Power On Hours `14174` | gut |
|
||||||
|
|
||||||
|
Pruefkommando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
smartctl -a /dev/nvme0n1
|
||||||
|
smartctl -a /dev/sdb
|
||||||
|
smartctl -a /dev/sdc
|
||||||
|
```
|
||||||
|
|
||||||
|
## USV / Power Loss
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| USV vorhanden | Nicht validiert / keine erkannte USV |
|
||||||
|
| Modell | Kein APC/Eaton/CyberPower-Geraet per `lsusb` erkannt |
|
||||||
|
| Verbindung | `apcupsd` ist auf USB vorkonfiguriert, aber kein passendes USB-USV-Geraet sichtbar |
|
||||||
|
| Software | `apcaccess` vorhanden; `apcupsd` laeuft nicht, `localhost:3551` liefert Connection refused |
|
||||||
|
| Konfigurierte Schwellen | `BATTERYLEVEL 5`, `MINUTES 3`, `TIMEOUT 0`, aber inaktiv solange `apcupsd` nicht laeuft |
|
||||||
|
| Laufzeit im Idle | Nicht messbar |
|
||||||
|
| Letzter Shutdown-Test | Nicht durchgefuehrt |
|
||||||
|
|
||||||
|
Bewertung:
|
||||||
|
|
||||||
|
- Aktueller Befund 2026-05-26: keine funktionierende USV-Absicherung nachgewiesen.
|
||||||
|
- `apcupsd` ist zwar auf dem System vorhanden, aber nicht aktiv.
|
||||||
|
- Power-Loss bleibt damit ein bewusst offenes Risiko fuer Docker-/DB-State und laufende Writes.
|
||||||
|
- Naechste Entscheidung: echte USV anschliessen und Shutdown testen oder Risiko bewusst akzeptieren und dokumentieren.
|
||||||
|
|
||||||
|
## Stromverbrauch
|
||||||
|
|
||||||
|
| Zustand | Verbrauch | Messmethode | Datum |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| Idle | TBD | externes Messgeraet erforderlich | TBD |
|
||||||
|
| Normalbetrieb | TBD | externes Messgeraet erforderlich | TBD |
|
||||||
|
| Backup-Lauf | TBD | externes Messgeraet erforderlich | TBD |
|
||||||
|
| Last | TBD | externes Messgeraet erforderlich | TBD |
|
||||||
|
|
||||||
|
## Ersatzteil- und Lifecycle-Plan
|
||||||
|
|
||||||
|
| Komponente | Trigger | Massnahme |
|
||||||
|
|---|---|---|
|
||||||
|
| Cache-NVMe | >70 % Fuellstand oder SMART-Warnung | Zweite NVMe / Pool-Entscheidung; aktuell 6 % belegt |
|
||||||
|
| Disk1 | >80 % Fuellstand oder SMART-Warnung | Array-Erweiterung / Ersatz; aktuell 33 % belegt |
|
||||||
|
| Parity | Kleiner als neue groesste Datenplatte | Parity-Upgrade vor Datenplatten-Upgrade |
|
||||||
|
| Boot-USB | Lesefehler oder Alter TBD | Flash-Backup verifizieren, Ersatzstick vorbereiten |
|
||||||
|
| RAM | Swap/OOM oder Immich/Nextcloud-Druck | Ausbau planen |
|
||||||
|
| USV | keine funktionierende USV-Abschaltung | USV anschaffen/anschliessen oder Risiko schriftlich akzeptieren |
|
||||||
|
|
||||||
|
## Audit-Kommandos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hostname
|
||||||
|
uname -a
|
||||||
|
cat /etc/unraid-version 2>/dev/null || true
|
||||||
|
lscpu
|
||||||
|
free -h
|
||||||
|
dmidecode -t baseboard | head -30
|
||||||
|
dmidecode -t bios -t system -t baseboard
|
||||||
|
dmidecode -t memory | grep -E "Size|Speed|Locator|Type" | head -40
|
||||||
|
lspci | egrep -i 'sata|ahci|raid|nvme|ethernet|network'
|
||||||
|
ip -br link
|
||||||
|
ethtool eth0
|
||||||
|
tailscale ip -4
|
||||||
|
lsblk -o NAME,SIZE,MODEL,SERIAL,FSTYPE,MOUNTPOINT,VENDOR
|
||||||
|
df -Th /mnt/cache /mnt/disk1 /mnt/user /boot
|
||||||
|
smartctl -a /dev/nvme0n1 | head -100
|
||||||
|
smartctl -a /dev/sdb | head -100
|
||||||
|
smartctl -a /dev/sdc | head -100
|
||||||
|
apcaccess status
|
||||||
|
/etc/rc.d/rc.apcupsd status
|
||||||
|
lsusb
|
||||||
|
```
|
||||||
@@ -15,13 +15,13 @@ Ziel: Home Assistant schreibt ausgewaehlte Ecowitt- und Energiesensoren nach Inf
|
|||||||
|
|
||||||
Der Stack haelt InfluxDB bewusst ohne Traefik-Route. Fuer Home Assistant wird nur der HTTP-Port `8181` auf einer internen LAN-Adresse veroeffentlicht.
|
Der Stack haelt InfluxDB bewusst ohne Traefik-Route. Fuer Home Assistant wird nur der HTTP-Port `8181` auf einer internen LAN-Adresse veroeffentlicht.
|
||||||
|
|
||||||
In Komodo/Stack-Environment fuer `ops/grafana-influxdb` setzen:
|
Im Zielzustand in Komodo/Stack-Environment fuer `monitoring` setzen:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
INFLUXDB_BIND_IP=192.168.178.58
|
INFLUXDB_BIND_IP=192.168.178.58
|
||||||
```
|
```
|
||||||
|
|
||||||
`192.168.178.58` ist die LAN-IP des Docker-Hosts, auf dem `influxdb3-core` laeuft. Nicht `0.0.0.0` verwenden, wenn es nicht notwendig ist.
|
`192.168.178.58` ist die LAN-IP des Docker-Hosts, auf dem `monitoring-influxdb3-core` laeuft. Nicht `0.0.0.0` verwenden, wenn es nicht notwendig ist.
|
||||||
|
|
||||||
Danach den Stack neu deployen und von Home Assistant aus pruefen:
|
Danach den Stack neu deployen und von Home Assistant aus pruefen:
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ ha core restart
|
|||||||
|
|
||||||
## 5. Grafana Smoke-Test
|
## 5. Grafana Smoke-Test
|
||||||
|
|
||||||
In Grafana mit der bestehenden Datenquelle `InfluxDB 3 Core` eine SQL-Abfrage testen:
|
In `https://monitoring.kaleschke.info` mit der bestehenden Datenquelle `InfluxDB 3 Core` eine SQL-Abfrage testen:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
SHOW TABLES
|
SHOW TABLES
|
||||||
|
|||||||
+198
-2
@@ -8,6 +8,7 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab
|
|||||||
- Komodo ist der einzige produktive Stack-Manager.
|
- Komodo ist der einzige produktive Stack-Manager.
|
||||||
- Portainer CE ist entfernt.
|
- Portainer CE ist entfernt.
|
||||||
- Firefly, Firefly-Fints und Semaphore sind entfernt.
|
- Firefly, Firefly-Fints und Semaphore sind entfernt.
|
||||||
|
- `monitoring/` ist der einzige aktive Observability-Stack; alte Repo-Pfade `ops/grafana-influxdb` und `ops/loki` sind entfernt.
|
||||||
- Borg UI ist produktiv, Dump-Automatisierung laeuft host-seitig und ein Restore-Smoke-Test wurde erfolgreich durchgefuehrt.
|
- Borg UI ist produktiv, Dump-Automatisierung laeuft host-seitig und ein Restore-Smoke-Test wurde erfolgreich durchgefuehrt.
|
||||||
- GitHub Desktop ist der bevorzugte lokale Workflow fuer `Fetch`, `Pull`, `Commit` und `Push`.
|
- GitHub Desktop ist der bevorzugte lokale Workflow fuer `Fetch`, `Pull`, `Commit` und `Push`.
|
||||||
- Mutable Image-Tags sind auf die aktuell laufenden Digests eingefroren.
|
- Mutable Image-Tags sind auf die aktuell laufenden Digests eingefroren.
|
||||||
@@ -16,6 +17,192 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab
|
|||||||
|
|
||||||
## Historische Meilensteine
|
## Historische Meilensteine
|
||||||
|
|
||||||
|
### 2026-05-26 - Audit-Baseline-Tag gesetzt
|
||||||
|
|
||||||
|
- Der Stand nach Hardware-/Capacity-Baseline, Policy-Triage und Recovery-Doku wurde als `audit-2026-05-25-baseline` markiert und nach Gitea gepusht.
|
||||||
|
|
||||||
|
### 2026-05-26 - Externe Abhaengigkeiten und Services-Recovery baseline dokumentiert
|
||||||
|
|
||||||
|
- `docs/EXTERNAL_DEPENDENCIES.md` von Template auf Betreiber-Baseline angehoben: Domain, Cloudflare, Hetzner, GitHub-Mirror, Tailscale, GMX, Let's Encrypt, Registries, Plex und mobile Push-Pfade sind mit Ausfallwirkung und Notfallplan dokumentiert.
|
||||||
|
- `docs/SERVICES_RECOVERY.md` finalisiert den Komodo-Bootstrap-Anker: `ops/komodo/docker-compose.yml` bleibt verbindlich; der `komodo`-Self-Stack hat keinen aktiven Gitea-Webhook und ist nicht der Recovery-Anker.
|
||||||
|
- Offene Off-Repo-Betreiberchecks bleiben Account-Besitz, 2FA-Recovery-Codes, Zahlungswege, Borg-Passphrase-Hinterlegung und Gitea-Bundle-/Mirror-Mechanik.
|
||||||
|
|
||||||
|
### 2026-05-26 - Policy-Warnings triagiert
|
||||||
|
|
||||||
|
- Plex `network_mode: host` wurde in den Policy-Ausnahmen als dokumentierte Discovery-Ausnahme erfasst.
|
||||||
|
- Mutable Tags bei `ddns-updater`, `glances` und `scrutiny` bleiben wegen vorhandener SHA256-Digests reproduzierbar gepinnt und werden im Policy-Report als Info-Ausnahmen sichtbar gehalten.
|
||||||
|
- `monitoring-influxdb3-core` bleibt als dokumentierte `user: "0"`-Ausnahme bewusst eine Warning, damit der Hardening-Punkt nicht aus dem Blick faellt.
|
||||||
|
|
||||||
|
### 2026-05-26 - Hardware-/Capacity-Baseline abgeschlossen
|
||||||
|
|
||||||
|
- Hardware-Inventar auf Host-Befund aktualisiert: BIOS AMI F21 vom 2025-06-19, Intel Raptor Lake SATA AHCI, Samsung NVMe Controller und Realtek RTL8125 2.5GbE mit aktuellem 1G-Link.
|
||||||
|
- RAM-Baseline dokumentiert: 4x 8 GB DDR4 ohne ECC, gemischte Module, aktuell 2133 MT/s konfiguriert.
|
||||||
|
- Capacity-Baseline dokumentiert: Cache 1.9T mit 97G genutzt (6 %), Disk1/User-Shares 5.5T mit 1.8T genutzt (33 %), lokale Backups 2.2G unter `/mnt/user/backups`.
|
||||||
|
- USV-Befund dokumentiert: `apcupsd` ist vorhanden und auf USB vorkonfiguriert, laeuft aber nicht; `apcaccess status` liefert Connection refused und `lsusb` zeigt keine erkannte USV. Power-Loss bleibt damit eine offene Betreiberentscheidung.
|
||||||
|
|
||||||
|
### 2026-05-26 - Komodo/Gitea-Restdrift bereinigt
|
||||||
|
|
||||||
|
- Der alte Komodo-Stack `grafana` wurde als historischer Altstand inert gemacht: keine Repo-Dateipfade, kein Webhook, keine alte Stack-ENV, keine `missing_files`/`remote_errors`. Rollback bleibt Git-Historie, nicht der alte Komodo-Stack.
|
||||||
|
- Der Gitea-Hook `35` fuer den alten `grafana`-Stack bleibt inaktiv. Der nicht sinnvolle `komodo`-Self-Hook `11` wurde deaktiviert, weil Komodo selbst nicht per Gitea-Webhook auf `master` deployed wird.
|
||||||
|
- Ein kurz sichtbarer Komodo-DB-Typfehler durch `updated_at` als Float wurde im selben Kontrollfenster auf nativen Mongo `Long` korrigiert; danach traten keine neuen `invalid type`-Fehler mehr auf.
|
||||||
|
- Nach der Bereinigung: aktive Gitea-Komodo-Hooks haben `0` Fehlstatus; `komodo-core`, `komodo-periphery`, `komodo-mongo`, `nextcloud` und die aktuellen `monitoring-*` Container laufen weiter.
|
||||||
|
|
||||||
|
### 2026-05-26 - Monitoring-Altstaende aus aktivem Repo entfernt
|
||||||
|
|
||||||
|
- Die abgeloesten Pfade `ops/grafana-influxdb` und `ops/loki` wurden per `git rm` aus dem aktiven Repo entfernt. `monitoring/` bleibt der einzige Observability-Zielstack.
|
||||||
|
- Live-Check vor dem Cleanup: nur `monitoring-grafana`, `monitoring-promtail`, `monitoring-influxdb3-core` und `monitoring-loki` laufen; alte Container `grafana`, `influxdb3-core`, `loki` und `alloy` sind nicht vorhanden.
|
||||||
|
- Rollback erfolgt bei Bedarf ueber Git-Historie, nicht ueber parallel gepflegte Compose-Verzeichnisse.
|
||||||
|
- Im selben GitOps-Kontrollfenster wurde Gitea-Webhook `35` fuer den alten `grafana`-Rollback-Stack als inaktiv bestaetigt. Der aktive Nextcloud-Hook `36` hatte einen Signaturfehler; sein Secret wurde ohne Ausgabe des Werts aus der Komodo-Stack-Konfiguration zurueck nach Gitea synchronisiert.
|
||||||
|
|
||||||
|
### 2026-05-26 - AdGuard Admin-Port auf Tailscale-Soll begrenzt
|
||||||
|
|
||||||
|
- Host-Audit per SSH gegen `Kallilabcore` durchgefuehrt: Tailscale IPv4 ist `100.80.98.33`, LAN-IP ist `192.168.178.58/24`, Gateway `192.168.178.1`.
|
||||||
|
- Repo-Soll fuer `host-services/Adguard/docker-compose.yml` geaendert: DNS `53/tcp+udp` bleibt unveraendert, die Admin-UI bindet nun auf `100.80.98.33:8082:80`.
|
||||||
|
- Architektur, Service-Katalog, Repo-Map, Netzwerk-Inventar und AI-Kontext wurden an das neue Modell angepasst: keine Traefik-/Authelia-2FA-Umstellung, aber keine LAN-weite Admin-Bindung mehr.
|
||||||
|
- Live-Deploy wurde nach Fast-Forward des AdGuard-Workspaces auf `5cb4017` mit `docker compose -p adguard ... up -d` ausgefuehrt. Validierung erfolgreich: `ss -ltnp` zeigt `100.80.98.33:8082`, DNS via `@127.0.0.1` und `@192.168.178.58` funktioniert, `http://100.80.98.33:8082/` liefert HTTP 302, `http://192.168.178.58:8082/` ist nicht mehr erreichbar.
|
||||||
|
- Nachpruefung des GitOps-Pfads: Gitea-Hook `1` zeigt auf Komodo-Stack `69c7b9e26b77cd827811b9d0` und lieferte HTTP 200. Komodo-Deploys fuer AdGuard scheiterten zunaechst im `Git pull` einmal an `.git/index.lock` und danach an `fatal: Cannot rebase onto multiple branches`; der Workspace wurde aufgeraeumt und steht sauber auf `origin/master`.
|
||||||
|
|
||||||
|
### 2026-05-26 - Audit-Umsetzung vorbereitet
|
||||||
|
|
||||||
|
- Aus `docs/AUDIT_2026-05-25.md` wurde `docs/AUDIT_2026-05-25_TODO.md` als operative Arbeitsliste abgeleitet. Authelia-2FA/OIDC bleibt bewusst geparkt und wird erst nach finaler Policy-Entscheidung umgesetzt.
|
||||||
|
- Neue Inventar- und Betriebsdokumente angelegt: `docs/HARDWARE_INVENTORY.md`, `docs/NETWORK_INVENTORY.md`, `docs/EXTERNAL_DEPENDENCIES.md`, `docs/CAPACITY_AND_LIFECYCLE.md` und `docs/FAMILY_ONBOARDING.md`.
|
||||||
|
- `docs/SERVICES_RECOVERY.md` beschreibt initial die recovery-kritischen `/mnt/user/services`-Pfade, Gitea-Repo-Mirror-Optionen, Komodo-Bootstrap und Secret-Recovery-Reihenfolge.
|
||||||
|
- Policy-Check lokal erneut ausgefuehrt: die alten SEC001-Warnings fuer `ddns-updater` und `scrutiny` sind nicht mehr aktuell; verbleibende Warnings betreffen Host-Netz-/User-/Image-Tag-Themen und Altstaende.
|
||||||
|
|
||||||
|
### 2026-05-25 - Unraid Flash-Backup in Borg-Scope aufgenommen
|
||||||
|
|
||||||
|
- `pre-backup-dumps.sh` erzeugt zusaetzlich zu den DB-Dumps ein sensibles `unraid-flash-config.tar.gz` aus `/boot/config` inklusive SHA256 und Manifest unter `/mnt/user/backups/borg/dumps/latest`.
|
||||||
|
- Da `/local/borg-dumps` bereits Teil des Borg-Scopes ist, wird das Flash-Konfigurationsartefakt mit dem bestehenden Hetzner/Borg-Backup historisiert. Downloadbare Plugin-Paketarchive unter `/boot/config/plugins/*/` werden aus dem Artefakt ausgeschlossen; Restore-relevante Konfiguration bleibt enthalten.
|
||||||
|
- Live-Erstlauf erfolgreich: `pre-borg.sh` lieferte `critical_count=0`, Freshness `Critical: 0`, `unraid-flash-config.tar.gz` ist 297 KiB gross, `0600 root:root`, SHA256-Pruefung `OK`, 356 Archiv-Eintraege, Manifest fuer Unraid `7.2.4`. Der Borg-UI-Job `Taegliche Sicherung` ist aktiv und umfasst `/local/borg-dumps`; der naechste planmaessige Hetzner-Lauf nimmt das neue Flash-Artefakt mit. Der Host-Repo-Clone unter `/mnt/user/services/homelab-infra` wurde wegen eines fehlgeschlagenen Fast-Forward-Checkouts frisch geklont; der vorherige Stand liegt archiviert unter `/mnt/user/services/_archive/homelab-infra-pre-refresh-20260525-194209`.
|
||||||
|
|
||||||
|
### 2026-05-25 - Monitoring-Zielstack finalisiert und Uptime Kuma entfernt
|
||||||
|
|
||||||
|
- `monitoring` und `glance` wurden auf Commit `b6bbca4` deployed; Komodo zeigt fuer beide `latest_hash` = `deployed_hash` = `b6bbca4` ohne `remote_errors`. Die zehn `monitoring-*` Container laufen, `monitoring.kaleschke.info` und `glance.kaleschke.info` leiten anonym zu Authelia, Prometheus ist ready und Loki `/ready` liefert `ready`.
|
||||||
|
- Alte Monitoring-Altcontainer `grafana`, `influxdb3-core`, `loki` und `alloy` sind in Docker nicht vorhanden; `ops/grafana-influxdb` und `ops/loki` bleiben nur als Rollback-/Migrationsreferenz im Repo. Der noch aktive Gitea-Hook `35` des alten `grafana`-Rollback-Stacks wurde deaktiviert, damit zukuenftige Pushes den Altstand nicht reaktivieren.
|
||||||
|
- Uptime Kuma wurde durch Blackbox/Prometheus/Grafana ersetzt: aktive Blackbox-Zielliste enthaelt 19 HTTPS-Ziele, `uptime.kaleschke.info` ist dort nicht mehr enthalten und liefert nach Stack-Removal 404. Der Komodo-Stack `uptime-kuma` wurde gestoppt/destroyed/geloescht, Gitea-Webhook `23` deaktiviert, Appdata nach `/mnt/user/appdata/_archive/uptime-kuma-removed-2026-05-25` und der alte Stack-Workspace nach `/mnt/user/services/stacks/_archive/uptime-kuma-removed-2026-05-25` verschoben.
|
||||||
|
- Authelia-Hostconfig wurde mit Backup `configuration.yml.pre-uptime-removal-20260525-164343.bak` um den toten `uptime.kaleschke.info`-Eintrag bereinigt, validiert und neu gestartet. Prometheus wurde wegen eines `Stale NFS file handle` auf der gebundenen Konfigurationsdatei per Komodo-Restart neu gemountet.
|
||||||
|
|
||||||
|
### 2026-05-25 - AdGuard Admin-Port bewusst LAN-direkt belassen
|
||||||
|
|
||||||
|
- Strategische Option `adguard.kaleschke.info` hinter Traefik/Authelia-2FA wurde bewertet, aber vom Operator bewusst verworfen, weil der Betriebsweg einfach bleiben soll. AdGuard bleibt als dokumentierte Ausnahme mit DNS `53/tcp+udp` und Admin `8082:80` LAN-direkt; keine Live-Aenderung an AdGuard, Authelia oder Traefik wurde vorgenommen.
|
||||||
|
|
||||||
|
### 2026-05-25 - Borg-Passphrase Host-Secret verifiziert
|
||||||
|
|
||||||
|
- Erwartete Host-Secret-Datei `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` aus der bestehenden Borg-UI-Repo-Konfiguration erzeugt, mit `root:root` und Modus `600` gesichert und per `borg info` gegen das Hetzner-Borg-Repo verifiziert. Analoge Offline-Hinterlegung bleibt bewusste Operator-Aufgabe; Secret-Wert wurde nicht ausgegeben oder dokumentiert.
|
||||||
|
|
||||||
|
### 2026-05-25 - Dashboard auf Glance konsolidiert
|
||||||
|
|
||||||
|
- Glance bleibt das einzige Homelab-Dashboard; Homepage wurde aus dem Zielbild entfernt. Authelia-Default-Redirect, Monitoring-Blackbox-Ziele, Cert-Check-Domains und Glance-Konfiguration zeigen nicht mehr auf `home.kaleschke.info`; Homepage wurde via Komodo-API gestoppt/destroyed, der Komodo-Stack geloescht, der alte Gitea-Webhook deaktiviert und Appdata nach `/mnt/user/appdata/_archive/homepage-removed-2026-05-25` verschoben.
|
||||||
|
|
||||||
|
### 2026-05-25 - Jellyfin aus Zielbild entfernt
|
||||||
|
|
||||||
|
- Plex-Smoke-Test erfolgreich (`/identity` HTTP 200, Container healthy, `/data/movies` und `/photos` sichtbar). Jellyfin wurde repo-seitig entfernt, aus Authelia-Baseline und Zielbild-Doku ausgetragen, via Komodo-API gestoppt/destroyed, der Komodo-Stack geloescht und Appdata nach `/mnt/user/appdata/_archive/jellyfin-removed-2026-05-25` verschoben; Plex bleibt einziger Medienserver.
|
||||||
|
|
||||||
|
### 2026-05-25 - Externer Repo-Mirror eingerichtet
|
||||||
|
|
||||||
|
- Gitea erlaubt fuer Repo-Migrationen und Mirror-Targets gezielt `github.com` und nutzt explizite externe DNS-Resolver. `Micha/homelab-infra` spiegelt nun als privater GitHub-Push-Mirror nach `michaelkaleschke-spec/homelab-infra`; erster manueller Sync erfolgreich, Gitea `push_mirror.last_error` leer. Token-Werte bleiben ausschliesslich in Gitea/GitHub und werden nicht dokumentiert.
|
||||||
|
|
||||||
|
### 2026-05-25 - Audit-Final nachgemessen
|
||||||
|
|
||||||
|
- Audit-Restliste erneut live geprueft: runtime-relevanter Stack-Inhalt fuer `gitea`, `borg-ui` und `monitoring` seit `66ee10c` unveraendert; abschliessende Audit-Doku-Commits liegen in Gitea; Monitoring inklusive Loki `/ready` gruen; Borg-Job und 15 kanonische Dump-Artefakte frisch; `docs/AUDIT_2026-05-23_FINAL.md` auf den Live-Stand aktualisiert.
|
||||||
|
|
||||||
|
### 2026-05-25 - Disk1 Phase 2 abgeschlossen
|
||||||
|
|
||||||
|
- Disk1 wurde nach H:-Freeze-Backup und finalem Service-Freeze von NTFS/`ntfs3` auf XFS migriert.
|
||||||
|
- Restore verifiziert: `media` final 2722 Dateien und 1,800,782,188,226 Bytes mit 0 missing/extra/size mismatch; Tar-Shares und Disk1-Extras aus den H:-Freeze-Archiven wiederhergestellt.
|
||||||
|
- Docker/Services nach XDG-Runtime-Fix wieder stabil: 49 Container laufend, 0 stopped, 0 unhealthy, 0 starting; Gitea, Komodo, Borg, Jellyfin und Monitoring per Smoke-Test erreichbar.
|
||||||
|
- Borg-UI meldet den letzten Backup-Job `completed`; `pre-backup-dumps.sh` wurde nach Wiederanlauf erneut ausgefuehrt und 15 kanonische Dump-Artefakte sind juenger als 24 h.
|
||||||
|
- `posture-check` erwartet Disk1 nun standardmaessig als XFS (`ALLOW_DISK1_NTFS=0`).
|
||||||
|
|
||||||
|
### 2026-05-23 - Audit-Endstufe verifiziert
|
||||||
|
|
||||||
|
- Lokalen Hardening-Commit `cd650b1` nach Gitea gepusht; Komodo-Workspaces fuer `gitea`, `borg-ui` und `monitoring` stehen auf `cd650b1`.
|
||||||
|
- Live-Audit in `docs/AUDIT_2026-05-23_LIVE.md` dokumentiert: Gitea-Registration geschlossen, Borg-Dumps frisch, Monitoring-Stack aktiv, alte Grafana/Loki-Altcontainer nicht mehr vorhanden.
|
||||||
|
- Jellyfin und Plex in Architektur, Service-Katalog und Repo-Map nachgetragen; Plex ist jetzt als Repo-Compose-Stack mit dokumentierter Host-Netz-Ausnahme gefuehrt.
|
||||||
|
- Repo-Hygiene abgeschlossen: `.serena/` ignoriert, leere Verzeichnisse entfernt, Windows-Reinstall-Helfer unter `ops/windows-reinstall/` bewusst versioniert.
|
||||||
|
|
||||||
|
### 2026-05-20 - Gitea 5xx-Bursts untersucht und Signup geschlossen
|
||||||
|
|
||||||
|
- Live-Befund zu `HomelabTraefik5xx`: kurze externe `POST /`-Bursts auf `gitea@docker` von `103.153.183.69` und `103.153.183.73`, jeweils HTTP 500 in unter 10 ms; normale Gitea-Checks und Git-Reads liefen parallel mit HTTP 200.
|
||||||
|
- Keine Hinweise auf erfolgreichen Zugriff: Gitea-Container ohne Restart/OOM, nur User `micha`, keine neuen User der letzten 30 Tage, keine neuen Repos, SSH-Keys oder Access-Tokens im Untersuchungsfenster.
|
||||||
|
- Live-Prometheus lief noch mit der alten Regel `rate(...[5m]) > 0`; die bereits im Repo vorbereitete Regel `increase(...[5m]) >= 5` wurde auf den Live-Mount kopiert und per Prometheus-Reload aktiviert.
|
||||||
|
- Gitea-Registrierung und OpenID-Signup wurden geschlossen: `DISABLE_REGISTRATION=true`, `REGISTER_EMAIL_CONFIRM=true`, `ENABLE_OPENID_SIGNIN=false`, `ENABLE_OPENID_SIGNUP=false`; Signup-Seite zeigt danach "Registration is disabled", OpenID-Login liefert 403.
|
||||||
|
|
||||||
|
### 2026-05-18 - Komodo Webhooks vollstaendig abgeglichen
|
||||||
|
|
||||||
|
- Live-Befund auf `Kallilabcore`: Komodo hatte fuer mehrere aktuelle Stacks `webhook_enabled: true`, aber Gitea enthielt noch nicht fuer alle aktuellen Stack-IDs aktive Webhooks.
|
||||||
|
- In der Gitea-Datenbank wurden aktive Webhooks fuer `monitoring` (`6a08d5297707b0930ab95c72`), `glance` (`6a09d7347707b0930ab96eae`), `grafana` (`69f31ecdf65eb72b757c497d`) und `nextcloud` (`69e519085fd5e8bc51f121f0`) nach dem bestehenden Komodo-Hook-Muster angelegt.
|
||||||
|
- Stale aktive Gitea-Hooks auf nicht mehr vorhandene bzw. alte Komodo-Stack-IDs wurden deaktiviert.
|
||||||
|
- Abgleich danach: 30 aktive Gitea-Komodo-Hooks fuer 30 Komodo-Stacks mit aktiviertem Webhook; `hermes` bleibt in Komodo bewusst `webhook_enabled: false`.
|
||||||
|
- Netzwerkpfad aus dem `gitea`-Container zu `komodo-core:9120` wurde erfolgreich verifiziert; `last_status=0` fuer neue Hooks bleibt bis zum ersten Push erwartbar.
|
||||||
|
|
||||||
|
### 2026-05-19 - Posture-Check Host-Version verifiziert
|
||||||
|
|
||||||
|
- Ursache fuer wiederholte ntfy-Warnings war nicht mehr die Repo-Logik allein, sondern dass auf dem Unraid-Host noch die alte Skriptversion unter `/mnt/user/services/homelab-infra/services/posture-check/posture-check.sh` ausgefuehrt wurde.
|
||||||
|
- Host-Skript wurde mit Backup ersetzt und mit `SEND_NTFY=0` direkt auf dem Host verifiziert.
|
||||||
|
- Ergebnis des echten Host-Laufs: `status: ok`, `critical_count: 0`, `warning_count: 0`.
|
||||||
|
- Betriebsregel daraus: Bei Host-User-Scripts nach Repo-Aenderungen immer den tatsaechlich ausgefuehrten Host-Pfad und den Live-Output pruefen.
|
||||||
|
|
||||||
|
### 2026-05-19 - Borg-Scope fuer GitOps Host Automation erweitert
|
||||||
|
|
||||||
|
- Nach den Gitea-/Komodo-Webhook- und Posture-Check-Aenderungen wurde der Backup-Scope um Host-GitOps-Pfade erweitert.
|
||||||
|
- Borg UI mountet kuenftig `/mnt/user/services` read-only als `/local/services`.
|
||||||
|
- In `all-important-sources.txt` wurden `/local/services/homelab-infra`, `/local/services/stacks` und `/local/services/posture-check` aufgenommen.
|
||||||
|
- `pre-backup-dumps.sh` wurde auf dem Host ausgefuehrt; frische Dumps fuer `gitea.sqlite.dump` und `komodo-mongo.archive.gz` liegen unter `/mnt/user/backups/borg/dumps/latest`.
|
||||||
|
- Wirksam wird der neue `/local/services`-Mount nach Redeploy/Recreate des `borg-ui`-Stacks.
|
||||||
|
|
||||||
|
### 2026-05-19 - Traefik-5xx Alert entstoert
|
||||||
|
|
||||||
|
- `HomelabTraefik5xx` hatte auf einzelne 5xx-Antworten reagiert, weil die Regel `rate(...[5m]) > 0` nutzte.
|
||||||
|
- Live-Befund fuer `gitea@docker`: zwei kurze `POST /` mit HTTP 500 von einer externen IP, danach durchgehend erfolgreiche Gitea-Checks; kein Container-Restart.
|
||||||
|
- Prometheus-Regel auf `increase(...[5m]) >= 5` geaendert, damit einzelne externe Fehlrequests keinen ntfy-Alarm ausloesen.
|
||||||
|
|
||||||
|
### 2026-05-17 - Glance Homelab-Dashboard vorbereitet
|
||||||
|
|
||||||
|
- `ops/glance` als geschuetztes Homelab-Dashboard unter `glance.kaleschke.info` vorbereitet.
|
||||||
|
- Glance zeigt HTTP-Monitore fuer Core, Apps und Ops, Docker-Containergruppen, Host-Snapshot und Bookmarks.
|
||||||
|
- Docker-Status laeuft nicht ueber einen direkten Socket-Mount in Glance, sondern ueber `glance-docker-socket-proxy` auf einem internen `glance_socket_net`.
|
||||||
|
- Die HTTP-Monitore nutzen oeffentliche URLs als Klickziel und interne `check-url`-Endpunkte auf `frontend_net`, damit Glance nicht vom externen Hairpin-/Auth-Pfad abhaengt.
|
||||||
|
- Das Immich Community-Widget wurde ergaenzt. Der API-Zugriff nutzt eine interne Service-URL und ein Stack-ENV-Token. Paperless, Scrutiny und Speedtest bleiben Kandidaten fuer einen spaeteren Widget-Pass, sobald die konkrete API-Ausgabe im Glance-Kontext sauber verifiziert ist.
|
||||||
|
- Das Dashboard-Layout wurde an `ginesjunior11/glance-dashboard-config` angelehnt: dunkleres blaues Theme, Zeitfortschrittsgruppe, farbige Dashboard-Icons, dichter `Homelab Status`, Server-Stats im Hauptbereich und eine zweite Seite `Infrastructure and Media`. Die rechte Home-Spalte zeigt WAN-Infos aus Speedtest Tracker, Speedtest-Livewerte, AdGuard-DNS-Stats, DNS/Ingress-Monitore und eine separate Netzwerk-Containergruppe.
|
||||||
|
|
||||||
|
### 2026-05-17 - Monitoring-Zielstack konsolidiert
|
||||||
|
|
||||||
|
- `monitoring/` als zentraler Observability-Zielstack fuer Prometheus, Loki, Promtail, Grafana, node-exporter, cAdvisor und InfluxDB 3 Core vorbereitet.
|
||||||
|
- `monitoring-grafana` nutzt den Repo-Standard `authelia@file,secure-headers@file` und Secrets per Datei statt Klartext-Stack-ENV.
|
||||||
|
- `monitoring-influxdb3-core` uebernimmt den LAN-only Writer-Endpunkt fuer Home Assistant (`8181` via `INFLUXDB_BIND_IP`).
|
||||||
|
- `ops/loki` und `ops/grafana-influxdb` sind abgeloeste Altstaende und bleiben nur als Rollback-/Migrationsreferenz im Repo.
|
||||||
|
|
||||||
|
### 2026-05-07 - Vaultwarden Restore-Test praktisch verifiziert
|
||||||
|
|
||||||
|
- Erster echter Vaultwarden-Mini-Restore gegen das produktive Borg-Repo `hetzner_borg_appdata_critical` erfolgreich durchgefuehrt.
|
||||||
|
- Restore lief isoliert nach `/mnt/user/backups/restore-lab/vaultwarden`, nicht gegen produktive Pfade.
|
||||||
|
- Testinstanz `restoretest-vaultwarden` wurde lokal auf `127.0.0.1:18080` gestartet; HTTP 200 und Login-Seite wurden erfolgreich bestaetigt.
|
||||||
|
- Report wurde unter `/mnt/user/backups/restore-reports/vaultwarden-2026-05-07.md` geschrieben.
|
||||||
|
- Fuer den praktischen Restore-Pfad wurden zwei hostseitige Voraussetzungen sichtbar und umgesetzt:
|
||||||
|
- `known_hosts` fuer das Hetzner-Ziel im `borg-ui`-Container
|
||||||
|
- Host-Secret-Datei `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` fuer kuenftige Restore-Tests
|
||||||
|
- Testdaten unter `/mnt/user/backups/restore-lab/vaultwarden/data` wurden nach erfolgreichem Lauf wieder bereinigt.
|
||||||
|
|
||||||
|
### 2026-05-07 - Gitea Restore-Test praktisch verifiziert
|
||||||
|
|
||||||
|
- Erster echter Gitea-Mini-Restore gegen das produktive Borg-Repo `hetzner_borg_appdata_critical` erfolgreich durchgefuehrt.
|
||||||
|
- Restore lief isoliert nach `/mnt/user/backups/restore-lab/gitea`, nicht gegen produktive Pfade.
|
||||||
|
- Testinstanz `restoretest-gitea` wurde lokal auf `127.0.0.1:13000` und `127.0.0.1:12222` gestartet.
|
||||||
|
- HTTP 200, HTML-Titel und lokaler SSH-Port wurden erfolgreich bestaetigt.
|
||||||
|
- Report wurde unter `/mnt/user/backups/restore-reports/gitea-2026-05-07.md` geschrieben.
|
||||||
|
- Testdaten unter `/mnt/user/backups/restore-lab/gitea/data` wurden nach erfolgreichem Lauf wieder bereinigt.
|
||||||
|
|
||||||
|
### 2026-05-07 - Paperless Restore-Test praktisch verifiziert
|
||||||
|
|
||||||
|
- Erster echter Paperless-Mini-Restore gegen das produktive Borg-Repo `hetzner_borg_appdata_critical` erfolgreich durchgefuehrt.
|
||||||
|
- Restore umfasste sowohl die Dateipfade als auch `postgresql17-paperless.dump` aus dem Borg-Archiv.
|
||||||
|
- Testinstanzen `restoretest-paperless`, `restoretest-paperless-postgres` und `restoretest-paperless-redis` liefen isoliert ohne Traefik.
|
||||||
|
- Login-Seite war lokal auf `127.0.0.1:18120` erreichbar.
|
||||||
|
- Der Dump-Import in Test-Postgres war erfolgreich; die Test-Datenbank enthielt `25` Dokumente.
|
||||||
|
- Report wurde unter `/mnt/user/backups/restore-reports/paperless-2026-05-07.md` geschrieben.
|
||||||
|
- Testdaten unter `/mnt/user/backups/restore-lab/paperless` wurden nach erfolgreichem Lauf wieder bereinigt.
|
||||||
|
|
||||||
### 2026-05-06 - Komodo Webhook Secret getrennt
|
### 2026-05-06 - Komodo Webhook Secret getrennt
|
||||||
|
|
||||||
- `KOMODO_WEBHOOK_SECRET` von `KOMODO_SECRET_KEY` getrennt und als eigene Stack-ENV-Variable dokumentiert.
|
- `KOMODO_WEBHOOK_SECRET` von `KOMODO_SECRET_KEY` getrennt und als eigene Stack-ENV-Variable dokumentiert.
|
||||||
@@ -42,6 +229,15 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab
|
|||||||
- Leere `env/domains.env.example` und `env/global.env.example` mit nicht geheimen Beispielwerten gefuellt.
|
- Leere `env/domains.env.example` und `env/global.env.example` mit nicht geheimen Beispielwerten gefuellt.
|
||||||
- Veraltete `.keep`-Platzhalter aus Verzeichnissen mit echten Compose-/Repo-Inhalten sowie zwei reine Geister-Verzeichnisse (`host-services/plex`, `infra/dns`) entfernt.
|
- Veraltete `.keep`-Platzhalter aus Verzeichnissen mit echten Compose-/Repo-Inhalten sowie zwei reine Geister-Verzeichnisse (`host-services/plex`, `infra/dns`) entfernt.
|
||||||
|
|
||||||
|
### 2026-05-16 - Backup-Konsistenz und erster Hardening-Schnitt
|
||||||
|
|
||||||
|
- SQLite-Dumps fuer Gitea, Vaultwarden, Speedtest Tracker und Filebrowser werden containerseitig als `*.sqlite.dump` erzeugt und per Freshness-Check geprueft; Uptime Kuma wurde am 2026-05-25 aus dem aktiven Dump-Scope entfernt.
|
||||||
|
- `nextcloud.dump` und die Nextcloud-Userdaten sind als Option A im Borg-Scope dokumentiert.
|
||||||
|
- Filebrowser mountet keine breite `/mnt/user/appdata`-Flaeche mehr, sondern nur noch Documents, Photos, Projekte sowie eigenen App-State.
|
||||||
|
- Authelia Argon2id-Parameter in der Repo-Baseline auf `iterations: 3`, `memory: 65536`, `parallelism: 4` gesetzt; produktive Host-Config muss kontrolliert gemerged und mit Test-User validiert werden.
|
||||||
|
- Redis-Caches wurden auf `redis:7.4-alpine@sha256:...` vereinheitlicht; Nextcloud wurde mit Registry-validiertem Digest gepinnt.
|
||||||
|
- Eindeutig aufloesbare `latest@sha256`-Images wurden auf konkrete Tags umgestellt: Homepage `v1.12.3`, code-server `4.116.0`, Filebrowser `v2.63.2`, Speedtest Tracker `1.13.12`.
|
||||||
|
|
||||||
### 2026-05-05 - M3b versionierte App-Images digest-gepinnt
|
### 2026-05-05 - M3b versionierte App-Images digest-gepinnt
|
||||||
|
|
||||||
- Versionierte Nicht-Komodo-Images fuer BentoPDF, Mealie, Paperless, Paperless-GPT, AdGuard Home, Grafana, InfluxDB 3 Core und Traefik auf die am Host laufenden, manifest-validierten Digests gepinnt.
|
- Versionierte Nicht-Komodo-Images fuer BentoPDF, Mealie, Paperless, Paperless-GPT, AdGuard Home, Grafana, InfluxDB 3 Core und Traefik auf die am Host laufenden, manifest-validierten Digests gepinnt.
|
||||||
@@ -60,7 +256,7 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab
|
|||||||
- PostgreSQL 17 Datenhalter auf `postgres:17.9@sha256:5b96f1a16bd9768b060dd2ffe55cb6225c4d9ef4d214a8b21eb08134869a97e4` gepinnt (`postgresql17`, `mealie-postgres`, `nextcloud-postgres`).
|
- PostgreSQL 17 Datenhalter auf `postgres:17.9@sha256:5b96f1a16bd9768b060dd2ffe55cb6225c4d9ef4d214a8b21eb08134869a97e4` gepinnt (`postgresql17`, `mealie-postgres`, `nextcloud-postgres`).
|
||||||
- Immich pgvector-Postgres auf `tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52` gepinnt.
|
- Immich pgvector-Postgres auf `tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52` gepinnt.
|
||||||
- Komodo Mongo auf `mongo:7.0.32@sha256:32979a1189dfdc44da3f5ed40d910495f5ad8f6f7f77556646f890a30b2d3f56` sowie Komodo Core/Periphery und Gitea auf die am Host laufenden Digests gepinnt.
|
- Komodo Mongo auf `mongo:7.0.32@sha256:32979a1189dfdc44da3f5ed40d910495f5ad8f6f7f77556646f890a30b2d3f56` sowie Komodo Core/Periphery und Gitea auf die am Host laufenden Digests gepinnt.
|
||||||
- Redis-Caches bleiben bewusst ohne Digest-Pin; Redeploys erfolgen stackweise mit Smoke-Test, nicht parallel.
|
- Redis-Caches wurden am 2026-05-16 auf `redis:7.4-alpine@sha256:...` vereinheitlicht; Redeploys erfolgen stackweise mit Smoke-Test, nicht parallel.
|
||||||
|
|
||||||
### 2026-05-04 - Komodo Self-Stack Drift auf persistenten Pfad zurueckgefuehrt
|
### 2026-05-04 - Komodo Self-Stack Drift auf persistenten Pfad zurueckgefuehrt
|
||||||
|
|
||||||
@@ -107,7 +303,7 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab
|
|||||||
- Pre-Backup-Dumps host-seitig ueber Unraid User Scripts etabliert.
|
- Pre-Backup-Dumps host-seitig ueber Unraid User Scripts etabliert.
|
||||||
- Dump-Zielpfad auf `/mnt/user/backups/borg/dumps` umgestellt.
|
- Dump-Zielpfad auf `/mnt/user/backups/borg/dumps` umgestellt.
|
||||||
- Restore-Smoke-Test fuer `postgresql17-globals.sql` und `gitea.db` erfolgreich nachgewiesen.
|
- Restore-Smoke-Test fuer `postgresql17-globals.sql` und `gitea.db` erfolgreich nachgewiesen.
|
||||||
- Monitoring fuer Borg ueber `ntfy` und Uptime Kuma eingerichtet.
|
- Monitoring fuer Borg war historisch ueber `ntfy` und Uptime Kuma eingerichtet; seit 2026-05-25 ersetzt durch `ntfy`, Blackbox/Prometheus und Monitoring Grafana.
|
||||||
|
|
||||||
### 2026-04-15 - Repo- und Betriebsbereinigung
|
### 2026-04-15 - Repo- und Betriebsbereinigung
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Network Inventory - KalliLab CORE
|
||||||
|
|
||||||
|
Status: Initialer Host-Audit erfasst, Router-/VLAN-Details offen.
|
||||||
|
Letzte Pruefung: 2026-05-26
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
|
||||||
|
Dieses Dokument beschreibt Router, DNS, Tailscale, Portfreigaben und Netztrennung. Es ergaenzt das Architektur-Zielbild in `HOMELAB_ARCHITECTURE_MASTER_V2.md` um konkrete Hardware- und Betriebswerte.
|
||||||
|
|
||||||
|
## Internet und Router
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Anschluss / Provider | TBD |
|
||||||
|
| Router-Modell | TBD |
|
||||||
|
| Firmware | TBD |
|
||||||
|
| Router-IP | 192.168.178.1 |
|
||||||
|
| DHCP-Server | vermutlich Router, zu pruefen |
|
||||||
|
| Lokales Subnetz | 192.168.178.0/24 |
|
||||||
|
| IPv6 aktiv | TBD |
|
||||||
|
| DynDNS / DDNS | Cloudflare via `ddns-updater`, Details TBD |
|
||||||
|
|
||||||
|
## DNS
|
||||||
|
|
||||||
|
| Komponente | Rolle | Adresse | Bemerkung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| AdGuard Home | LAN DNS / Filter | Host `192.168.178.58`, Docker `172.23.0.3` | DNS auf Port 53; Admin soll nur via Tailscale-IP `100.80.98.33:8082` erreichbar sein |
|
||||||
|
| Unbound | Rekursiver Resolver | Docker `dns_net` | Upstream fuer AdGuard |
|
||||||
|
| Cloudflare | Authoritative DNS | extern | DNS-Challenge fuer TLS |
|
||||||
|
| Router | DHCP DNS-Verteilung | TBD | Muss auf AdGuard zeigen, falls so betrieben |
|
||||||
|
|
||||||
|
## Tailscale
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Node-Name | Kallilabcore |
|
||||||
|
| Tailscale IPv4 | 100.80.98.33 |
|
||||||
|
| Tailscale IPv6 | TBD |
|
||||||
|
| Exit Node | TBD |
|
||||||
|
| Subnet Router | TBD |
|
||||||
|
| ACL-Policy extern dokumentiert | TBD |
|
||||||
|
|
||||||
|
Pruefkommando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tailscale status
|
||||||
|
tailscale ip -4
|
||||||
|
tailscale ip -6
|
||||||
|
```
|
||||||
|
|
||||||
|
## Portfreigaben und Exposure
|
||||||
|
|
||||||
|
| Port | Ziel | Zweck | Bewertung |
|
||||||
|
|---:|---|---|---|
|
||||||
|
| 80/tcp | Traefik | HTTP->HTTPS / ACME | erwartet |
|
||||||
|
| 443/tcp | Traefik | HTTPS | erwartet |
|
||||||
|
| 222/tcp | Gitea SSH | Git SSH | dokumentierte Ausnahme |
|
||||||
|
| 53/tcp+udp | AdGuard | DNS | dokumentierte Ausnahme |
|
||||||
|
| 8082/tcp | AdGuard Admin | Admin UI | Repo-Soll: nur `100.80.98.33:8082`, DNS-Port 53 unveraendert |
|
||||||
|
| 8181/tcp | InfluxDB 3 Core | LAN Writer fuer Home Assistant | LAN-only, Bind-IP pruefen |
|
||||||
|
|
||||||
|
Pruefkommando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ss -ltnp | sort -k4
|
||||||
|
docker ps --format "{{.Names}}: {{.Ports}}" | sort
|
||||||
|
```
|
||||||
|
|
||||||
|
## Netztrennung
|
||||||
|
|
||||||
|
| Netz | Status | Bemerkung |
|
||||||
|
|---|---|---|
|
||||||
|
| LAN | 192.168.178.0/24 | Hauptnetz, Host `192.168.178.58` |
|
||||||
|
| Gast-WLAN | TBD | Zugriff auf AdGuard Admin muss ausgeschlossen sein |
|
||||||
|
| IoT-Netz | TBD | Zugriff auf AdGuard Admin muss ausgeschlossen sein |
|
||||||
|
| Tailscale | aktiv | Operator-Zugang, Host-IP `100.80.98.33` |
|
||||||
|
| VLANs | TBD | Router-/Switch-Faehigkeit pruefen |
|
||||||
|
|
||||||
|
## Docker-Netze
|
||||||
|
|
||||||
|
Authoritativ ist `HOMELAB_ARCHITECTURE_MASTER_V2.md`. Dieses Inventar haelt nur den Laufzeit-Snapshot fest.
|
||||||
|
|
||||||
|
| Docker-Netz | Zweck | Erwartung |
|
||||||
|
|---|---|---|
|
||||||
|
| frontend_net | Traefik/Web | external bridge |
|
||||||
|
| backend_net | DB/Cache intern | internal bridge |
|
||||||
|
| dns_net | AdGuard/Unbound | bridge |
|
||||||
|
| monitoring_net | Observability | compose-intern |
|
||||||
|
| app-interne Netze | Stack-isoliert | nur wenn technisch noetig |
|
||||||
|
|
||||||
|
Pruefkommando:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network ls
|
||||||
|
docker network inspect frontend_net | jq '.[0].Containers | keys'
|
||||||
|
docker network inspect backend_net | jq '.[0].Internal'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Offene Entscheidungen
|
||||||
|
|
||||||
|
| Thema | Status | Naechster Schritt |
|
||||||
|
|---|---|---|
|
||||||
|
| AdGuard Admin nur via Tailscale | live validiert 2026-05-26 | Compose bindet Admin-Port auf `100.80.98.33:8082`; DNS auf Port 53 funktioniert, LAN-Zugriff auf `192.168.178.58:8082` schlaegt fehl |
|
||||||
|
| Gast-/IoT-Zugriff auf Admin-Ports | offen | Router-Regeln pruefen |
|
||||||
|
| IPv6 Exposure | offen | Router und Traefik/Cloudflare pruefen |
|
||||||
|
| Home Assistant InfluxDB Bind | offen | Effektive Listener-Adresse pruefen |
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Aktuelle Restliste - KalliLab CORE
|
||||||
|
|
||||||
|
Stand: 2026-05-17
|
||||||
|
|
||||||
|
Diese Datei ersetzt die alte Sprint-Liste vom 2026-05-16. Die damaligen Backup-, Posture-, Logging- und Hardening-Bloecke sind weitgehend erledigt oder dokumentiert. Sie bleibt nur als kurze Restliste fuer die naechsten bewussten Arbeitspakete bestehen.
|
||||||
|
|
||||||
|
## Erledigt / nicht mehr offen
|
||||||
|
|
||||||
|
- Filebrowser-Hardening: breiter Appdata-Mount ist entfernt; Filebrowser mountet nur noch Documents, Photos, Projekte und eigenen App-State.
|
||||||
|
- Authelia Argon2id-Haertung: `iterations: 3`, `memory: 65536`, `parallelism: 4`, `key_length: 32`, `salt_length: 16` sind gesetzt.
|
||||||
|
- Gitea Webhook-Allowlist: `GITEA__webhook__ALLOWED_HOST_LIST` ist auf `komodo-core,localhost,127.0.0.1,192.168.178.0/24` eingeschraenkt.
|
||||||
|
- Backup-Konsistenz: SQLite-/Nextcloud-Dumps, Borg-Scope fuer Nextcloud-Daten, Restore-Matrix und Freshness-Checks sind umgesetzt.
|
||||||
|
- Posture-/Cert-/Drift-Checks: Skripte und Unraid User Scripts sind vorhanden und geplant.
|
||||||
|
- Monitoring-Zielstack: `monitoring/` buendelt Prometheus, Loki, Promtail, Grafana, node-exporter, cAdvisor und InfluxDB 3 Core im Repo-Zielzustand.
|
||||||
|
- Docker-Log-Rotation: Unraid-native Rotation ist dokumentiert; keine separate `/etc/docker/daemon.json` setzen.
|
||||||
|
- Disk1-NTFS-Migration Phase 2: am 2026-05-25 abgeschlossen; Disk1 ist XFS, `posture-check` akzeptiert NTFS nicht mehr als Standard.
|
||||||
|
|
||||||
|
## Morgen / bewusst spaeter
|
||||||
|
|
||||||
|
- Monitoring live finalisieren:
|
||||||
|
- Secrets `monitoring_grafana_admin_password.txt`, `monitoring_grafana_influxdb_token.txt`, `influxdb3_admin_token.json` auf dem Host pruefen/anlegen
|
||||||
|
- `monitoring` in Komodo deployen
|
||||||
|
- alte Stacks `ops/loki` und `ops/grafana-influxdb` nach erfolgreichem Smoke-Test stoppen
|
||||||
|
- `https://monitoring.kaleschke.info`, Prometheus Targets, Loki-Logs und InfluxDB-Datasource pruefen
|
||||||
|
- Home Assistant -> InfluxDB finalisieren:
|
||||||
|
- HA-Token/Writer final pruefen
|
||||||
|
- erste Messwerte in InfluxDB verifizieren
|
||||||
|
- Grafana-HA-/Wetter-Dashboard in `monitoring-grafana` aufbauen
|
||||||
|
- Hermes VM-Seite:
|
||||||
|
- Runner-VM, echte `.env`, SSH-Key und Dashboard/Gateway final zusammenfuehren
|
||||||
|
- NAS-Stack erst starten, wenn VM-Seite bereit ist
|
||||||
|
|
||||||
|
## Verbleibende bekannte Warnings
|
||||||
|
|
||||||
|
- `ddns-updater`, `glances`, `scrutiny`: nutzen noch `latest...@sha256`; spaeter durch konkrete Versionstags ersetzen, sofern upstream sinnvoll versioniert.
|
||||||
|
- `ops/grafana-influxdb` und `ops/loki`: bleiben nur noch als Rollback-/Migrationsreferenz im Repo, nach Live-Migration nicht parallel betreiben.
|
||||||
|
- `scrutiny`: bleibt `privileged: true`; dokumentierte SMART-Ausnahme, spaeter erneut pruefen.
|
||||||
|
|
||||||
|
## Regel
|
||||||
|
|
||||||
|
Neue Arbeit erst starten, wenn klar ist, ob sie eines der drei Morgen-Themen betrifft oder eine der bekannten Warnings bewusst abbaut.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Recovery Handoff - KalliLab CORE - 2026-05-15
|
||||||
|
|
||||||
|
Zweck: Startpunkt fuer einen neuen Chat, ohne das komplette Repo erneut zu lesen.
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
- Incident: NTFS-Cache-Vorfall ab 2026-05-11.
|
||||||
|
- Host: Unraid `Kallilabcore`, SSH `root@192.168.178.58`.
|
||||||
|
- Root Cause: Cache war NTFS/ntfs3; Disk1 ist noch NTFS/ntfs3 und wird spaeter separat migriert.
|
||||||
|
- Recovery-Prinzip: `docs/STORAGE_LAYOUT.draft.md` ist fuer diesen Restore bindend, obwohl die Datei noch `.draft` heisst.
|
||||||
|
- Keine Stacks starten, wenn ein Pfad/Setting gegen Storage Layout, Restore Matrix oder Architecture Master verstoesst.
|
||||||
|
|
||||||
|
## Host-Zustand
|
||||||
|
|
||||||
|
- Cache wurde erfolgreich von NTFS auf XFS neu formatiert.
|
||||||
|
- Verifiziert: `/mnt/cache` ist XFS auf `/dev/nvme0n1p1`.
|
||||||
|
- Disk1 bleibt vorerst NTFS auf `/mnt/disk1`; Migration ist Phase 2 nach stabilem Cache-Betrieb.
|
||||||
|
- Docker und Libvirt wurden nach dem Format wieder gestoppt.
|
||||||
|
- `/mnt/user/appdata` ist leer bzw. nur Basisverzeichnis; produktive Appdaten sind noch nicht restored.
|
||||||
|
- Share-Settings wurden nach Storage Layout korrigiert:
|
||||||
|
- `appdata`, `system`, `domains`: cache `only`
|
||||||
|
- `services`, `documents`, `photos`, `backups`, `media`, `finance`, `projekte`: cache `no`, include `disk1`
|
||||||
|
- `isos`: cache `yes`
|
||||||
|
- Backup alter Share-Configs: `/boot/config/shares.bak-20260515-pre-storage-layout`
|
||||||
|
|
||||||
|
## Image und Backups
|
||||||
|
|
||||||
|
- Full NVMe image liegt auf Windows `H:\kallilab-recovery\2026-05-14\nvme0n1-full-20260514.img`.
|
||||||
|
- `dd` exit code war `0`; Image-Groesse/Padding geprueft; Source-Raw-Hash war fertig.
|
||||||
|
- Image-Data-Hash wurde aus Zeitgruenden bewusst abgebrochen. Risiko wurde als ca. 1-3 Prozent eingeschaetzt.
|
||||||
|
- Hetzner-Borg-Archiv `Taegliche-Sicherung-2026-05-10T04:30:52.050` wurde als lesbare Recovery-Quelle verifiziert.
|
||||||
|
- Verifiziert wurden u. a. Vaultwarden SQLite, Gitea SQLite, Postgres-Dumps und Komodo Mongo-Archiv-Header.
|
||||||
|
- Lokaler Verify-Auszug liegt unter `H:\kallilab-recovery\2026-05-14\borg-verify-may10`.
|
||||||
|
|
||||||
|
## Entscheidungen seit dem Cache-Rebuild
|
||||||
|
|
||||||
|
- WD MyBookLive Duo wird komplett aus dem Setup entfernt.
|
||||||
|
- Backrest wird komplett aus dem aktiven Setup entfernt.
|
||||||
|
- Borg ist alleinige Backup-Technologie.
|
||||||
|
- Appdata Backup Plugin bleibt deaktiviert; WD-Ziele wurden aus aktiver Host-Konfiguration geleert.
|
||||||
|
- Unassigned Devices SMB-Remote fuer `//MYBOOKLIVEDUO/Public` wurde aus aktiver Host-Konfiguration entfernt.
|
||||||
|
- Backrest User Script `check_backrest_hetzner` wurde aus Schedule/Cron entfernt.
|
||||||
|
- Host-Konfig-Backup fuer diese Bereinigung: `/boot/config/cleanup-backup-20260515-remove-wd-backrest`
|
||||||
|
|
||||||
|
## Repo-Aenderungen im aktuellen Arbeitsbaum
|
||||||
|
|
||||||
|
Backrest wurde aus dem aktiven Zielbild entfernt:
|
||||||
|
|
||||||
|
- `ops/backrest/docker-compose.yml` geloescht
|
||||||
|
- `HOMELAB_ARCHITECTURE_MASTER_V2.md` aktualisiert
|
||||||
|
- `docs/REPO_MAP.md` aktualisiert
|
||||||
|
- `docs/SERVICE_CATALOG.md` aktualisiert
|
||||||
|
- `docs/RESTORE_MATRIX.md` aktualisiert
|
||||||
|
- `docs/AI_CONTEXT.md` aktualisiert
|
||||||
|
- `docs/DISASTER_RECOVERY.md` aktualisiert
|
||||||
|
- `ops/borg-ui/BACKUP_SCOPE.md` aktualisiert
|
||||||
|
- `ops/hermes-agent/services.json` aktualisiert
|
||||||
|
- `ops/hermes-agent/services.yaml` aktualisiert
|
||||||
|
- `ops/policy-checks/last-report.md` aktualisiert
|
||||||
|
|
||||||
|
Verifikation:
|
||||||
|
|
||||||
|
- `rg "/mnt/(cache|disk1|disks|remotes)" -g docker-compose.yml -g compose.yaml -g *.yml -g *.yaml` findet keine aktiven Compose/YAML-Treffer.
|
||||||
|
- `rg "ops/backrest|backrest.kaleschke|/mnt/user/appdata/backrest|192.168.178.86|MYBOOKLIVEDUO|WD-DUO"` findet nur historische/gewollte Hinweise.
|
||||||
|
- `python -m json.tool ops/hermes-agent/services.json` ok.
|
||||||
|
- `ops/hermes-agent/services.yaml` YAML ok.
|
||||||
|
- `ops/policy-checks/check_repo.ps1` ok: 29 Compose-Dateien, 0 Critical, 4 Warnings.
|
||||||
|
|
||||||
|
## Wichtigste Stop-Regeln
|
||||||
|
|
||||||
|
- Keine Container starten, solange Core-Pfade oder Share-Settings nicht gegen Storage Layout geprueft sind.
|
||||||
|
- Keine Backrest-/WD-Referenzen reaktivieren.
|
||||||
|
- Keine Bind-Mounts auf `/mnt/cache`, `/mnt/disk1`, `/mnt/disks`, `/mnt/remotes`.
|
||||||
|
- Keine Schreibaktionen auf Disk1 ausser bewusst noetig; Disk1 ist noch NTFS.
|
||||||
|
- Komodo nur gemeinsam und explizit anfassen.
|
||||||
|
- Erst Daten/Secrets restoren, dann Stacks einzeln starten und smoke-testen.
|
||||||
|
|
||||||
|
## Naechster sinnvoller Schritt
|
||||||
|
|
||||||
|
1. Repo-Aenderungen kurz reviewen und committen/pushen, bevor Komodo wieder produktiv wird.
|
||||||
|
2. DNS-Basis wiederherstellen:
|
||||||
|
- AdGuard: `/mnt/user/appdata/adguard/conf` aus Borg oder Image restoren; `work` kann frisch sein.
|
||||||
|
- Unbound: `/mnt/user/appdata/unbound/config` aus Borg oder Image restoren.
|
||||||
|
- Danach nur AdGuard + Unbound starten und DNS testen.
|
||||||
|
3. Danach Traefik + Authelia + Gitea/Vaultwarden in kleinen Schritten.
|
||||||
|
|
||||||
|
## Startprompt fuer neuen Chat
|
||||||
|
|
||||||
|
Lies zuerst `docs/RECOVERY_HANDOFF_2026-05-15.md`, dann `docs/STORAGE_LAYOUT.draft.md`, `docs/RESTORE_MATRIX.md` und nur die Compose-Dateien des naechsten betroffenen Stacks. Fuehre den KalliLab-CORE-Restore token-sparend fort. Nichts erfinden, keine Container starten, wenn etwas gegen Storage Layout verstoesst. Backrest und WD MyBookLive Duo sind entfernt und duerfen nicht wieder ins Setup.
|
||||||
+52
-36
@@ -1,6 +1,6 @@
|
|||||||
# Repository Map
|
# Repository Map
|
||||||
|
|
||||||
Stand: 2026-05-04
|
Stand: 2026-05-23
|
||||||
|
|
||||||
Diese Datei ist eine technische Landkarte des Repositories. Sie wurde aus Markdown-Dokumenten, `docker-compose.yml`-Dateien, Env-Beispielen, Traefik-Dynamic-Configs, Komodo/Periphery-Dateien und Skripten abgeleitet. Sie beschreibt den Repo-Sollzustand, nicht zwingend den Live-Zustand auf dem Host.
|
Diese Datei ist eine technische Landkarte des Repositories. Sie wurde aus Markdown-Dokumenten, `docker-compose.yml`-Dateien, Env-Beispielen, Traefik-Dynamic-Configs, Komodo/Periphery-Dateien und Skripten abgeleitet. Sie beschreibt den Repo-Sollzustand, nicht zwingend den Live-Zustand auf dem Host.
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
|||||||
| `host-services/` | Host-nahe Dienste mit direkten Ports oder Host-Netz |
|
| `host-services/` | Host-nahe Dienste mit direkten Ports oder Host-Netz |
|
||||||
| `infra/` | technische Infrastruktur wie PostgreSQL, Redis, DDNS |
|
| `infra/` | technische Infrastruktur wie PostgreSQL, Redis, DDNS |
|
||||||
| `ops/` | Operations-, Backup-, Monitoring- und Admin-Tools |
|
| `ops/` | Operations-, Backup-, Monitoring- und Admin-Tools |
|
||||||
|
| `services/` | Host-seitige Betriebsskripte und Recovery-kritische Service-Hilfen |
|
||||||
| `security/` | Identity/Security-Dienste wie Authelia und Vaultwarden |
|
| `security/` | Identity/Security-Dienste wie Authelia und Vaultwarden |
|
||||||
| `traefik/` | Reverse Proxy Compose und dynamic File-Provider-Konfiguration |
|
| `traefik/` | Reverse Proxy Compose und dynamic File-Provider-Konfiguration |
|
||||||
|
|
||||||
@@ -29,8 +30,16 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
|||||||
| `docs/WORKFLOW.md` | GitOps-/No-Drift-Arbeitsregeln |
|
| `docs/WORKFLOW.md` | GitOps-/No-Drift-Arbeitsregeln |
|
||||||
| `docs/GITOPS_DRIFT_RUNBOOK.md` | Pflichtmatrix fuer Git/Gitea/Komodo/Docker/Host-Drift |
|
| `docs/GITOPS_DRIFT_RUNBOOK.md` | Pflichtmatrix fuer Git/Gitea/Komodo/Docker/Host-Drift |
|
||||||
| `docs/DISASTER_RECOVERY.md` | Wiederanlauf nach Host-/Systemausfall |
|
| `docs/DISASTER_RECOVERY.md` | Wiederanlauf nach Host-/Systemausfall |
|
||||||
| `docs/RESTORE_MATRIX.md` | Restore-Quellen und Smoke-Tests je Dienst |
|
| `docs/RESTORE_MATRIX.md` | Restore-Quellen, Dump-Artefakte und Smoke-Tests je Dienst |
|
||||||
| `docs/ROLLBACK.md` | Rueckweg im GitOps-Betrieb |
|
| `docs/SERVICES_RECOVERY.md` | Recovery-kritische `/mnt/user/services`-Pfade, Gitea-Mirror und Komodo-Bootstrap |
|
||||||
|
| `docs/HARDWARE_INVENTORY.md` | Hardware-, Disk-, SMART-, USV- und Strom-Inventar |
|
||||||
|
| `docs/NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netztrennung |
|
||||||
|
| `docs/EXTERNAL_DEPENDENCIES.md` | Externe Provider, Konten, Ausfall-Szenarien und kritische Off-Repo-Abhaengigkeiten |
|
||||||
|
| `docs/CAPACITY_AND_LIFECYCLE.md` | Capacity-Schwellen, Wachstum, Upgrade-Trigger und Restore-Zeitziele |
|
||||||
|
| `docs/FAMILY_ONBOARDING.md` | Familienorientierte Nutzungsdoku ohne Operator-Details |
|
||||||
|
| `docs/AUDIT_2026-05-25_TODO.md` | Operative Arbeitsliste aus dem Audit vom 2026-05-25; Authelia-2FA bewusst geparkt |
|
||||||
|
| `docs/ALERTING_MAP.md` | ntfy Topic-Konvention und Sender-Mapping fuer Homelab-Alerts |
|
||||||
|
| `docs/ROLLBACK.md` | Rueckweg bei Fehlern im GitOps-Betrieb |
|
||||||
| `docs/SECRETS_MAP.md` | Secret-Namen, Pfade und Einbindungsarten ohne Werte |
|
| `docs/SECRETS_MAP.md` | Secret-Namen, Pfade und Einbindungsarten ohne Werte |
|
||||||
| `docs/HOME_ASSISTANT_INFLUXDB_ECOWITT.md` | Home Assistant -> InfluxDB 3 -> Grafana Ablauf |
|
| `docs/HOME_ASSISTANT_INFLUXDB_ECOWITT.md` | Home Assistant -> InfluxDB 3 -> Grafana Ablauf |
|
||||||
| `docs/AI_CONTEXT.md` | Gesamtverstaendnis fuer KI-Agenten |
|
| `docs/AI_CONTEXT.md` | Gesamtverstaendnis fuer KI-Agenten |
|
||||||
@@ -44,12 +53,19 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
|||||||
| `traefik/dynamic/dashboards.yml` | leer; File-Provider-Platzhalter |
|
| `traefik/dynamic/dashboards.yml` | leer; File-Provider-Platzhalter |
|
||||||
| `traefik/dynamic/tls.yml` | leer; File-Provider-Platzhalter |
|
| `traefik/dynamic/tls.yml` | leer; File-Provider-Platzhalter |
|
||||||
| `security/authelia/configuration.yml` | versionierte Authelia-Baseline fuer nicht geheime ACL-/Session-/Storage-Einstellungen; manuelle Host-Merge-Pflicht, User-Daten, OIDC-Client-Konfiguration und Secret-Werte bleiben ausserhalb von Git |
|
| `security/authelia/configuration.yml` | versionierte Authelia-Baseline fuer nicht geheime ACL-/Session-/Storage-Einstellungen; manuelle Host-Merge-Pflicht, User-Daten, OIDC-Client-Konfiguration und Secret-Werte bleiben ausserhalb von Git |
|
||||||
| `ops/grafana-influxdb/provisioning/datasources/influxdb.yml` | Grafana Datasource Provisioning fuer InfluxDB 3 Core |
|
| `monitoring/prometheus/prometheus.yml` | Prometheus Scrape-Konfiguration fuer dedizierten Monitoring-Stack |
|
||||||
| `ops/borg-ui/scripts/pre-backup-dumps.sh` | Host-seitiges Dump-Skript fuer PostgreSQL, Mealie, Immich und Komodo Mongo |
|
| `monitoring/loki/loki-config.yml` | Loki Filesystem/Retention-Konfiguration fuer dedizierten Monitoring-Stack |
|
||||||
|
| `monitoring/promtail/promtail-config.yml` | Promtail Docker-Socket-Discovery fuer dedizierten Monitoring-Stack |
|
||||||
|
| `monitoring/grafana/provisioning/*` | Grafana Datasource-/Dashboard-Provisioning fuer Prometheus und Loki |
|
||||||
|
| `ops/glance/config/glance.yml` | Glance Dashboard-Konfiguration fuer Homelab-Monitore, Internet-/DNS-/VPN-Widgets, Community-Widgets, Docker-Containergruppen, Zeitfortschritt, Host-Snapshot, Bookmarks und zweite Infrastruktur-Seite |
|
||||||
|
| `ops/borg-ui/scripts/pre-backup-dumps.sh` | Host-seitiges Dump-Skript fuer PostgreSQL, SQLite-Container-Dumps und Komodo Mongo |
|
||||||
|
| `services/posture-check/posture-check.sh` | Host-seitiger Posture-Check fuer Filesystem, Mover-Drift, NVMe-SMART, Fuellstand und ntfy-Alarmierung |
|
||||||
|
| `services/posture-check/docker-critical-events.sh` | Host-seitiger Docker-Event-Watcher fuer kritische ntfy-Alarme |
|
||||||
|
| `services/posture-check/posture_check.sh` | Kompatibilitaets-Wrapper fuer die Schreibweise aus `STORAGE_LAYOUT.draft.md` |
|
||||||
| `ops/hermes-agent/config/hermes/config.yaml` | Hermes Agent Konfiguration mit Env-Platzhaltern |
|
| `ops/hermes-agent/config/hermes/config.yaml` | Hermes Agent Konfiguration mit Env-Platzhaltern |
|
||||||
| `ops/hermes-agent/hermes.env.example` | Beispiel fuer Hermes `.env`; echte Datei liegt auf Host-Appdata |
|
| `ops/hermes-agent/hermes.env.example` | Beispiel fuer Hermes `.env`; echte Datei liegt auf Host-Appdata |
|
||||||
| `ops/hermes-agent/stack.env.example` | Beispiel fuer Hermes Stack-ENV; echte `stack.env` bleibt host-/komodoseitig und ist per `.gitignore` ausgeschlossen |
|
| `ops/hermes-agent/stack.env.example` | Beispiel fuer Hermes Stack-ENV; echte `stack.env` bleibt host-/komodoseitig und ist per `.gitignore` ausgeschlossen |
|
||||||
| `ops/grafana-influxdb/stack.env.example` | `INFLUXDB_BIND_IP` Default `127.0.0.1`; auf Unraid fuer Home Assistant auf LAN-IP setzen |
|
| `monitoring/stack.env.example` | `INFLUXDB_BIND_IP` Default `127.0.0.1`; im Zielzustand fuer Home Assistant auf LAN-IP setzen |
|
||||||
| `ops/komodo/stack.env.example` | Komodo Stack-ENV-Beispiel, Secret-Werte nicht enthalten |
|
| `ops/komodo/stack.env.example` | Komodo Stack-ENV-Beispiel, Secret-Werte nicht enthalten |
|
||||||
|
|
||||||
## Stack-Inventar
|
## Stack-Inventar
|
||||||
@@ -59,7 +75,6 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
|||||||
| Stack | Compose | Services / Images | Traefik Hosts | Networks | Ports | Abhaengigkeiten |
|
| Stack | Compose | Services / Images | Traefik Hosts | Networks | Ports | Abhaengigkeiten |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| BentoPDF | `apps/bentopdf/docker-compose.yml` | `bentopdf` -> `bentopdfteam/bentopdf:2.8.4` | `pdf.kaleschke.info` | `frontend_net` | keine | Traefik + Authelia; COOP/COEP Middleware |
|
| BentoPDF | `apps/bentopdf/docker-compose.yml` | `bentopdf` -> `bentopdfteam/bentopdf:2.8.4` | `pdf.kaleschke.info` | `frontend_net` | keine | Traefik + Authelia; COOP/COEP Middleware |
|
||||||
| Homepage | `apps/homepage/docker-compose.yml` | `homepage` -> `ghcr.io/gethomepage/homepage:latest@sha256:...` | `home.kaleschke.info` | `frontend_net` | keine | Docker-Socket read-only fuer Widgets; viele `HOMEPAGE_VAR_*` Env-Keys |
|
|
||||||
| Immich | `apps/immich/docker-compose.yml` | `immich-server`, `immich-machine-learning`, `database`, `redis` | `immich.kaleschke.info` | `frontend_net`, `immich_default` | keine | `immich-server` depends on `database`, `redis` |
|
| Immich | `apps/immich/docker-compose.yml` | `immich-server`, `immich-machine-learning`, `database`, `redis` | `immich.kaleschke.info` | `frontend_net`, `immich_default` | keine | `immich-server` depends on `database`, `redis` |
|
||||||
| Mail Archiver | `apps/mail-archiver/docker-compose.yml` | `mail-archiver` -> `s1t5/mailarchiver@sha256:...` | `mail.kaleschke.info` | `frontend_net`, `backend_net` | keine | shared PostgreSQL via env connection string; Internet fuer IMAP |
|
| Mail Archiver | `apps/mail-archiver/docker-compose.yml` | `mail-archiver` -> `s1t5/mailarchiver@sha256:...` | `mail.kaleschke.info` | `frontend_net`, `backend_net` | keine | shared PostgreSQL via env connection string; Internet fuer IMAP |
|
||||||
| Mealie | `apps/mealie/docker-compose.yml` | `mealie`, `mealie-postgres` | `mealie.kaleschke.info` | `frontend_net`, `mealie_internal` | keine | eigene PostgreSQL im internen Netz |
|
| Mealie | `apps/mealie/docker-compose.yml` | `mealie`, `mealie-postgres` | `mealie.kaleschke.info` | `frontend_net`, `mealie_internal` | keine | eigene PostgreSQL im internen Netz |
|
||||||
@@ -73,35 +88,35 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
|||||||
|
|
||||||
| Stack | Compose | Services / Images | Traefik Hosts | Networks | Ports | Abhaengigkeiten |
|
| Stack | Compose | Services / Images | Traefik Hosts | Networks | Ports | Abhaengigkeiten |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| Gitea | `core/gitea/docker-compose.yml` | `gitea` -> `docker.gitea.com/gitea:1.25.4@sha256:...` | `git.kaleschke.info` | `frontend_net` | `222:22/tcp` | SQLite in `/data`; SSH-Port ist dokumentierte Ausnahme |
|
| Gitea | `core/gitea/docker-compose.yml` | `gitea` -> `docker.gitea.com/gitea:1.25.4@sha256:...` | `git.kaleschke.info` | `frontend_net` | `222:22/tcp` | SQLite in `/data`; SSH-Port ist dokumentierte Ausnahme; `github.com` ist als Mirror-Ziel erlaubt und externe DNS-Resolver sind gesetzt |
|
||||||
| Authelia | `security/authelia/docker-compose.yml` | `authelia` -> `authelia/authelia:latest@sha256:...` | `auth.kaleschke.info` | `frontend_net`, `backend_net` | keine | PostgreSQL 17 Storage, Traefik ForwardAuth; bewusst ohne Redis-Session-Backend |
|
| Authelia | `security/authelia/docker-compose.yml` | `authelia` -> `authelia/authelia:latest@sha256:...` | `auth.kaleschke.info` | `frontend_net`, `backend_net` | keine | PostgreSQL 17 Storage, Traefik ForwardAuth; bewusst ohne Redis-Session-Backend |
|
||||||
| Vaultwarden | `security/vaultwarden/docker-compose.yml` | `vaultwarden` -> `vaultwarden/server:latest@sha256:...` | `vault.kaleschke.info` | `frontend_net` | keine | Datei-Persistenz, `ADMIN_TOKEN_FILE` |
|
| Vaultwarden | `security/vaultwarden/docker-compose.yml` | `vaultwarden` -> `vaultwarden/server:latest@sha256:...` | `vault.kaleschke.info` | `frontend_net` | keine | Datei-Persistenz, `ADMIN_TOKEN_FILE` |
|
||||||
| ddns-updater | `infra/ddns-updater/docker-compose.yml` | `ddns-updater` -> `ghcr.io/qdm12/ddns-updater:latest@sha256:...` | keine | `frontend_net` | keine | Cloudflare/API-Internetbedarf |
|
| ddns-updater | `infra/ddns-updater/docker-compose.yml` | `ddns-updater` -> `ghcr.io/qdm12/ddns-updater:latest@sha256:...` | keine | `frontend_net` | keine | Cloudflare/API-Internetbedarf |
|
||||||
| PostgreSQL 17 | `infra/postgresql17/docker-compose.yml` | `postgresql17` -> `postgres:17.9@sha256:...` | keine | `backend_net` | keine | shared DB-Cluster |
|
| PostgreSQL 17 | `infra/postgresql17/docker-compose.yml` | `postgresql17` -> `postgres:17.9@sha256:...` | keine | `backend_net` | keine | shared DB-Cluster |
|
||||||
| Redis | `infra/redis/docker-compose.yml` | `Redis` -> `redis:7-alpine` | keine | `backend_net` | keine | shared Cache, Passwort-Datei |
|
| Redis | `infra/redis/docker-compose.yml` | `Redis` -> `redis:7.4-alpine@sha256:...` | keine | `backend_net` | keine | shared Cache, Passwort-Datei |
|
||||||
|
|
||||||
### Host Services
|
### Host Services
|
||||||
|
|
||||||
| Stack | Compose | Services / Images | Hosts | Networks | Ports / Mode | Abhaengigkeiten |
|
| Stack | Compose | Services / Images | Hosts | Networks | Ports / Mode | Abhaengigkeiten |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| AdGuard Home | `host-services/Adguard/docker-compose.yml` | `adguard` -> `adguard/adguardhome:v0.107.52` | keine Traefik-Route | `dns_net`, `frontend_net` | `53/tcp`, `53/udp`, `8082:80/tcp` | Unbound in `dns_net`; direkte Ports sind dokumentierte Ausnahme |
|
| AdGuard Home | `host-services/Adguard/docker-compose.yml` | `adguard` -> `adguard/adguardhome:v0.107.52` | keine Traefik-Route | `dns_net`, `frontend_net` | `53/tcp`, `53/udp`, `100.80.98.33:8082:80/tcp` | Unbound in `dns_net`; DNS-Port 53 ist direkte Ausnahme; Admin 8082 ist auf Tailscale-IP begrenzt |
|
||||||
|
| Plex | `host-services/plex/docker-compose.yml` | `plex` -> `plexinc/pms-docker:1.43.1.10611-1e34174b1@sha256:...` | keine Traefik-Route | `network_mode: host` | host network | Medienserver; Host-Netz bleibt fuer Discovery / Plex GDM dokumentierte Ausnahme |
|
||||||
| Tailscale | `host-services/tailscale/docker-compose.yml` | `Tailscale-Docker` -> `tailscale/tailscale:stable@sha256:...` | keine | `network_mode: host` | host network | VPN/Remote-Zugang |
|
| Tailscale | `host-services/tailscale/docker-compose.yml` | `Tailscale-Docker` -> `tailscale/tailscale:stable@sha256:...` | keine | `network_mode: host` | host network | VPN/Remote-Zugang |
|
||||||
|
|
||||||
### Operations
|
### Operations
|
||||||
|
|
||||||
| Stack | Compose | Services / Images | Traefik Hosts | Networks | Ports | Abhaengigkeiten |
|
| Stack | Compose | Services / Images | Traefik Hosts | Networks | Ports | Abhaengigkeiten |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| Backrest | `ops/backrest/docker-compose.yml` | `backrest` -> `ghcr.io/garethgeorge/backrest:latest@sha256:...` | `backrest.kaleschke.info` | `frontend_net`, `backend_net` | keine | breite Backup-Mounts, SSH/Repo-Creds im Appdata |
|
|
||||||
| Borg UI | `ops/borg-ui/docker-compose.yml` | `borg-ui` -> `ainullcode/borg-ui:latest@sha256:...` | `borg.kaleschke.info` | `frontend_net` | keine | Borg repo, Dump-Scope, Restore-Ziel |
|
| Borg UI | `ops/borg-ui/docker-compose.yml` | `borg-ui` -> `ainullcode/borg-ui:latest@sha256:...` | `borg.kaleschke.info` | `frontend_net` | keine | Borg repo, Dump-Scope, Restore-Ziel |
|
||||||
| code-server | `ops/code-server/docker-compose.yml` | `code-server` -> `lscr.io/linuxserver/code-server:latest@sha256:...` | `code.kaleschke.info` | `frontend_net` | keine | Passwort-Datei, Workspace-Mounts |
|
| code-server | `ops/code-server/docker-compose.yml` | `code-server` -> `lscr.io/linuxserver/code-server:4.116.0@sha256:...` | `code.kaleschke.info` | `frontend_net` | keine | Passwort-Datei, Workspace-Mounts |
|
||||||
| Filebrowser | `ops/filebrowser/docker-compose.yml` | `filebrowser` -> `filebrowser/filebrowser:latest@sha256:...` | `files.kaleschke.info` | `frontend_net` | keine | Appdata-Mount, Admin-UI hinter Authelia |
|
| Filebrowser | `ops/filebrowser/docker-compose.yml` | `filebrowser` -> `filebrowser/filebrowser:v2.63.2@sha256:...` | `files.kaleschke.info` | `frontend_net` | keine | Documents/Photos/Projekte-Mounts, Admin-UI hinter Authelia |
|
||||||
|
| Glance | `ops/glance/docker-compose.yml` | `glance` -> `glanceapp/glance:v0.8.4`, `glance-docker-socket-proxy` -> `tecnativa/docker-socket-proxy:v0.4.2` | `glance.kaleschke.info` | `frontend_net`, `glance_socket_net` | keine | Homelab-Dashboard mit Home- und Infrastructure-Seite, Monitor-, Community-, Docker-, Internet-/DNS-/VPN- und Server-Stats-Widgets; aktives Community-Widget: Immich; Docker-API nur ueber internen Socket-Proxy |
|
||||||
| Glances | `ops/glances/docker-compose.yml` | `glances` -> `nicolargo/glances:latest-full@sha256:...` | `glances.kaleschke.info` | `frontend_net` | keine | Rootfs/Docker-Socket fuer Monitoring |
|
| Glances | `ops/glances/docker-compose.yml` | `glances` -> `nicolargo/glances:latest-full@sha256:...` | `glances.kaleschke.info` | `frontend_net` | keine | Rootfs/Docker-Socket fuer Monitoring |
|
||||||
| Grafana/InfluxDB | `ops/grafana-influxdb/docker-compose.yml` | `grafana`, `influxdb3-core` | `grafana.kaleschke.info` | `frontend_net`, `grafana_influx_internal`, `grafana_influx_lan` | `influxdb3-core`: `${INFLUXDB_BIND_IP:-127.0.0.1}:8181:8181` | InfluxDB LAN-only fuer Home Assistant; Grafana datasource token; beide Container laufen aktuell als `user: "0"` |
|
| Monitoring | `monitoring/docker-compose.yml` | `monitoring-prometheus`, `monitoring-alertmanager`, `monitoring-alertmanager-ntfy-bridge`, `monitoring-blackbox-exporter`, `monitoring-loki`, `monitoring-promtail`, `monitoring-grafana`, `monitoring-node-exporter`, `monitoring-cadvisor`, `monitoring-influxdb3-core`, optional `monitoring-grafana-dashboard-importer` | `monitoring.kaleschke.info` | `frontend_net`, `monitoring_net`, `monitoring_influx_lan` | `monitoring-influxdb3-core`: `${INFLUXDB_BIND_IP:-127.0.0.1}:8181:8181` | zentraler Zielstack fuer Prometheus/Loki/Grafana/InfluxDB; Alertmanager sendet via ntfy-Bridge nach `homelab-alerts`; Blackbox ersetzt Uptime-Kuma-Checks nach Parallelphase; Promtail nutzt Docker socket read-only; Dashboard-Importer nur via `bootstrap`-Profil |
|
||||||
| Hermes Agent | `ops/hermes-agent/docker-compose.yml` | `hermes-gateway`, `hermes-dashboard` -> local build from Dockerfile | `hermes.kaleschke.info` via `${HERMES_DASHBOARD_HOST}` | `hermes_net`, dashboard zusaetzlich `frontend_net` | `8642` nur expose intern | SSH runner, Home Assistant optional, LLM provider env; Dashboard hinter Authelia |
|
| Hermes Agent | `ops/hermes-agent/docker-compose.yml` | `hermes-gateway`, `hermes-dashboard` -> local build from Dockerfile | `hermes.kaleschke.info` via `${HERMES_DASHBOARD_HOST}` | `hermes_net`, dashboard zusaetzlich `frontend_net` | `8642` nur expose intern | SSH runner, Home Assistant optional, LLM provider env; Dashboard hinter Authelia |
|
||||||
| Komodo | `ops/komodo/docker-compose.yml` | `komodo-core`, `komodo-mongo`, `komodo-periphery` | `komodo.kaleschke.info` | `frontend_net`, `komodo_net` | keine | Mongo, Docker socket, `/mnt/user/services` workspace mount, Gitea DNS override |
|
| Komodo | `ops/komodo/docker-compose.yml` | `komodo-core`, `komodo-mongo`, `komodo-periphery` | `komodo.kaleschke.info` | `frontend_net`, `komodo_net` | keine | Mongo, Docker socket, `/mnt/user/services` workspace mount, Gitea DNS override |
|
||||||
| Scrutiny | `ops/scrutiny/docker-compose.yml` | `scrutiny` -> `ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:...` | `scrutiny.kaleschke.info` | `frontend_net` | keine | `privileged: true`, device mounts fuer SMART |
|
| Scrutiny | `ops/scrutiny/docker-compose.yml` | `scrutiny` -> `ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:...` | `scrutiny.kaleschke.info` | `frontend_net` | keine | `privileged: true`, device mounts fuer SMART |
|
||||||
| Speedtest Tracker | `ops/speedtest/docker-compose.yml` | `speedtest-tracker` -> `lscr.io/linuxserver/speedtest-tracker:latest@sha256:...` | `speedtest.kaleschke.info` | `frontend_net` | keine | App key/admin env, SQLite/config path |
|
| Speedtest Tracker | `ops/speedtest/docker-compose.yml` | `speedtest-tracker` -> `lscr.io/linuxserver/speedtest-tracker:1.13.12@sha256:...` | `speedtest.kaleschke.info` | `frontend_net` | keine | App key/admin env, SQLite/config path |
|
||||||
| Uptime Kuma | `ops/uptime-kuma/docker-compose.yml` | `UptimeKuma` -> `louislam/uptime-kuma:1@sha256:...` | `uptime.kaleschke.info` | `frontend_net` | keine | Monitor-State in Appdata |
|
|
||||||
|
|
||||||
### Traefik
|
### Traefik
|
||||||
|
|
||||||
@@ -114,20 +129,19 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
|||||||
| Host | Service | Zugriff |
|
| Host | Service | Zugriff |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `auth.kaleschke.info` | Authelia | Auth provider / bypass fuer eigene Domain |
|
| `auth.kaleschke.info` | Authelia | Auth provider / bypass fuer eigene Domain |
|
||||||
| `backrest.kaleschke.info` | Backrest | Traefik + Authelia |
|
|
||||||
| `borg.kaleschke.info` | Borg UI | Traefik + Authelia |
|
| `borg.kaleschke.info` | Borg UI | Traefik + Authelia |
|
||||||
| `cloud.kaleschke.info` | Nextcloud | Traefik, native App-Auth |
|
| `cloud.kaleschke.info` | Nextcloud | Traefik, native App-Auth |
|
||||||
| `code.kaleschke.info` | code-server | Traefik + Authelia |
|
| `code.kaleschke.info` | code-server | Traefik + Authelia |
|
||||||
| `files.kaleschke.info` | Filebrowser | Traefik + Authelia |
|
| `files.kaleschke.info` | Filebrowser | Traefik + Authelia |
|
||||||
| `git.kaleschke.info` | Gitea Web | Traefik |
|
| `git.kaleschke.info` | Gitea Web | Traefik |
|
||||||
|
| `glance.kaleschke.info` | Glance | Traefik + Authelia |
|
||||||
| `glances.kaleschke.info` | Glances | Traefik + Authelia |
|
| `glances.kaleschke.info` | Glances | Traefik + Authelia |
|
||||||
| `grafana.kaleschke.info` | Grafana | Traefik + Authelia |
|
|
||||||
| `hermes.kaleschke.info` | Hermes Dashboard | Traefik + Authelia |
|
| `hermes.kaleschke.info` | Hermes Dashboard | Traefik + Authelia |
|
||||||
| `home.kaleschke.info` | Homepage | Traefik + Authelia; faellt in Authelia unter die 1FA-Wildcard-Regel |
|
|
||||||
| `immich.kaleschke.info` | Immich | Traefik, native App-Auth |
|
| `immich.kaleschke.info` | Immich | Traefik, native App-Auth |
|
||||||
| `komodo.kaleschke.info` | Komodo | Traefik, native Komodo-Auth; keine pauschale ForwardAuth |
|
| `komodo.kaleschke.info` | Komodo | Traefik, native Komodo-Auth; keine pauschale ForwardAuth |
|
||||||
| `mail.kaleschke.info` | Mail Archiver | Traefik + Authelia + App-Auth |
|
| `mail.kaleschke.info` | Mail Archiver | Traefik + Authelia + App-Auth |
|
||||||
| `mealie.kaleschke.info` | Mealie | Traefik |
|
| `mealie.kaleschke.info` | Mealie | Traefik |
|
||||||
|
| `monitoring.kaleschke.info` | Monitoring Grafana | Traefik + Authelia |
|
||||||
| `ntfy.kaleschke.info` | ntfy | Traefik |
|
| `ntfy.kaleschke.info` | ntfy | Traefik |
|
||||||
| `paperless.kaleschke.info` | Paperless-ngx | Traefik |
|
| `paperless.kaleschke.info` | Paperless-ngx | Traefik |
|
||||||
| `paperless-gpt.kaleschke.info` | Paperless-GPT | Traefik + Authelia |
|
| `paperless-gpt.kaleschke.info` | Paperless-GPT | Traefik + Authelia |
|
||||||
@@ -135,7 +149,6 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
|||||||
| `scrutiny.kaleschke.info` | Scrutiny | Traefik + Authelia |
|
| `scrutiny.kaleschke.info` | Scrutiny | Traefik + Authelia |
|
||||||
| `speedtest.kaleschke.info` | Speedtest Tracker | Traefik + Authelia |
|
| `speedtest.kaleschke.info` | Speedtest Tracker | Traefik + Authelia |
|
||||||
| `traefik.kaleschke.info` | Traefik Dashboard | Traefik + Authelia |
|
| `traefik.kaleschke.info` | Traefik Dashboard | Traefik + Authelia |
|
||||||
| `uptime.kaleschke.info` | Uptime Kuma | Traefik + Authelia |
|
|
||||||
| `vault.kaleschke.info` | Vaultwarden | Traefik |
|
| `vault.kaleschke.info` | Vaultwarden | Traefik |
|
||||||
|
|
||||||
## Networks
|
## Networks
|
||||||
@@ -146,13 +159,14 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
|||||||
| `backend_net` | external/internal laut Architektur | PostgreSQL 17, Redis, Authelia, Paperless, Mail Archiver, Traefik |
|
| `backend_net` | external/internal laut Architektur | PostgreSQL 17, Redis, Authelia, Paperless, Mail Archiver, Traefik |
|
||||||
| `dns_net` | App-/Host-Netz | AdGuard Home und Unbound |
|
| `dns_net` | App-/Host-Netz | AdGuard Home und Unbound |
|
||||||
| `immich_default` | Compose-intern, `internal: true` | Immich Server, ML, Postgres, Redis |
|
| `immich_default` | Compose-intern, `internal: true` | Immich Server, ML, Postgres, Redis |
|
||||||
| `mealie_internal` | Compose-intern | Mealie und Mealie Postgres |
|
| `mealie_internal` | Compose-intern; Laufzeitname mit Compose-Projektpraefix typischerweise `mealie_mealie_internal` | Mealie und Mealie Postgres |
|
||||||
| `nextcloud_internal` | Compose-intern | Nextcloud, Nextcloud Postgres, Nextcloud Redis |
|
| `nextcloud_internal` | Compose-intern | Nextcloud, Nextcloud Postgres, Nextcloud Redis |
|
||||||
| `grafana_influx_internal` | Compose-intern, `internal: true` | Grafana und InfluxDB |
|
| `monitoring_net` | Compose-/Stack-Netz bridge | Prometheus, Loki, Promtail, Monitoring-Grafana, node-exporter, cAdvisor; Traefik fuer Metrics-Scrape |
|
||||||
| `grafana_influx_lan` | Compose-intern bridge | InfluxDB Host-Port-Publishing fuer LAN Writer |
|
| `monitoring_influx_lan` | Compose-intern bridge | InfluxDB Host-Port-Publishing fuer LAN Writer im zentralen Monitoring-Stack |
|
||||||
|
| `glance_socket_net` | Compose-intern, `internal: true` | Glance und `glance-docker-socket-proxy`; keine Traefik-Anbindung |
|
||||||
| `komodo_net` | Compose-intern, `internal: true` | Komodo Core, Mongo, Periphery |
|
| `komodo_net` | Compose-intern, `internal: true` | Komodo Core, Mongo, Periphery |
|
||||||
| `hermes_net` | Compose-intern bridge | Hermes Gateway/Dashboard |
|
| `hermes_net` | Compose-intern bridge | Hermes Gateway/Dashboard |
|
||||||
| `host` | Host-Netz | Tailscale; Plex historisch ausserhalb Repo |
|
| `host` | Host-Netz | Tailscale; Plex als Repo-Compose-Stack unter `host-services/plex/` |
|
||||||
|
|
||||||
## Volumes und Datenpfade
|
## Volumes und Datenpfade
|
||||||
|
|
||||||
@@ -169,20 +183,19 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
|||||||
| Mealie | `/mnt/user/appdata/mealie/data`, `/mnt/user/appdata/mealie/postgres` |
|
| Mealie | `/mnt/user/appdata/mealie/data`, `/mnt/user/appdata/mealie/postgres` |
|
||||||
| Mail Archiver | `/mnt/user/appdata/mailarchiver/data-protection-keys` |
|
| Mail Archiver | `/mnt/user/appdata/mailarchiver/data-protection-keys` |
|
||||||
| Nextcloud | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data`, `/mnt/user/appdata/nextcloud/postgres`, `/mnt/user/appdata/nextcloud/redis` |
|
| Nextcloud | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data`, `/mnt/user/appdata/nextcloud/postgres`, `/mnt/user/appdata/nextcloud/redis` |
|
||||||
| Homepage | `/mnt/user/appdata/homepage`, `/mnt/user/appdata/homepage/images`, Docker socket read-only |
|
| Plex | `/mnt/user/appdata/plex/config`, `/mnt/user/appdata/plex/transcode`, `/mnt/user/media`, `/mnt/user/photos` |
|
||||||
| ntfy | `/mnt/user/appdata/ntfy` |
|
| ntfy | `/mnt/user/appdata/ntfy` |
|
||||||
| Paperless-GPT | `/mnt/user/appdata/paperless-gpt/data`, `/mnt/user/appdata/paperless-gpt/prompts` |
|
| Paperless-GPT | `/mnt/user/appdata/paperless-gpt/data`, `/mnt/user/appdata/paperless-gpt/prompts` |
|
||||||
| AdGuard | `/mnt/user/appdata/adguard/work`, `/mnt/user/appdata/adguard/conf` |
|
| AdGuard | `/mnt/user/appdata/adguard/work`, `/mnt/user/appdata/adguard/conf` |
|
||||||
| Tailscale | `/mnt/user/appdata/tailscale` |
|
| Tailscale | `/mnt/user/appdata/tailscale` |
|
||||||
| Backrest | `/mnt/user/appdata/backrest/*`, broad `/mnt/user/...` source mounts, remote repo mount |
|
|
||||||
| Borg UI | `/mnt/user/appdata/borg-ui/data`, `/mnt/user/appdata/borg-ui/cache`, `/mnt/user/backups/borg/dumps`, selected restore/source mounts |
|
| Borg UI | `/mnt/user/appdata/borg-ui/data`, `/mnt/user/appdata/borg-ui/cache`, `/mnt/user/backups/borg/dumps`, selected restore/source mounts |
|
||||||
| code-server | `/mnt/user/appdata/code-server`, `/mnt/user/services/dev`, Homepage production mount |
|
| code-server | `/mnt/user/appdata/code-server`, `/mnt/user/services/dev` |
|
||||||
| Filebrowser | `/mnt/user/appdata`, Filebrowser database/config paths |
|
| Filebrowser | `/mnt/user/documents`, `/mnt/user/photos`, `/mnt/user/projekte`, Filebrowser database/config paths |
|
||||||
|
| Glance | Repo-Konfiguration unter `ops/glance/config/glance.yml`; keine produktive Datenpersistenz; Docker-Socket nur am internen Proxy |
|
||||||
| Glances | `/`, Docker socket, `/etc/os-release` |
|
| Glances | `/`, Docker socket, `/etc/os-release` |
|
||||||
| Scrutiny | `/mnt/user/appdata/scrutiny/*`, `/run/udev`, selected `/dev/...` disks |
|
| Scrutiny | `/mnt/user/appdata/scrutiny/*`, `/run/udev`, selected `/dev/...` disks |
|
||||||
| Speedtest | `/mnt/user/appdata/speedtest-tracker/config` |
|
| Speedtest | `/mnt/user/appdata/speedtest-tracker/config` |
|
||||||
| Uptime Kuma | `/mnt/user/appdata/uptime-kuma` |
|
| Monitoring | named volumes `prometheus_data`, `loki_data`, `promtail_positions`, `grafana_data`; InfluxDB-Persistenz unter `/mnt/user/appdata/influxdb3/data` und `/mnt/user/appdata/influxdb3/plugins`; Provisioning im Repo unter `monitoring/grafana/provisioning`; Dashboards unter `monitoring/grafana/dashboards`; historische Altstandsdaten unter `/mnt/user/appdata/grafana`, `/mnt/user/appdata/loki` und `/mnt/user/appdata/alloy` nicht blind loeschen |
|
||||||
| Grafana/InfluxDB | `/mnt/user/appdata/grafana`, Grafana provisioning, `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` |
|
|
||||||
| Hermes Agent | `/mnt/user/appdata/hermes-agent/data`, `/mnt/user/appdata/hermes-agent/ssh`, SSH private key path |
|
| Hermes Agent | `/mnt/user/appdata/hermes-agent/data`, `/mnt/user/appdata/hermes-agent/ssh`, SSH private key path |
|
||||||
| Komodo | `komodo_keys`, `/mnt/user/appdata/komodo/core`, `/mnt/user/appdata/komodo/mongo`, `/mnt/user/appdata/komodo/periphery`, `/mnt/user/services` |
|
| Komodo | `komodo_keys`, `/mnt/user/appdata/komodo/core`, `/mnt/user/appdata/komodo/mongo`, `/mnt/user/appdata/komodo/periphery`, `/mnt/user/services` |
|
||||||
|
|
||||||
@@ -198,33 +211,36 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
|||||||
| Paperless | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS` als Komodo Stack ENV |
|
| Paperless | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS` als Komodo Stack ENV |
|
||||||
| Immich | `IMMICH_DB_PASSWORD` Stack ENV; `immich_postgres_password.txt` fuer Postgres |
|
| Immich | `IMMICH_DB_PASSWORD` Stack ENV; `immich_postgres_password.txt` fuer Postgres |
|
||||||
| Mail Archiver | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` als Stack ENV |
|
| Mail Archiver | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` als Stack ENV |
|
||||||
| Homepage | viele `HOMEPAGE_VAR_*` Stack ENV Keys fuer Tokens/Logins |
|
| Glance | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY` als Stack ENV fuer Community-/Live-Widgets |
|
||||||
| Speedtest | `APP_KEY`, `ADMIN_PASSWORD` als Stack ENV |
|
| Speedtest | `APP_KEY`, `ADMIN_PASSWORD` als Stack ENV |
|
||||||
| Nextcloud | Admin User, Admin Password, Postgres Password via Secret-Dateien |
|
| Nextcloud | Admin User, Admin Password, Postgres Password via Secret-Dateien |
|
||||||
| Komodo | `KOMODO_SECRET_KEY`, `KOMODO_WEBHOOK_SECRET`, `KOMODO_JWT_SECRET`, `KOMODO_MONGO_PASSWORD`, `KOMODO_PERIPHERY_PASSKEY`; Mongo Passwort-Datei |
|
| Komodo | `KOMODO_SECRET_KEY`, `KOMODO_WEBHOOK_SECRET`, `KOMODO_JWT_SECRET`, `KOMODO_MONGO_PASSWORD`, `KOMODO_PERIPHERY_PASSKEY`; Mongo Passwort-Datei |
|
||||||
| Borg UI | Borg Credentials, Admin Login, SSH Keys in persistentem Appdata, nicht im Git |
|
| Borg UI | Borg Credentials, Admin Login, SSH Keys in persistentem Appdata, nicht im Git |
|
||||||
| Hermes Agent | provider keys, API server key, messaging tokens, Home Assistant token in Host `.env`; SSH private key als Host-Secret |
|
| Hermes Agent | provider keys, API server key, messaging tokens, Home Assistant token in Host `.env`; SSH private key als Host-Secret |
|
||||||
| Grafana/InfluxDB | Grafana Admin Password, InfluxDB Admin Token JSON, Grafana Datasource Token |
|
| Monitoring | `monitoring_grafana_admin_password.txt`, `monitoring_grafana_influxdb_token.txt`, `influxdb3_admin_token.json` fuer zentrale Grafana-/InfluxDB-Funktionen; ersetzt Uptime Kuma fuer HTTP-Verfuegbarkeit |
|
||||||
|
|
||||||
## Skripte
|
## Skripte
|
||||||
|
|
||||||
| Skript | Ausfuehrungsort | Zweck |
|
| Skript | Ausfuehrungsort | Zweck |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `ops/borg-ui/scripts/pre-backup-dumps.sh` | Unraid Host, nicht Borg-UI Inline-Hook | erzeugt aktuelle Dumps unter `/mnt/user/backups/borg/dumps/latest` |
|
| `ops/borg-ui/scripts/pre-backup-dumps.sh` | Unraid Host, nicht Borg-UI Inline-Hook | erzeugt aktuelle Dumps unter `/mnt/user/backups/borg/dumps/latest` |
|
||||||
|
| `services/posture-check/posture-check.sh` | Unraid Host | schreibt `/mnt/user/services/posture-check/last.json` und alarmiert via ntfy bei Warning/Critical |
|
||||||
|
| `services/posture-check/docker-critical-events.sh` | Unraid Host | beobachtet Docker `die`/`oom`/`kill` Events und alarmiert via `homelab-alerts` |
|
||||||
|
|
||||||
Das Skript liest Secret-Dateien auf dem Host und schreibt Dump-Artefakte. Bei Analyse niemals Secret-Inhalte ausgeben.
|
Das Skript liest Secret-Dateien auf dem Host und schreibt Dump-Artefakte. Bei Analyse niemals Secret-Inhalte ausgeben.
|
||||||
|
|
||||||
## Unsicherheiten / TODOs aus Repo-Sicht
|
## Unsicherheiten / TODOs aus Repo-Sicht
|
||||||
|
|
||||||
- Echte `stack.env`- und `.env`-Dateien sind per `.gitignore` ausgeschlossen; nur `*.example`-Dateien gehoeren ins Repo.
|
- Echte `stack.env`- und `.env`-Dateien sind per `.gitignore` ausgeschlossen; nur `*.example`-Dateien gehoeren ins Repo.
|
||||||
|
- Hardware-, Netzwerk- und Provider-Inventare sind initiale Templates und muessen nach einem Host-Audit mit echten Werten gefuellt werden.
|
||||||
|
- Authelia-2FA-/OIDC-Aenderungen sind nach Audit 2026-05-25 bewusst geparkt und werden nicht als Sofortmassnahme behandelt.
|
||||||
- Authelia `configuration.yml` ist Repo-Baseline fuer nicht geheime Einstellungen, wird aber nicht automatisch von Komodo auf den Host kopiert. Die produktive Host-Datei kann zusaetzliche OIDC-/Secret-Konfiguration enthalten; Aenderungen muessen manuell gemerged und validiert werden.
|
- Authelia `configuration.yml` ist Repo-Baseline fuer nicht geheime Einstellungen, wird aber nicht automatisch von Komodo auf den Host kopiert. Die produktive Host-Datei kann zusaetzliche OIDC-/Secret-Konfiguration enthalten; Aenderungen muessen manuell gemerged und validiert werden.
|
||||||
- `apps/mealie` nutzt in Compose `mealie_internal`; Architektur-Doku nennt teils `mealie_mealie_internal`. Laufzeitnamen koennen durch Compose-Projektpraefixe abweichen.
|
|
||||||
- `backend_net` ist in der Architektur als `internal: true` beschrieben; einzelne Compose-Dateien referenzieren es external. Live-Netz-Attribute bei Drift-Fragen pruefen.
|
- `backend_net` ist in der Architektur als `internal: true` beschrieben; einzelne Compose-Dateien referenzieren es external. Live-Netz-Attribute bei Drift-Fragen pruefen.
|
||||||
- Einige Images bleiben trotz Digest-Pin semantisch auf mutable Tags (`latest@sha256`, `release@sha256`). Das ist bewusst dokumentiert, aber bei Updates gesondert pruefen.
|
- Einige Images bleiben trotz Digest-Pin semantisch auf mutable Tags (`latest@sha256`, `release@sha256`). Das ist bewusst dokumentiert, aber bei Updates gesondert pruefen.
|
||||||
- Stateful Datenhalter sind seit 2026-05-05 bevorzugt mit Minor-/Patch-Tag plus Digest gepinnt; Redis-Caches bleiben bewusst ohne Digest-Pin.
|
- Stateful Datenhalter sind seit 2026-05-05 bevorzugt mit Minor-/Patch-Tag plus Digest gepinnt; Redis-Caches wurden im Hardening-Sprint 2026-05-16 auf `redis:7.4-alpine@sha256:...` vereinheitlicht.
|
||||||
- `scrutiny` bleibt `privileged: true`; dokumentierte Ausnahme, aber weiterhin pruefenswert.
|
- `scrutiny` bleibt `privileged: true`; dokumentierte Ausnahme, aber weiterhin pruefenswert.
|
||||||
- `tailscale` nutzt Host-Netz, `NET_ADMIN`, `NET_RAW` und `/dev/net/tun` als dokumentierte VPN-Ausnahme.
|
- `tailscale` nutzt Host-Netz, `NET_ADMIN`, `NET_RAW` und `/dev/net/tun` als dokumentierte VPN-Ausnahme.
|
||||||
- `grafana` und `influxdb3-core` laufen aktuell als `user: "0"`; UID/GID-Hardening nur als eigener Sprint.
|
- `monitoring-influxdb3-core` laeuft aktuell als `user: "0"`; UID/GID-Hardening nur als eigener Sprint.
|
||||||
- Leere `.keep`-Platzhalter wurden entfernt; neue Verzeichnisse sollen erst mit konkretem Inhalt ins Repo.
|
- Leere `.keep`-Platzhalter wurden entfernt; neue Verzeichnisse sollen erst mit konkretem Inhalt ins Repo.
|
||||||
- `Plex-Media-Server` ist als historischer Host-Sonderfall dokumentiert, aber nicht als Repo-Compose-Stack enthalten.
|
- `plex` ist als Repo-Compose-Stack unter `host-services/plex/` enthalten; `network_mode: host` bleibt die dokumentierte Discovery-Ausnahme.
|
||||||
- BentoPDF kann je nach Live-Stand vorbereitet statt produktiv sein; Hermes Dashboard ist produktiv unter `hermes.kaleschke.info`.
|
- BentoPDF kann je nach Live-Stand vorbereitet statt produktiv sein; Hermes Dashboard ist produktiv unter `hermes.kaleschke.info`.
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# Restore Handbook - KalliLab CORE
|
||||||
|
|
||||||
|
Stand: 2026-05-07
|
||||||
|
|
||||||
|
Dieses Handbuch ist die praktische Betriebsanleitung fuer Restore-Checks und Restore-Lab in KalliLab CORE.
|
||||||
|
|
||||||
|
Es ergaenzt:
|
||||||
|
|
||||||
|
- `docs/RESTORE_MATRIX.md`
|
||||||
|
- `docs/DISASTER_RECOVERY.md`
|
||||||
|
- `ops/restore-tests/*`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ziel
|
||||||
|
|
||||||
|
Dieses Handbuch beantwortet vier Fragen:
|
||||||
|
|
||||||
|
1. Was ist die Restore-Quelle?
|
||||||
|
2. Wo wird getestet?
|
||||||
|
3. Wie pruefen wir Erfolg?
|
||||||
|
4. Wie machen wir das regelmaessig mit wenig Handarbeit?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Grundmuster
|
||||||
|
|
||||||
|
Alle validierten Restore-Tests folgen demselben Muster:
|
||||||
|
|
||||||
|
- Quelle bleibt das produktive Borg-Repo bei Hetzner
|
||||||
|
- Borg-Zugriff laeuft ueber den vorhandenen `borg-ui`-Container
|
||||||
|
- Passphrase kommt aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||||
|
- Testdaten landen unter `/mnt/user/backups/restore-lab/<dienst>`
|
||||||
|
- Reports landen unter `/mnt/user/backups/restore-reports`
|
||||||
|
- Testinstanzen laufen lokal ohne Traefik und ohne produktive Domain
|
||||||
|
- nach Erfolg werden Testcontainer und Testdaten wieder entfernt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Bereits praktisch verifiziert
|
||||||
|
|
||||||
|
### Vaultwarden
|
||||||
|
|
||||||
|
- Report: `/mnt/user/backups/restore-reports/vaultwarden-2026-05-07.md`
|
||||||
|
- Nachweis:
|
||||||
|
- Borg-Restore erfolgreich
|
||||||
|
- Testcontainer startete
|
||||||
|
- Login-Seite war erreichbar
|
||||||
|
|
||||||
|
### Gitea
|
||||||
|
|
||||||
|
- Report: `/mnt/user/backups/restore-reports/gitea-2026-05-07.md`
|
||||||
|
- Nachweis:
|
||||||
|
- Borg-Restore erfolgreich
|
||||||
|
- Web-UI antwortete
|
||||||
|
- SSH-Port reagierte
|
||||||
|
|
||||||
|
### Paperless
|
||||||
|
|
||||||
|
- Report: `/mnt/user/backups/restore-reports/paperless-2026-05-07.md`
|
||||||
|
- Nachweis:
|
||||||
|
- Borg-Datei-Restore erfolgreich
|
||||||
|
- Paperless-Dump aus Borg importiert
|
||||||
|
- Login-Seite war erreichbar
|
||||||
|
- Test-DB enthielt `25` Dokumente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Verzeichnisstruktur
|
||||||
|
|
||||||
|
### Produktiv
|
||||||
|
|
||||||
|
- `/mnt/user/appdata`
|
||||||
|
- `/mnt/user/services`
|
||||||
|
- `/mnt/user/documents`
|
||||||
|
- `/mnt/user/backups/borg/dumps/latest`
|
||||||
|
|
||||||
|
### Restore-Lab
|
||||||
|
|
||||||
|
- `/mnt/user/backups/restore-lab/vaultwarden`
|
||||||
|
- `/mnt/user/backups/restore-lab/gitea`
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless`
|
||||||
|
|
||||||
|
### Reports
|
||||||
|
|
||||||
|
- `/mnt/user/backups/restore-reports`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Restore-Frequenz
|
||||||
|
|
||||||
|
- jeden Montag, 06:30:
|
||||||
|
- Frische-Check fuer Dumps und Reports
|
||||||
|
- 1. Samstag im Monat, 07:00:
|
||||||
|
- Vaultwarden
|
||||||
|
- 3. Samstag im Monat, 07:00:
|
||||||
|
- Gitea
|
||||||
|
- jeder 2. Monat, 2. Samstag, 08:00:
|
||||||
|
- Paperless
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Betriebsmodi
|
||||||
|
|
||||||
|
### V1
|
||||||
|
|
||||||
|
- validierte Bash-Host-Jobs
|
||||||
|
- Host-Job-Definitionen liegen im Repo
|
||||||
|
- Scheduler kann bereits echte Frische- und Restore-Checks fahren
|
||||||
|
- `ntfy` und Hermes-Auswertung folgen danach
|
||||||
|
|
||||||
|
### V2
|
||||||
|
|
||||||
|
- `ntfy` bei Erfolg/Fehler
|
||||||
|
- Hermes liest Reports und baut Uebersichten
|
||||||
|
- zusaetzliche Rotation, Sammelreports und weitere Dienste
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. User Script Jobs auf Unraid
|
||||||
|
|
||||||
|
Die Vorlagen stehen in:
|
||||||
|
|
||||||
|
- `ops/restore-tests/unraid-user-scripts.md`
|
||||||
|
|
||||||
|
Host-Repo-Pfad:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mnt/user/services/homelab
|
||||||
|
```
|
||||||
|
|
||||||
|
V1-Jobs:
|
||||||
|
|
||||||
|
1. `restore-freshness-weekly`
|
||||||
|
2. `restore-vaultwarden-monthly`
|
||||||
|
3. `restore-gitea-monthly`
|
||||||
|
4. `restore-paperless-bimonthly`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Erfolgskriterien
|
||||||
|
|
||||||
|
Ein Restore-Test gilt nur dann als erfolgreich, wenn:
|
||||||
|
|
||||||
|
- Restore-Quelle lesbar war
|
||||||
|
- Daten im Restore-Lab ankamen
|
||||||
|
- Testcontainer startete
|
||||||
|
- Smoke-Test erfolgreich war
|
||||||
|
- Report geschrieben wurde
|
||||||
|
|
||||||
|
Nur `Container laeuft` reicht nicht.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Sicherheitsregeln
|
||||||
|
|
||||||
|
- keine produktiven Pfade beschreiben
|
||||||
|
- keine produktiven Container fuer Restore-Tests verwenden
|
||||||
|
- keine produktiven Domains fuer Testinstanzen verwenden
|
||||||
|
- keine Secrets im Repo
|
||||||
|
- keine Restore-Automatik fuer neue Dienste ohne bewusste Freigabe
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Schnellstart
|
||||||
|
|
||||||
|
### Frische-Check
|
||||||
|
|
||||||
|
Auf dem Unraid-Host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh freshness
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vaultwarden Restore-Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh vaultwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gitea Restore-Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
### Paperless Restore-Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh paperless
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional mit `ntfy`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-info
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Naechste Ausbaustufen
|
||||||
|
|
||||||
|
1. Vollautomatik fuer Vaultwarden, Gitea und Paperless
|
||||||
|
2. `ntfy`-Meldungen fuer Erfolg/Fehler
|
||||||
|
3. Hermes-Zusammenfassung ueber vorhandene Reports
|
||||||
|
4. naechster Referenz-Restore fuer `mail-archiver` oder `mealie`
|
||||||
+37
-16
@@ -26,15 +26,17 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
|
|
||||||
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
|
| Unraid OS Flash | Borg-Artefakt + optional Unraid Connect | `/boot/config` aus `unraid-flash-config.tar.gz` | `unraid-flash-config.tar.gz`, `.sha256`, Manifest | enthaelt sensible Host-Konfiguration, wie Secret-Material behandeln | Unraid USB Flash Creator / neuer Boot-Stick | Unraid bootet, Array-Zuordnung und Shares sind sichtbar |
|
||||||
| Traefik | Share / Borg | `/mnt/user/appdata/traefik`, besonders `dynamic/`, `letsencrypt`, `secrets` | keine eigene DB | `cloudflare_dns_api_token` | `frontend_net`, `backend_net` | `https://traefik.kaleschke.info` erreichbar, Dashboard ueber Authelia |
|
| Traefik | Share / Borg | `/mnt/user/appdata/traefik`, besonders `dynamic/`, `letsencrypt`, `secrets` | keine eigene DB | `cloudflare_dns_api_token` | `frontend_net`, `backend_net` | `https://traefik.kaleschke.info` erreichbar, Dashboard ueber Authelia |
|
||||||
| AdGuard Home | Share / Borg | `/mnt/user/appdata/adguard/conf` | keine | keine zusaetzlichen Repo-Secrets dokumentiert | `dns_net`, `frontend_net` | DNS-Aufloesung funktioniert |
|
| AdGuard Home | Share / Borg | `/mnt/user/appdata/adguard/conf` | keine | keine zusaetzlichen Repo-Secrets dokumentiert | `dns_net`, `frontend_net` | DNS-Aufloesung funktioniert |
|
||||||
| Tailscale | Share / Borg | `/mnt/user/appdata/tailscale` | keine | Tailscale-State im Pfad | Host-Netz | Tailscale verbunden |
|
| Tailscale | Share / Borg | `/mnt/user/appdata/tailscale` | keine | Tailscale-State im Pfad | Host-Netz | Tailscale verbunden |
|
||||||
| PostgreSQL 17 | Share + Dumps | `/mnt/user/appdata/postgresql17` | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt` | `backend_net` | DB startet, Ziel-Datenbanken vorhanden |
|
| PostgreSQL 17 | Share + Dumps | `/mnt/user/appdata/postgresql17` | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt` | `backend_net` | DB startet, Ziel-Datenbanken vorhanden |
|
||||||
| Redis | Share / Host | `/mnt/user/appdata/redis` | keine | `redis_password.txt` | `backend_net` | Redis startet, Apps verbinden sich |
|
| Redis | Share / Host | `/mnt/user/appdata/redis` | keine | `redis_password.txt` | `backend_net` | Redis startet, Apps verbinden sich |
|
||||||
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 17, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut |
|
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 17, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut |
|
||||||
| Gitea | Borg / Share | `/mnt/user/services/gitea/data` | SQLite in `/data` | keine separaten Secret-Dateien dokumentiert | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert |
|
| Gitea | GitHub-Mirror fuer Repo-Bootstrap, Borg + Dump fuer Gitea-Appstate | `/mnt/user/services/gitea/data` | `gitea.sqlite.dump` | `borg_repo_passphrase.txt` fuer Restore-Tests; GitHub-Push-Mirror-PAT liegt nur in Gitea-Mirror-Settings | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert; GitHub-Push-Mirror synchronisiert ohne `last_error`; Mini-Restore nach `/mnt/user/backups/restore-lab/gitea` am 2026-05-07 erfolgreich validiert |
|
||||||
| Komodo | Borg / Share | `/mnt/user/appdata/komodo/core`, `/mnt/user/appdata/komodo/periphery` | `komodo-mongo.archive.gz` falls verifiziert | `komodo_mongo_password.txt`, `KOMODO_*` Stack ENV | Traefik, Mongo, Gitea | UI erreichbar, Periphery verbunden |
|
| Komodo | Borg / Share | `/mnt/user/appdata/komodo/core`, `/mnt/user/appdata/komodo/periphery`, `/mnt/user/services/stacks` | `komodo-mongo.archive.gz` falls verifiziert | `komodo_mongo_password.txt`, `KOMODO_*` Stack ENV | Traefik, Mongo, Gitea | UI erreichbar, Periphery verbunden |
|
||||||
| Vaultwarden | Borg / Share | `/mnt/user/appdata/vaultwarden` | dateibasiert | `vaultwarden_admin_token.txt` | Traefik | Login-Seite erreichbar, Tresor-Daten sichtbar |
|
| GitOps Host Automation | Borg / Git | `/mnt/user/services/homelab-infra`, `/mnt/user/services/posture-check` | keine eigene DB | keine | Gitea, Komodo, Unraid User Scripts | `posture-check` laeuft vom Host-Pfad und liefert `warning_count: 0` im bekannten Uebergangszustand |
|
||||||
|
| Vaultwarden | Borg + Dump | `/mnt/user/appdata/vaultwarden` | `vaultwarden.sqlite.dump` | `vaultwarden_admin_token.txt`, `borg_repo_passphrase.txt` fuer Restore-Tests | Traefik | Login-Seite erreichbar, Tresor-Daten sichtbar; Mini-Restore nach `/mnt/user/backups/restore-lab/vaultwarden` am 2026-05-07 erfolgreich validiert |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -42,12 +44,12 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
|
|
||||||
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| Paperless-ngx | Borg + Dumps | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/paperless/export`, `/mnt/user/documents/scans_inbox` | `postgresql17-paperless.dump` | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS` | PostgreSQL 17, Redis, Traefik | Web-UI startet, Dokumente vorhanden |
|
| Paperless-ngx | Borg + Dumps | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/paperless/export`, `/mnt/user/documents/scans_inbox` | `postgresql17-paperless.dump` | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS`, `borg_repo_passphrase.txt` fuer Restore-Tests | PostgreSQL 17, Redis, Traefik | Web-UI startet, Dokumente vorhanden; Mini-Restore nach `/mnt/user/backups/restore-lab/paperless` am 2026-05-07 erfolgreich validiert |
|
||||||
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, optional `/mnt/user/appdata/mealie/postgres` bei lokalem Share-Weiterbetrieb | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden |
|
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, optional `/mnt/user/appdata/mealie/postgres` bei lokalem Share-Weiterbetrieb | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden |
|
||||||
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | `immich.dump` | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt` | `immich_postgres`, `immich_redis`, Traefik | UI startet, Medienbibliothek sichtbar |
|
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | `immich.dump` | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt` | `immich_postgres`, `immich_redis`, Traefik | UI startet, Medienbibliothek sichtbar |
|
||||||
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 17, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen |
|
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 17, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen |
|
||||||
| Nextcloud | Borg + Share | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/appdata/nextcloud/postgres`, `/mnt/user/appdata/nextcloud/redis`, `/mnt/user/documents/nextcloud-data` | app-eigene PostgreSQL unter `/mnt/user/appdata/nextcloud/postgres` | `nextcloud_admin_user.txt`, `nextcloud_admin_password.txt`, `nextcloud_postgres_password.txt` | `nextcloud-postgres`, `nextcloud-redis`, Traefik | Web-UI startet, Login funktioniert, Dateien sichtbar |
|
| Nextcloud | Borg + Dump | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | `nextcloud.dump` | `nextcloud_admin_user.txt`, `nextcloud_admin_password.txt`, `nextcloud_postgres_password.txt` | `nextcloud-postgres`, `nextcloud-redis`, Traefik | Web-UI startet, Login funktioniert, Dateien sichtbar |
|
||||||
| Homepage | Borg / Share | `/mnt/user/appdata/homepage` | keine | `HOMEPAGE_VAR_*` | Traefik, Authelia | Dashboard startet, Widgets laden |
|
| Glance | Git / Borg-Repo | Repo-Konfiguration unter `ops/glance/config/glance.yml`; keine kritische Datenpersistenz | keine | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY` | Traefik, Authelia, optional interne API-Ziele | Dashboard startet, Widgets laden, Docker-Status laeuft nur ueber `glance-docker-socket-proxy` |
|
||||||
| ntfy | Borg / Share | `/mnt/user/appdata/ntfy` | keine | keine besonderen Secret-Dateien dokumentiert | Traefik | UI und Push-Endpunkt erreichbar |
|
| ntfy | Borg / Share | `/mnt/user/appdata/ntfy` | keine | keine besonderen Secret-Dateien dokumentiert | Traefik | UI und Push-Endpunkt erreichbar |
|
||||||
| Paperless-GPT | Borg / Share | `/mnt/user/appdata/paperless-gpt` | keine eigene DB | `PAPERLESS_API_TOKEN` | Traefik, Paperless | UI startet, Konfiguration vorhanden |
|
| Paperless-GPT | Borg / Share | `/mnt/user/appdata/paperless-gpt` | keine eigene DB | `PAPERLESS_API_TOKEN` | Traefik, Paperless | UI startet, Konfiguration vorhanden |
|
||||||
|
|
||||||
@@ -57,17 +59,17 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
|
|
||||||
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| Borg UI | Borg / Share | `/mnt/user/appdata/borg-ui/data` | keine eigene DB | Borg-Repo-Creds in `/data` | Traefik | UI startet, Repo-Verbindung bekannt |
|
| Borg UI | Borg + Dump | `/mnt/user/appdata/borg-ui/data` | `borg-ui.sqlite` | Borg-Repo-Creds in `/data` | Traefik | UI startet, Repo-Verbindung bekannt |
|
||||||
| Backrest | Share | `/mnt/user/appdata/backrest/*` | keine | SSH-/Repo-Creds im Mount | Traefik | UI startet |
|
| Filebrowser | Share / Fresh + Dump | `/mnt/user/appdata/filebrowser` | `filebrowser.bolt.dump` | `filebrowser_admin_password.txt` bei Fresh-Rebuild | Traefik, Authelia | UI startet, Admin-User vorhanden |
|
||||||
| Uptime Kuma | Share | `/mnt/user/appdata/uptime-kuma` | keine | keine besonderen Secret-Dateien dokumentiert | Traefik, Authelia | Monitore vorhanden |
|
|
||||||
| Filebrowser | Share | `/mnt/user/appdata/filebrowser` | keine | keine separaten Secret-Dateien dokumentiert | Traefik, Authelia | UI startet |
|
|
||||||
| Glances | Rebuildbar | kein kritischer Zustand | keine | keine | Traefik, Authelia | UI startet |
|
| Glances | Rebuildbar | kein kritischer Zustand | keine | keine | Traefik, Authelia | UI startet |
|
||||||
| Scrutiny | Teilweise rebuildbar | `/mnt/user/appdata/scrutiny` falls gewuenscht | InfluxDB bewusst nicht Teil des Critical-Scope | keine | Traefik, Authelia | UI startet, Laufwerke sichtbar |
|
| Scrutiny | Teilweise rebuildbar | `/mnt/user/appdata/scrutiny` falls gewuenscht | InfluxDB bewusst nicht Teil des Critical-Scope | keine | Traefik, Authelia | UI startet, Laufwerke sichtbar |
|
||||||
| Speedtest Tracker | Share | `/mnt/user/appdata/speedtest-tracker/config` | SQLite im App-Pfad | `APP_KEY`, `ADMIN_PASSWORD` | Traefik, Authelia | UI startet |
|
| Speedtest Tracker | Share + Dump | `/mnt/user/appdata/speedtest-tracker/config` | `speedtest-tracker.sqlite.dump` | `APP_KEY`, `ADMIN_PASSWORD` | Traefik, Authelia | UI startet |
|
||||||
| BentoPDF | Rebuildbar | keine kritische Persistenz; alte Stirling-PDF-Daten unter `/mnt/user/appdata/stirling-pdf` bis zur Abnahme behalten | keine | keine separaten Secret-Dateien dokumentiert | Traefik, Authelia | UI startet, PDF-Tools verfuegbar, Office-Konvertierung ueber HTTPS funktioniert |
|
| BentoPDF | Rebuildbar | keine kritische Persistenz; alte Stirling-PDF-Daten unter `/mnt/user/appdata/stirling-pdf` bis zur Abnahme behalten | keine | keine separaten Secret-Dateien dokumentiert | Traefik, Authelia | UI startet, PDF-Tools verfuegbar, Office-Konvertierung ueber HTTPS funktioniert |
|
||||||
| Grafana | Share | `/mnt/user/appdata/grafana`, inklusive `provisioning/datasources/influxdb.yml` | SQLite im App-Pfad | `grafana_admin_password.txt`, `grafana_influxdb_token.txt` | Traefik, Authelia, InfluxDB 3 Core | UI startet, InfluxDB-Datenquelle testet erfolgreich |
|
| Grafana | historischer Altstand | `/mnt/user/appdata/grafana` | `grafana.sqlite` | `grafana_admin_password.txt`, `grafana_influxdb_token.txt` | nicht aktiv | Compose-Pfad aus aktivem Repo entfernt; nur ueber Git-Historie wiederherstellen, falls ein Rollback wirklich noetig ist |
|
||||||
| InfluxDB 3 Core | Share | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | dateibasierter Object Store | `influxdb3_admin_token.json` | internes `grafana_influx_internal` Netz | `homelab`-Datenbank vorhanden, Grafana kann SQL-Abfrage ausfuehren |
|
| InfluxDB 3 Core | historischer Altstand / Datenuebernahme | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | dateibasierter Object Store | `influxdb3_admin_token.json` | `monitoring-influxdb3-core` | Datenpfad wird vom Monitoring-Zielstack weitergenutzt und darf nicht blind geloescht werden |
|
||||||
| Hermes Agent | Borg / Share | `/mnt/user/appdata/hermes-agent/data`, `/mnt/user/appdata/hermes-agent/ssh` | keine eigene DB | Host-`.env` fuer Provider-/API-/Home-Assistant-Tokens, `hermes_runner_id_ed25519`, `HERMES_DASHBOARD_HOST` | Traefik, Authelia, `hermes_net` | Gateway-Health ist ok, Dashboard leitet anonym zu Authelia weiter, SSH-Runner-Key ist vorhanden |
|
| Loki / Alloy | historischer Altstand | `/mnt/user/appdata/loki/config`, `/mnt/user/appdata/loki/data`, `/mnt/user/appdata/alloy/config` | keine primaere DB; Loki-Dateispeicher war transient | keine zusaetzlichen Secrets | nicht aktiv | Compose-Pfad aus aktivem Repo entfernt; aktuelle Logsammlung laeuft ueber `monitoring-loki`/`monitoring-promtail` |
|
||||||
|
| Monitoring Stack | Rebuild + named volumes + InfluxDB-Appdata | `prometheus_data`, `loki_data`, `promtail_positions`, `grafana_data`; InfluxDB unter `/mnt/user/appdata/influxdb3/data` und `/mnt/user/appdata/influxdb3/plugins`; Provisioning aus `monitoring/grafana/provisioning` | Prometheus-TSDB, Loki-Dateispeicher und InfluxDB-Dateistore; Diagnose-/Langzeitdaten, keine Tier-1-Restore-Quelle | `monitoring_grafana_admin_password.txt`, `monitoring_grafana_influxdb_token.txt`, `influxdb3_admin_token.json` | `monitoring_net`, `monitoring_influx_lan`, `frontend_net`, Traefik, Authelia, Docker socket read-only fuer Promtail, Host-Mounts fuer node-exporter/cAdvisor | `https://monitoring.kaleschke.info` leitet zu Authelia; Prometheus Targets sind up; Grafana-Datasources `Prometheus`, `Loki` und `InfluxDB 3 Core` funktionieren |
|
||||||
|
| Hermes Agent | VM-seitig offen | `/mnt/user/appdata/hermes-agent/data`, `/mnt/user/appdata/hermes-agent/ssh` | keine eigene DB | Host-`.env` fuer Provider-/API-/Home-Assistant-Tokens, `hermes_runner_id_ed25519`, `HERMES_DASHBOARD_HOST` | separate Hermes-VM/Runner, Traefik, Authelia, `hermes_net` | NAS-Stack nicht starten, solange Runner-VM und echte `.env` fehlen |
|
||||||
| ddns-updater | Rebuildbar | geringe Persistenzrelevanz | keine | Provider-Zugang ueber Stack ENV | Internetzugang | Update-Job laeuft |
|
| ddns-updater | Rebuildbar | geringe Persistenzrelevanz | keine | Provider-Zugang ueber Stack ENV | Internetzugang | Update-Job laeuft |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -82,6 +84,15 @@ Aktuell relevante Dump-Artefakte unter `/mnt/user/backups/borg/dumps/latest`:
|
|||||||
- `postgresql17-authelia.dump` (optional / wenn vorhanden)
|
- `postgresql17-authelia.dump` (optional / wenn vorhanden)
|
||||||
- `mealie.dump`
|
- `mealie.dump`
|
||||||
- `immich.dump`
|
- `immich.dump`
|
||||||
|
- `nextcloud.dump`
|
||||||
|
- `gitea.sqlite.dump`
|
||||||
|
- `vaultwarden.sqlite.dump`
|
||||||
|
- `speedtest-tracker.sqlite.dump`
|
||||||
|
- `filebrowser.bolt.dump`
|
||||||
|
- `borg-ui.sqlite`
|
||||||
|
- `grafana.sqlite`
|
||||||
|
- `unraid-flash-config.tar.gz` plus `unraid-flash-config.tar.gz.sha256` und Manifest
|
||||||
|
- Monitoring-Stack: keine verpflichtenden Dump-Artefakte; Prometheus/Loki/Grafana named volumes sind Diagnose-/Dashboard-Zustand, keine primaere Restore-Quelle.
|
||||||
- `komodo-mongo.archive.gz` (noch gesondert verifizieren)
|
- `komodo-mongo.archive.gz` (noch gesondert verifizieren)
|
||||||
|
|
||||||
Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.sh` vorgesehen.
|
Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.sh` vorgesehen.
|
||||||
@@ -95,14 +106,24 @@ Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.s
|
|||||||
- Bei Unsicherheit zuerst in Testpfade oder Testinstanzen pruefen.
|
- Bei Unsicherheit zuerst in Testpfade oder Testinstanzen pruefen.
|
||||||
- Der minimale Erfolg ist **nicht** "Container startet", sondern "fachlicher Smoke-Test gelingt".
|
- Der minimale Erfolg ist **nicht** "Container startet", sondern "fachlicher Smoke-Test gelingt".
|
||||||
|
|
||||||
|
### Validiertes Restore-Lab-Muster
|
||||||
|
|
||||||
|
- Restore-Quelle bleibt das produktive Borg-Repo
|
||||||
|
- Testziel liegt unter `/mnt/user/backups/restore-lab/<dienst>`
|
||||||
|
- Reports liegen unter `/mnt/user/backups/restore-reports`
|
||||||
|
- Borg-Zugriff kann ueber den vorhandenen `borg-ui`-Container laufen
|
||||||
|
- Borg-Passphrasen fuer Restore-Tests sollten aus Host-Secret-Dateien kommen, nicht aus UI-Interna
|
||||||
|
- Testinstanzen bekommen keine produktive Domain und keine Traefik-Route
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Erste sinnvolle Referenz-Restores
|
## Erste sinnvolle Referenz-Restores
|
||||||
|
|
||||||
Wenn weitere Restore-Uebungen dokumentiert werden sollen, sind diese Dienste besonders geeignet:
|
Wenn weitere Restore-Uebungen dokumentiert werden sollen, sind diese Dienste besonders geeignet:
|
||||||
|
|
||||||
1. `gitea`
|
1. `mail-archiver`
|
||||||
2. `paperless-ngx`
|
2. `paperless-ngx`
|
||||||
3. `vaultwarden`
|
3. `gitea`
|
||||||
|
4. `vaultwarden`
|
||||||
|
|
||||||
Sie liefern hohen Erkenntnisgewinn ohne den kompletten Homelab-Neuaufbau zu brauchen.
|
Sie liefern hohen Erkenntnisgewinn ohne den kompletten Homelab-Neuaufbau zu brauchen.
|
||||||
|
|||||||
+33
-1
@@ -89,11 +89,43 @@ Vor dem ersten produktiven Einsatz reicht es, den vorbereiteten Stack nicht zu d
|
|||||||
|
|
||||||
Nach einem Deploy:
|
Nach einem Deploy:
|
||||||
|
|
||||||
1. `ops/grafana-influxdb` in Komodo stoppen oder den letzten Git-Stand ohne diesen Stack deployen
|
1. alten Grafana/InfluxDB-Stack in Komodo gestoppt lassen; der fruehere Compose-Pfad `ops/grafana-influxdb` ist seit 2026-05-26 nicht mehr im aktiven Repo
|
||||||
2. Persistenz unter `/mnt/user/appdata/grafana` und `/mnt/user/appdata/influxdb3` unangetastet lassen
|
2. Persistenz unter `/mnt/user/appdata/grafana` und `/mnt/user/appdata/influxdb3` unangetastet lassen
|
||||||
3. Secrets unter `/mnt/user/appdata/secrets/grafana_admin_password.txt`, `/mnt/user/appdata/secrets/grafana_influxdb_token.txt` und `/mnt/user/appdata/secrets/influxdb3_admin_token.json` nur nach bewusstem Entscheid entfernen
|
3. Secrets unter `/mnt/user/appdata/secrets/grafana_admin_password.txt`, `/mnt/user/appdata/secrets/grafana_influxdb_token.txt` und `/mnt/user/appdata/secrets/influxdb3_admin_token.json` nur nach bewusstem Entscheid entfernen
|
||||||
4. Grafana-Domain und InfluxDB-Zugriff testen, bis klar ist, dass keine produktiven Dashboards oder Writer mehr davon abhaengen
|
4. Grafana-Domain und InfluxDB-Zugriff testen, bis klar ist, dass keine produktiven Dashboards oder Writer mehr davon abhaengen
|
||||||
|
|
||||||
|
## Monitoring-Zielstack Rollback
|
||||||
|
|
||||||
|
Der Zielzustand ist `monitoring/` als einziger Observability-Stack. Bei Problemen nach der Migration:
|
||||||
|
|
||||||
|
1. `monitoring` in Komodo stoppen oder auf den letzten funktionierenden Commit zurueckgehen
|
||||||
|
2. nur im echten Notfall die abgeloesten Altstaende aus der Git-Historie vor dem Repo-Cleanup wiederherstellen, z. B. aus Commit `ff5991c`; nicht dauerhaft parallel zum Zielstack betreiben
|
||||||
|
3. named volumes `prometheus_data`, `loki_data`, `promtail_positions`, `grafana_data` sowie `/mnt/user/appdata/influxdb3` nicht blind loeschen
|
||||||
|
4. Secrets `monitoring_grafana_admin_password.txt`, `monitoring_grafana_influxdb_token.txt` und `influxdb3_admin_token.json` nur nach bewusstem Entscheid entfernen
|
||||||
|
5. Home Assistant Writer erst wieder umstellen, wenn `curl -i http://192.168.178.58:8181/` erwartbar `401 Unauthorized` liefert
|
||||||
|
6. Grafana-Datasources `Prometheus`, `Loki` und `InfluxDB 3 Core` testen
|
||||||
|
|
||||||
|
## Uptime Kuma Removal Rollback
|
||||||
|
|
||||||
|
Falls die Blackbox-/Grafana-Ablösung unerwartet nicht ausreicht:
|
||||||
|
|
||||||
|
1. per Ruecknahme-Commit `ops/uptime-kuma/docker-compose.yml`, die Blackbox-/Glance-/Authelia-Referenzen und die Restore-Freshness-Pruefung auf den letzten Uptime-Kuma-Stand zurueckbringen
|
||||||
|
2. nach Gitea pushen und den Uptime-Kuma-Stack in Komodo neu anlegen oder aus dem letzten Stack-Backup wiederherstellen
|
||||||
|
3. `/mnt/user/appdata/_archive/uptime-kuma-removed-2026-05-25` nach `/mnt/user/appdata/uptime-kuma` zurueckverschieben, falls die Archivierung bereits erfolgt ist
|
||||||
|
4. `https://uptime.kaleschke.info` und die Monitore pruefen
|
||||||
|
5. erst danach den Blackbox-/Grafana-Zielzustand erneut bewerten
|
||||||
|
|
||||||
|
## Glance Dashboard Rollback
|
||||||
|
|
||||||
|
Vor dem ersten produktiven Einsatz reicht es, den vorbereiteten Stack `ops/glance` nicht zu deployen oder per Ruecknahme-Commit aus dem Repo zu entfernen.
|
||||||
|
|
||||||
|
Nach einem Deploy:
|
||||||
|
|
||||||
|
1. `glance` in Komodo stoppen oder auf den letzten funktionierenden Commit zurueckgehen
|
||||||
|
2. keine Produktivdaten loeschen; Glance nutzt nur Repo-Konfiguration und Stack-ENV
|
||||||
|
3. pruefen, ob `https://glance.kaleschke.info` nicht mehr geroutet wird oder wieder den erwarteten Stand zeigt
|
||||||
|
4. der `glance-docker-socket-proxy` darf nicht separat als Dauercontainer laufen bleiben
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Daten-Rollback
|
## Daten-Rollback
|
||||||
|
|||||||
+22
-10
@@ -20,32 +20,37 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
| Traefik | Cloudflare DNS API Token | `/mnt/user/appdata/traefik/secrets/cloudflare_dns_api_token` -> Docker Secret `cloudflare_dns_api_token` | aktiv |
|
| Traefik | Cloudflare DNS API Token | `/mnt/user/appdata/traefik/secrets/cloudflare_dns_api_token` -> Docker Secret `cloudflare_dns_api_token` | aktiv |
|
||||||
| PostgreSQL 17 | DB Password | `/mnt/user/appdata/secrets/postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
| PostgreSQL 17 | DB Password | `/mnt/user/appdata/secrets/postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
||||||
| Redis | Passwort | `/mnt/user/appdata/secrets/redis_password.txt` -> Datei-Mount + Startkommando in `infra/redis/docker-compose.yml` | aktiv |
|
| Redis | Passwort | `/mnt/user/appdata/secrets/redis_password.txt` -> Datei-Mount + Startkommando in `infra/redis/docker-compose.yml` | aktiv |
|
||||||
| Mealie | DB Password | `/mnt/user/appdata/secrets/mealie_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
| Mealie | DB Password | `/mnt/user/appdata/secrets/mealie_postgres_password.txt` -> nicht versionierte Stack-`.env` `${MEALIE_POSTGRES_PASSWORD}` -> `POSTGRES_PASSWORD` | aktiv |
|
||||||
| mealie-postgres | DB Password | `/mnt/user/appdata/secrets/mealie_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
| mealie-postgres | DB Password | `/mnt/user/appdata/secrets/mealie_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
||||||
| Paperless-ngx | DB Password | Stack ENV `${PAPERLESS_DBPASS}` | aktiv |
|
| Paperless-ngx | DB Password | Stack ENV `${PAPERLESS_DBPASS}` | aktiv |
|
||||||
| Paperless-ngx | Redis URL | Stack ENV `${PAPERLESS_REDIS}` | aktiv |
|
| Paperless-ngx | Redis URL | Stack ENV `${PAPERLESS_REDIS}` | aktiv |
|
||||||
| code-server | Passwort | `/mnt/user/appdata/code-server/secrets/password` -> `PASSWORD_FILE` | aktiv |
|
| code-server | Passwort | `/mnt/user/appdata/code-server/secrets/password` -> `FILE__PASSWORD` | aktiv |
|
||||||
|
| Filebrowser | Admin Password | `/mnt/user/appdata/secrets/filebrowser_admin_password.txt` -> initialisierte SQLite-DB | aktiv |
|
||||||
| Immich (server) | DB Password | Stack ENV `${IMMICH_DB_PASSWORD}` | aktiv |
|
| Immich (server) | DB Password | Stack ENV `${IMMICH_DB_PASSWORD}` | aktiv |
|
||||||
| immich-postgres | DB Password | `/mnt/user/appdata/secrets/immich_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
| immich-postgres | DB Password | `/mnt/user/appdata/secrets/immich_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
||||||
| mail-archiver | DB Connection | Stack ENV `${MAILARCHIVER_DB_CONNECTION}` | aktiv |
|
| mail-archiver | DB Connection | Stack ENV `${MAILARCHIVER_DB_CONNECTION}` | aktiv |
|
||||||
| mail-archiver | Auth Password | Stack ENV `${MAILARCHIVER_AUTH_PASSWORD}` | aktiv |
|
| mail-archiver | Auth Password | Stack ENV `${MAILARCHIVER_AUTH_PASSWORD}` | aktiv |
|
||||||
| Authelia | JWT Secret | `/mnt/user/appdata/secrets/authelia_jwt_secret.txt` -> `AUTHELIA_JWT_SECRET_FILE` | aktiv |
|
| Authelia | JWT Secret | `/mnt/user/appdata/secrets/authelia_jwt_secret.txt` -> `AUTHELIA_JWT_SECRET_FILE` | aktiv |
|
||||||
| Authelia | Session Secret | `/mnt/user/appdata/secrets/authelia_session_secret.txt` -> `AUTHELIA_SESSION_SECRET_FILE` | aktiv |
|
| Authelia | Session Secret | `/mnt/user/appdata/secrets/authelia_session_secret.txt` -> `AUTHELIA_SESSION_SECRET_FILE` | aktiv |
|
||||||
|
| Authelia | SMTP Password | `/mnt/user/appdata/secrets/authelia_smtp_password.txt` -> Host-Secret fuer SMTP-Notifier | aktiv |
|
||||||
| Authelia | Storage Encryption Key | `/mnt/user/appdata/secrets/authelia_storage_encryption_key.txt` -> `AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE` | aktiv |
|
| Authelia | Storage Encryption Key | `/mnt/user/appdata/secrets/authelia_storage_encryption_key.txt` -> `AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE` | aktiv |
|
||||||
| Authelia | Postgres Password | `/mnt/user/appdata/secrets/authelia_postgres_password.txt` -> `AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE` | aktiv |
|
| Authelia | Postgres Password | `/mnt/user/appdata/secrets/authelia_postgres_password.txt` -> `AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE` | aktiv |
|
||||||
| Komodo Mongo | Root Password | `/mnt/user/appdata/secrets/komodo_mongo_password.txt` -> `MONGO_INITDB_ROOT_PASSWORD_FILE` | aktiv |
|
| Komodo Mongo | Root Password | `/mnt/user/appdata/secrets/komodo_mongo_password.txt` -> `MONGO_INITDB_ROOT_PASSWORD_FILE` | aktiv |
|
||||||
| Komodo Core | App Secrets | Stack ENV `${KOMODO_SECRET_KEY}`, `${KOMODO_WEBHOOK_SECRET}`, `${KOMODO_JWT_SECRET}`, `${KOMODO_MONGO_PASSWORD}`, `${KOMODO_PERIPHERY_PASSKEY}` | aktiv |
|
| Komodo Core | App Secrets | Stack ENV `${KOMODO_SECRET_KEY}`, `${KOMODO_WEBHOOK_SECRET}`, `${KOMODO_JWT_SECRET}`, `${KOMODO_MONGO_PASSWORD}`, `${KOMODO_PERIPHERY_PASSKEY}` | aktiv |
|
||||||
| Homepage | API Tokens / Zugangsdaten | Stack ENV `HOMEPAGE_VAR_*` | aktiv |
|
| Gitea Push Mirror | GitHub fine-grained PAT fuer `michaelkaleschke-spec/homelab-infra` | Gitea Repository Mirror Settings, persistent in `/mnt/user/services/gitea/data`; kein Datei-Secret im Repo | aktiv |
|
||||||
|
| Glance | Community Widget API Tokens | Stack ENV `${GLANCE_IMMICH_API_KEY}`, `${GLANCE_ADGUARD_USERNAME}`, `${GLANCE_ADGUARD_PASSWORD}`, `${GLANCE_SPEEDTEST_API_KEY}` | aktiv |
|
||||||
| speedtest-tracker | App Key / Admin-Zugang | Stack ENV `${APP_KEY}`, `${ADMIN_PASSWORD}` | aktiv |
|
| speedtest-tracker | App Key / Admin-Zugang | Stack ENV `${APP_KEY}`, `${ADMIN_PASSWORD}` | aktiv |
|
||||||
| Nextcloud | Admin User | `/mnt/user/appdata/secrets/nextcloud_admin_user.txt` -> `NEXTCLOUD_ADMIN_USER_FILE` | neu |
|
| Nextcloud | Admin User | `/mnt/user/appdata/secrets/nextcloud_admin_user.txt` -> `NEXTCLOUD_ADMIN_USER_FILE` | neu |
|
||||||
| Nextcloud | Admin Password | `/mnt/user/appdata/secrets/nextcloud_admin_password.txt` -> `NEXTCLOUD_ADMIN_PASSWORD_FILE` | neu |
|
| Nextcloud | Admin Password | `/mnt/user/appdata/secrets/nextcloud_admin_password.txt` -> `NEXTCLOUD_ADMIN_PASSWORD_FILE` | neu |
|
||||||
| nextcloud-postgres | DB Password | `/mnt/user/appdata/secrets/nextcloud_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | neu |
|
| nextcloud-postgres | DB Password | `/mnt/user/appdata/secrets/nextcloud_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | neu |
|
||||||
| Borg UI / Borg | Admin-Login, `SECRET_KEY`, SSH-Keys, Repo-Credentials | persistent unter `/mnt/user/appdata/borg-ui/data/` | aktiv |
|
| Borg UI / Borg | Admin-Login, `SECRET_KEY`, SSH-Keys, Repo-Credentials | persistent unter `/mnt/user/appdata/borg-ui/data/` | aktiv |
|
||||||
| Hermes Agent | Provider-Keys, Bot-Tokens, API-Server-Key | `/mnt/user/appdata/hermes-agent/data/.env` | neu |
|
| Borg Repo | Borg-Passphrase fuer Restore-Tests und Notfallzugriff | `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` -> Host-Secret-Datei, nicht im Repo | aktiv |
|
||||||
| Hermes Agent | SSH-Runner Private Key | `/mnt/user/appdata/secrets/hermes_runner_id_ed25519` -> `/root/.ssh/id_ed25519` | neu |
|
| 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 |
|
||||||
| Grafana | Admin Password | `/mnt/user/appdata/secrets/grafana_admin_password.txt` -> `GF_SECURITY_ADMIN_PASSWORD__FILE` | vorbereitet |
|
| Hermes Agent | Provider-Keys, Bot-Tokens, API-Server-Key | `/mnt/user/appdata/hermes-agent/data/.env` | VM-seitig offen |
|
||||||
| InfluxDB 3 Core | Admin Token JSON | `/mnt/user/appdata/secrets/influxdb3_admin_token.json` -> Docker Secret `/run/secrets/influxdb3_admin_token` | vorbereitet |
|
| Hermes Agent | SSH-Runner Private Key | `/mnt/user/appdata/secrets/hermes_runner_id_ed25519` -> `/root/.ssh/id_ed25519` | VM-seitig offen |
|
||||||
| Grafana -> InfluxDB | Datasource Token | `/mnt/user/appdata/secrets/grafana_influxdb_token.txt` -> Docker Secret `/run/secrets/grafana_influxdb_token` | vorbereitet |
|
| InfluxDB 3 Core | Admin Token JSON | `/mnt/user/appdata/secrets/influxdb3_admin_token.json` -> Docker Secret `/run/secrets/influxdb3_admin_token` | aktiv |
|
||||||
|
| Monitoring Grafana | Admin Password | `/mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt` -> Docker Secret `/run/secrets/monitoring_grafana_admin_password` -> `GF_SECURITY_ADMIN_PASSWORD__FILE` | aktiv |
|
||||||
|
| Monitoring Grafana -> InfluxDB | Datasource Token | `/mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt` -> Docker Secret `/run/secrets/monitoring_grafana_influxdb_token` | aktiv |
|
||||||
| Home Assistant -> InfluxDB | HA InfluxDB Token | `/homeassistant/secrets.yaml` -> `influxdb3_homeassistant_token` | geplant |
|
| Home Assistant -> InfluxDB | HA InfluxDB Token | `/homeassistant/secrets.yaml` -> `influxdb3_homeassistant_token` | geplant |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -56,6 +61,8 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Gotify | `gotify_password.txt` / `GOTIFY_DEFAULTUSER_PASS_FILE` | Dienst nicht mehr aktiv |
|
| Gotify | `gotify_password.txt` / `GOTIFY_DEFAULTUSER_PASS_FILE` | Dienst nicht mehr aktiv |
|
||||||
| diun | Stack ENV | Container entfernt |
|
| diun | Stack ENV | Container entfernt |
|
||||||
|
| Uptime Kuma | `uptime_kuma_admin_password.txt` | Dienst am 2026-05-25 entfernt; nur fuer Alt-Appdata/Archiv behalten |
|
||||||
|
| Grafana Altstand | `grafana_admin_password.txt`, `grafana_influxdb_token.txt` | Compose-Pfad aus aktivem Repo entfernt; nur fuer Git-Historie-/Rollback-Fall behalten |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -66,18 +73,21 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
|-- authelia_jwt_secret.txt
|
|-- authelia_jwt_secret.txt
|
||||||
|-- authelia_postgres_password.txt
|
|-- authelia_postgres_password.txt
|
||||||
|-- authelia_session_secret.txt
|
|-- authelia_session_secret.txt
|
||||||
|
|-- authelia_smtp_password.txt
|
||||||
|-- authelia_storage_encryption_key.txt
|
|-- authelia_storage_encryption_key.txt
|
||||||
|-- immich_postgres_password.txt
|
|-- immich_postgres_password.txt
|
||||||
|-- komodo_mongo_password.txt
|
|-- komodo_mongo_password.txt
|
||||||
|-- mealie_postgres_password.txt
|
|-- mealie_postgres_password.txt
|
||||||
|
|-- monitoring_grafana_admin_password.txt
|
||||||
|
|-- monitoring_grafana_influxdb_token.txt
|
||||||
|-- nextcloud_admin_password.txt
|
|-- nextcloud_admin_password.txt
|
||||||
|-- nextcloud_admin_user.txt
|
|-- nextcloud_admin_user.txt
|
||||||
|-- nextcloud_postgres_password.txt
|
|-- nextcloud_postgres_password.txt
|
||||||
|-- postgres_password.txt
|
|-- postgres_password.txt
|
||||||
|-- redis_password.txt
|
|-- redis_password.txt
|
||||||
|-- grafana_admin_password.txt
|
|-- borg_repo_passphrase.txt
|
||||||
|-- grafana_influxdb_token.txt
|
|
||||||
|-- influxdb3_admin_token.json
|
|-- influxdb3_admin_token.json
|
||||||
|
|-- filebrowser_admin_password.txt
|
||||||
`-- vaultwarden_admin_token.txt
|
`-- vaultwarden_admin_token.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -87,6 +97,8 @@ Weitere dokumentierte Secret-Pfade:
|
|||||||
- `/mnt/user/appdata/secrets/hermes_runner_id_ed25519`
|
- `/mnt/user/appdata/secrets/hermes_runner_id_ed25519`
|
||||||
- `/mnt/user/appdata/traefik/secrets/cloudflare_dns_api_token`
|
- `/mnt/user/appdata/traefik/secrets/cloudflare_dns_api_token`
|
||||||
- Borg UI verwaltet Session-Secret, Admin-Login, SSH-Keys und Repo-Credentials in seiner persistenten `/data`-Struktur. Diese Daten liegen nicht im Git, muessen aber gesichert werden.
|
- Borg UI verwaltet Session-Secret, Admin-Login, SSH-Keys und Repo-Credentials in seiner persistenten `/data`-Struktur. Diese Daten liegen nicht im Git, muessen aber gesichert werden.
|
||||||
|
- Die Borg-Repo-Passphrase liegt zusaetzlich als Host-Secret-Datei fuer Restore-Tests und Notfallzugriff vor; der Wert muss ausserhalb des Homelabs analog gesichert werden.
|
||||||
|
- Gitea verwaltet den GitHub-Push-Mirror-PAT in den Repository-Mirror-Settings. Der Wert wird nicht dokumentiert und nicht in Dateien unter `docs/` oder `core/gitea/` geschrieben.
|
||||||
- `paperless-ngx` ist eine bewusste Ausnahme: DB-Passwort und Redis-URL bleiben aktuell als Komodo Stack Environment Variables hinterlegt, um den stabil laufenden Produktionsstand nicht fuer eine reine Secret-Mechanik-Migration zu riskieren.
|
- `paperless-ngx` ist eine bewusste Ausnahme: DB-Passwort und Redis-URL bleiben aktuell als Komodo Stack Environment Variables hinterlegt, um den stabil laufenden Produktionsstand nicht fuer eine reine Secret-Mechanik-Migration zu riskieren.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# Services Recovery - KalliLab CORE
|
||||||
|
|
||||||
|
Status: Initiale Recovery-Baseline 2026-05-26, aus dem Audit 2026-05-25 abgeleitet.
|
||||||
|
Verwandte Docs: `docs/DISASTER_RECOVERY.md`, `docs/RESTORE_MATRIX.md`, `docs/STORAGE_LAYOUT.draft.md`, `docs/SECRETS_MAP.md`
|
||||||
|
|
||||||
|
## Zweck
|
||||||
|
|
||||||
|
Der Share `/mnt/user/services/` ist recovery-kritisch, weil dort GitOps- und Host-Automation-Pfade liegen. Dieses Dokument beschreibt, welche Services-Pfade gesichert werden muessen und wie ein Kaltstart ohne laufendes Komodo gedacht ist.
|
||||||
|
|
||||||
|
## Kritische Pfade
|
||||||
|
|
||||||
|
| Pfad | Zweck | Kritikalitaet | Backup-Anforderung |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `/mnt/user/services/homelab-infra` | Host-Repo-Clone fuer Automation/Posture | hoch | Borg + GitHub/Gitea Mirror |
|
||||||
|
| `/mnt/user/services/stacks` | Komodo Stack Workspaces | hoch | Borg, vor strukturellen Aenderungen extra sichern |
|
||||||
|
| `/mnt/user/services/gitea/git/repositories` | Gitea Repository-Inhalte | kritisch | Borg + separater Mirror <= 6 h |
|
||||||
|
| `/mnt/user/services/posture-check` | Hostseitig ausgefuehrte Checks | hoch | Borg + Repo-Abgleich |
|
||||||
|
| `/mnt/user/appdata/secrets` | Runtime Secrets | kritisch | Borg + ausgewählte analoge/off-system Kopien |
|
||||||
|
|
||||||
|
## Gitea Repository Mirror
|
||||||
|
|
||||||
|
Ziel: Verlustfenster fuer `/mnt/user/services/gitea/git/repositories/` auf maximal 6 Stunden begrenzen.
|
||||||
|
|
||||||
|
Optionen:
|
||||||
|
|
||||||
|
| Option | Bewertung |
|
||||||
|
|---|---|
|
||||||
|
| `git bundle` je Repository auf zweites Medium | Sehr gut fuer Git-Recovery, transparent, gut pruefbar |
|
||||||
|
| `rsync` auf externe Platte oder zweiten Host | Einfach, aber Ziel muss regelmaessig erreichbar und geprueft sein |
|
||||||
|
| Separates Borg-Repo mit kurzem Schedule | Konsistent zum bestehenden Backup, aber wieder Borg-/Passphrase-abhaengig |
|
||||||
|
|
||||||
|
Empfohlener Start:
|
||||||
|
|
||||||
|
1. `git bundle`-Job fuer alle Gitea-Repositories definieren.
|
||||||
|
2. Ziel auf zweitem physischen Medium oder separatem Off-site-Ziel ablegen.
|
||||||
|
3. Job alle 6 Stunden ausfuehren.
|
||||||
|
4. Stichprobe: ein Bundle in Wegwerfpfad klonen.
|
||||||
|
|
||||||
|
Erfolgskriterium:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone /path/to/repo.bundle /tmp/repo-restore-test
|
||||||
|
git -C /tmp/repo-restore-test fsck
|
||||||
|
```
|
||||||
|
|
||||||
|
## Komodo Bootstrap
|
||||||
|
|
||||||
|
Problem: Komodo verwaltet Stacks, ist aber selbst Teil des Recovery-Pfads. Ein kalter Host darf nicht voraussetzen, dass Komodo schon laeuft.
|
||||||
|
|
||||||
|
Komodo ist deshalb bewusst kein normaler Auto-Deploy-Stack: der `komodo`-Self-Stack hat keinen aktiven Gitea-Webhook. Recovery und Aenderungen laufen ueber den dokumentierten Bootstrap-Pfad und muessen nach dem Start in Komodo validiert werden.
|
||||||
|
|
||||||
|
Minimaler Wiederanlauf:
|
||||||
|
|
||||||
|
1. Docker und externe Netze herstellen (`frontend_net`, `backend_net`, ggf. weitere dokumentierte Netze).
|
||||||
|
2. Repo aus Gitea/GitHub Mirror klonen.
|
||||||
|
3. Komodo Compose aus `ops/komodo/docker-compose.yml` oder einem spaeteren Bootstrap-Pfad starten.
|
||||||
|
4. Erforderliche `.env`/Secrets aus Host-Secret-Backup wiederherstellen.
|
||||||
|
5. Komodo-Core, Periphery und Mongo starten.
|
||||||
|
6. Web-UI und Periphery-Verbindung pruefen.
|
||||||
|
|
||||||
|
Offene Aufgabe:
|
||||||
|
|
||||||
|
- Entscheidung 2026-05-26: `ops/komodo/docker-compose.yml` bleibt die verbindliche Bootstrap-Quelle. Der `komodo`-Self-Stack hat keinen aktiven Gitea-Webhook und ist nicht der Recovery-Anker.
|
||||||
|
- Offen bleibt nur ein spaeterer Trockenlauf, bei dem die Komodo-Startkommandos gegen echte Restore-Pfade getestet werden.
|
||||||
|
|
||||||
|
Validierung:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f ops/komodo/docker-compose.yml config
|
||||||
|
docker compose -f ops/komodo/docker-compose.yml up -d
|
||||||
|
docker ps --filter "name=komodo"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets Recovery Reihenfolge
|
||||||
|
|
||||||
|
Authoritativ ist `docs/SECRETS_MAP.md`. Fuer den Kaltstart ist diese Reihenfolge praktisch:
|
||||||
|
|
||||||
|
1. Borg-Passphrase analog/off-system beschaffen.
|
||||||
|
2. Borg-Repo-Zugang/SSH-Key wiederherstellen.
|
||||||
|
3. `/mnt/user/appdata/secrets/` aus Borg wiederherstellen.
|
||||||
|
4. Komodo Stack ENV / Recovery ENV wiederherstellen.
|
||||||
|
5. Gitea Secrets und SSH-Material wiederherstellen.
|
||||||
|
6. Traefik/Cloudflare Secret wiederherstellen.
|
||||||
|
7. App-spezifische Secrets nach Tier-Reihenfolge wiederherstellen.
|
||||||
|
|
||||||
|
## Break-glass Regeln
|
||||||
|
|
||||||
|
- Keine Secret-Werte in Git oder Tickets kopieren.
|
||||||
|
- Restore-Tests laufen in Wegwerfpfaden, nie direkt gegen produktive Pfade.
|
||||||
|
- Wenn Gitea und Komodo beide down sind, gewinnt der externe GitHub-Mirror als Repo-Quelle.
|
||||||
|
- Wenn Borg ohne Passphrase nicht entschluesselbar ist, ist Recovery blockiert. Deshalb ist die analoge Passphrase-Sicherung P0.
|
||||||
|
|
||||||
|
## Naechste Aufgaben
|
||||||
|
|
||||||
|
| Status | Aufgabe |
|
||||||
|
|---|---|
|
||||||
|
| offen | Gitea-Bundle- oder Mirror-Mechanik final entscheiden |
|
||||||
|
| erledigt | Komodo-Bootstrap-Quelle finalisieren |
|
||||||
|
| offen | Restore-Kommandos nach erstem Trockenlauf mit echten Pfaden ergaenzen |
|
||||||
|
| erledigt | Services-Recovery in `docs/DISASTER_RECOVERY.md` verlinken |
|
||||||
+44
-22
@@ -1,6 +1,6 @@
|
|||||||
# Service Catalog
|
# Service Catalog
|
||||||
|
|
||||||
Stand: 2026-05-04
|
Stand: 2026-05-23
|
||||||
|
|
||||||
Dieser Katalog beschreibt produktive und repo-vorbereitete Dienste aus Sicht von Betrieb, Restore und KI-Kontext. Er basiert auf dem Repo-Sollzustand. Vor produktiven Eingriffen immer den Live-Zustand in Komodo/Docker pruefen.
|
Dieser Katalog beschreibt produktive und repo-vorbereitete Dienste aus Sicht von Betrieb, Restore und KI-Kontext. Er basiert auf dem Repo-Sollzustand. Vor produktiven Eingriffen immer den Live-Zustand in Komodo/Docker pruefen.
|
||||||
|
|
||||||
@@ -11,17 +11,17 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||||
|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|
|
||||||
| `traefik` | zentraler Reverse Proxy, TLS, Docker-Label-Routing | `traefik/docker-compose.yml`, `traefik/dynamic/*` | `https://traefik.kaleschke.info` | Docker socket, Cloudflare DNS API, `frontend_net`, `backend_net` | `/mnt/user/appdata/traefik/dynamic`, `/mnt/user/appdata/traefik/letsencrypt` | Tier 1, Share/Borg | ja, eigene Dashboard-Route mit Authelia | Host-Ports 80/443 sind zentrale Ausnahme; dynamic configs werden nicht automatisch von Komodo deployed |
|
| `traefik` | zentraler Reverse Proxy, TLS, Docker-Label-Routing | `traefik/docker-compose.yml`, `traefik/dynamic/*` | `https://traefik.kaleschke.info` | Docker socket, Cloudflare DNS API, `frontend_net`, `backend_net` | `/mnt/user/appdata/traefik/dynamic`, `/mnt/user/appdata/traefik/letsencrypt` | Tier 1, Share/Borg | ja, eigene Dashboard-Route mit Authelia | Host-Ports 80/443 sind zentrale Ausnahme; dynamic configs werden nicht automatisch von Komodo deployed |
|
||||||
| `adguard` | DNS-Server / LAN DNS | `host-services/Adguard/docker-compose.yml` | LAN-Port `53`, Admin `8082` | `dns_net`, `frontend_net`, Unbound | `/mnt/user/appdata/adguard/conf`, `/mnt/user/appdata/adguard/work` | Tier 1, config relevant | nein | Direkte Ports 53 und 8082 dokumentierte Ausnahme; Admin-Port spaeter ggf. absichern |
|
| `adguard` | DNS-Server / LAN DNS | `host-services/Adguard/docker-compose.yml` | LAN-Port `53`, Admin `100.80.98.33:8082` | `dns_net`, `frontend_net`, Unbound | `/mnt/user/appdata/adguard/conf`, `/mnt/user/appdata/adguard/work` | Tier 1, config relevant | nein | Direkter DNS-Port 53 bleibt; Admin-Port ist bewusst ohne Traefik/2FA, aber auf Tailscale-IP begrenzt (Operator-Entscheidung 2026-05-26) |
|
||||||
| `unbound` | Upstream DNS Resolver fuer AdGuard | `apps/unbound/docker-compose.yml` | intern | `dns_net` | `/mnt/user/appdata/unbound/config` | rebuildbar / config relevant | nein | intern isoliert |
|
| `unbound` | Upstream DNS Resolver fuer AdGuard | `apps/unbound/docker-compose.yml` | intern | `dns_net` | `/mnt/user/appdata/unbound/config` | rebuildbar / config relevant | nein | intern isoliert |
|
||||||
| `tailscale` | VPN/Remote-Zugang | `host-services/tailscale/docker-compose.yml` | Tailscale | Host-Netz | `/mnt/user/appdata/tailscale` | Tier 1, State relevant | nein | `network_mode: host`, `NET_ADMIN`, `NET_RAW` und `/dev/net/tun` sind dokumentierte VPN-Ausnahmen |
|
| `tailscale` | VPN/Remote-Zugang | `host-services/tailscale/docker-compose.yml` | Tailscale | Host-Netz | `/mnt/user/appdata/tailscale` | Tier 1, State relevant | nein | `network_mode: host`, `NET_ADMIN`, `NET_RAW` und `/dev/net/tun` sind dokumentierte VPN-Ausnahmen |
|
||||||
| `gitea` | Git-Server / origin fuer GitOps | `core/gitea/docker-compose.yml` | `https://git.kaleschke.info`, SSH `222` | Traefik, `frontend_net` | `/mnt/user/services/gitea/data` | Tier 1, SQLite in `/data` | ja | SSH-Port 222 direkte Host-Port-Ausnahme; ohne externen Mirror im DR kritisch |
|
| `gitea` | Git-Server / origin fuer GitOps | `core/gitea/docker-compose.yml` | `https://git.kaleschke.info`, SSH `222` | Traefik, `frontend_net`, externe DNS-Resolver fuer GitHub-Push-Mirror | `/mnt/user/services/gitea/data` | Tier 1, `gitea.sqlite.dump` + Share; privater GitHub-Push-Mirror fuer Repo-Bootstrap | ja | SSH-Port 222 direkte Host-Port-Ausnahme; Push-Mirror nach `michaelkaleschke-spec/homelab-infra` reduziert das DR-Bootstrap-Risiko |
|
||||||
|
|
||||||
## Security / Identity
|
## Security / Identity
|
||||||
|
|
||||||
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||||
|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|
|
||||||
| `authelia` | ForwardAuth / zentrale Auth fuer Admin-UIs | `security/authelia/docker-compose.yml`, `security/authelia/configuration.yml` | `https://auth.kaleschke.info` | PostgreSQL 17, Traefik, GMX SMTP | `/mnt/user/appdata/authelia/config`, Authelia Secret-Dateien | Tier 1, config + DB + secrets | ja | Bewusst ohne Redis-Session-Backend; SMTP-Notifier via GMX und `authelia_smtp_password.txt`; explizite DNS-Server fuer SMTP/NTP; Repo-Baseline muss manuell in die Host-Config gemerged werden, OIDC/Secrets bleiben hostseitig; Access-Control und Compose-Middleware bei Aenderungen abgleichen |
|
| `authelia` | ForwardAuth / zentrale Auth fuer Admin-UIs | `security/authelia/docker-compose.yml`, `security/authelia/configuration.yml` | `https://auth.kaleschke.info` | PostgreSQL 17, Traefik, GMX SMTP | `/mnt/user/appdata/authelia/config`, Authelia Secret-Dateien | Tier 1, config + DB + secrets | ja | Bewusst ohne Redis-Session-Backend; SMTP-Notifier via GMX und `authelia_smtp_password.txt`; explizite DNS-Server fuer SMTP/NTP; Repo-Baseline muss manuell in die Host-Config gemerged werden, OIDC/Secrets bleiben hostseitig; Access-Control und Compose-Middleware bei Aenderungen abgleichen |
|
||||||
| `vaultwarden` | Passwort-Tresor | `security/vaultwarden/docker-compose.yml` | `https://vault.kaleschke.info` | Traefik, `frontend_net` | `/mnt/user/appdata/vaultwarden` | Tier 1 | ja | `ADMIN_TOKEN_FILE`; keine direkten Ports |
|
| `vaultwarden` | Passwort-Tresor | `security/vaultwarden/docker-compose.yml` | `https://vault.kaleschke.info` | Traefik, `frontend_net` | `/mnt/user/appdata/vaultwarden` | Tier 1, `vaultwarden.sqlite.dump` + Share | ja | `ADMIN_TOKEN_FILE`; keine direkten Ports |
|
||||||
|
|
||||||
## Shared Infrastructure
|
## Shared Infrastructure
|
||||||
|
|
||||||
@@ -44,32 +44,46 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
|
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
|
||||||
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB |
|
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB |
|
||||||
| `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 17, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht |
|
| `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 17, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht |
|
||||||
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, Share + app-eigene DB | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
|
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
|
||||||
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres`, `nextcloud_postgres_password.txt` | Teil von Nextcloud-Restore | nein | interne DB |
|
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB |
|
||||||
| `nextcloud-redis` | Nextcloud Cache/Locking | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/redis` | Teil von Nextcloud-Restore | nein | interne Redis |
|
| `nextcloud-redis` | Nextcloud Cache/Locking | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/redis` | Teil von Nextcloud-Restore | nein | interne Redis |
|
||||||
| `ntfy` | Push-Benachrichtigungen | `apps/ntfy/docker-compose.yml` | `https://ntfy.kaleschke.info` | Traefik, upstream mobile push | `/mnt/user/appdata/ntfy` | Tier 2 | ja | `NTFY_BEHIND_PROXY=true`; Monitoring/Borg-Benachrichtigungen |
|
| `plex` | Medienserver mit LAN-/Client-Discovery | `host-services/plex/docker-compose.yml` | Plex native / LAN / Remote je Plex-Konfiguration | Host-Netz | `/mnt/user/appdata/plex/config`, `/mnt/user/appdata/plex/transcode`, `/mnt/user/media`, `/mnt/user/photos` | Tier 2, Appdata + Medienpfade im Borg-/Share-Scope | nein | Repo-Compose-Stack; `network_mode: host` bleibt dokumentierte Discovery-Ausnahme, kein Traefik-Stack |
|
||||||
|
| `ntfy` | Push-Benachrichtigungen | `apps/ntfy/docker-compose.yml` | `https://ntfy.kaleschke.info` | Traefik, upstream mobile push | `/mnt/user/appdata/ntfy` | Tier 2 | ja | `NTFY_BEHIND_PROXY=true`; Problem-Alerts gehen gebuendelt an `homelab-alerts`, optionale Erfolgsmeldungen an `homelab-info` |
|
||||||
| `bentopdf` | PDF-Tooling / Ersatz fuer Stirling-PDF | `apps/bentopdf/docker-compose.yml` | `https://pdf.kaleschke.info` | Traefik + Authelia | keine kritische Persistenz im Compose | Tier 3, rebuildbar | ja + Authelia | COOP/COEP per Middleware; fachliche Abnahme/Live-Status pruefen |
|
| `bentopdf` | PDF-Tooling / Ersatz fuer Stirling-PDF | `apps/bentopdf/docker-compose.yml` | `https://pdf.kaleschke.info` | Traefik + Authelia | keine kritische Persistenz im Compose | Tier 3, rebuildbar | ja + Authelia | COOP/COEP per Middleware; fachliche Abnahme/Live-Status pruefen |
|
||||||
|
|
||||||
## Operations / Monitoring / Admin
|
## Operations / Monitoring / Admin
|
||||||
|
|
||||||
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||||
|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|
|
||||||
| `homepage` | Start-Dashboard | `apps/homepage/docker-compose.yml` | `https://home.kaleschke.info` | Traefik, Docker socket read-only, viele API Tokens | `/mnt/user/appdata/homepage`, `/mnt/user/appdata/homepage/images` | Tier 2 | ja + Authelia laut Compose | Authelia schuetzt die Domain ueber die 1FA-Wildcard-Regel |
|
| `glance` | einziges Homelab-Uebersichts-/Status-Dashboard | `ops/glance/docker-compose.yml`, `ops/glance/config/glance.yml` | `https://glance.kaleschke.info` | Traefik + Authelia, interne HTTP-Checks, Immich API, AdGuard API, Speedtest API, interner Docker-Socket-Proxy | Repo-Konfiguration; keine kritische Persistenz | Tier 3, rebuildbar | ja + Authelia | Zeigt aktive Dienste, Immich-Community-Widget, Internet-/DNS-/VPN-Monitore, AdGuard-DNS-Stats, Speedtest-Livewerte, Docker-Containergruppen, Server-Stats, Zeitfortschritt, Bookmarks und eine Infrastruktur-Seite; Docker-API nur ueber `glance-docker-socket-proxy` auf internem Netz |
|
||||||
| `komodo-core` | GitOps UI/API/Stack-Manager | `ops/komodo/docker-compose.yml` | `https://komodo.kaleschke.info` | Mongo, Gitea, Traefik | `/mnt/user/appdata/komodo/core`, `komodo_keys` | Tier 1 | ja, native Auth | keine pauschale Authelia-ForwardAuth; Gitea DNS override |
|
| `komodo-core` | GitOps UI/API/Stack-Manager | `ops/komodo/docker-compose.yml` | `https://komodo.kaleschke.info` | Mongo, Gitea, Traefik | `/mnt/user/appdata/komodo/core`, `komodo_keys` | Tier 1 | ja, native Auth | keine pauschale Authelia-ForwardAuth; Gitea DNS override |
|
||||||
| `komodo-mongo` | Komodo Datenbank | `ops/komodo/docker-compose.yml` | intern | `komodo_net` | `/mnt/user/appdata/komodo/mongo`, `komodo_mongo_password.txt` | Tier 1, `komodo-mongo.archive.gz` | nein | Dump am 2026-05-04 bestaetigt; nach Major-Upgrades pruefen |
|
| `komodo-mongo` | Komodo Datenbank | `ops/komodo/docker-compose.yml` | intern | `komodo_net` | `/mnt/user/appdata/komodo/mongo`, `komodo_mongo_password.txt` | Tier 1, `komodo-mongo.archive.gz` | nein | Dump am 2026-05-04 bestaetigt; nach Major-Upgrades pruefen |
|
||||||
| `komodo-periphery` | Komodo Host-Agent | `ops/komodo/docker-compose.yml` | intern Core -> Periphery | Docker socket, `/mnt/user/services`, `frontend_net`, `komodo_net` | `/mnt/user/appdata/komodo/periphery`, `komodo_keys` | Tier 1 | nein | Docker-Socket-Ausnahme; `/mnt/user/services` Mount fuer Stack-Workspaces |
|
| `komodo-periphery` | Komodo Host-Agent | `ops/komodo/docker-compose.yml` | intern Core -> Periphery | Docker socket, `/mnt/user/services`, `frontend_net`, `komodo_net` | `/mnt/user/appdata/komodo/periphery`, `komodo_keys` | Tier 1 | nein | Docker-Socket-Ausnahme; `/mnt/user/services` Mount fuer Stack-Workspaces |
|
||||||
| `borg-ui` | Borg Backup-/Restore UI | `ops/borg-ui/docker-compose.yml` | `https://borg.kaleschke.info` | Traefik + Authelia, Borg repo credentials | `/mnt/user/appdata/borg-ui/data`, `/mnt/user/backups/borg/dumps`, Restore-Ziel | Tier 3 / Backup kritisch | ja + Authelia | breite Mounts bewusst; `/local/secrets` im DR-Scope |
|
| `borg-ui` | Borg Backup-/Restore UI | `ops/borg-ui/docker-compose.yml` | `https://borg.kaleschke.info` | Traefik + Authelia, Borg repo credentials | `/mnt/user/appdata/borg-ui/data`, `/mnt/user/backups/borg/dumps`, Restore-Ziel | Tier 3 / Backup kritisch, `borg-ui.sqlite` | ja + Authelia | breite Mounts bewusst; `/local/secrets` im DR-Scope; Nextcloud-Daten werden read-only nach `/local/nextcloud/data` eingebunden |
|
||||||
| `backrest` | Backup-Admin-Dienst / Legacy-Backup-Ebene | `ops/backrest/docker-compose.yml` | `https://backrest.kaleschke.info` | Traefik + Authelia, Repo/SSH-Mounts | `/mnt/user/appdata/backrest/*`, broad source mounts | Tier 3 | ja + Authelia | breite Mounts bewusst dokumentieren |
|
| `glances` | System-/Container-Monitoring | `ops/glances/docker-compose.yml` | `https://glances.kaleschke.info` | Docker socket, rootfs, Traefik + Authelia | kein kritischer Zustand | Tier 3, rebuildbar | ja + Authelia | Dokumentierte Host-Observability-Ausnahme: `pid: host`, `/:/rootfs:ro`, `/var/run/docker.sock:/var/run/docker.sock:ro`, `/etc/os-release:/etc/os-release:ro`; keine Appdaten ausserhalb `/mnt/user/...` |
|
||||||
| `uptime-kuma` | Monitoring / Uptime Checks | `ops/uptime-kuma/docker-compose.yml` | `https://uptime.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/uptime-kuma` | Tier 3 | ja + Authelia | Monitore nach Restore pruefen |
|
| `scrutiny` | Laufwerks-/SMART-Monitoring | `ops/scrutiny/docker-compose.yml` | `https://scrutiny.kaleschke.info` | Device mounts, Traefik + Authelia | `/mnt/user/appdata/scrutiny/config`, `/mnt/user/appdata/scrutiny/influxdb` | Tier 3, Metrics nicht kritisch | ja + Authelia | Dokumentierte Host-Observability-Ausnahme: `privileged: true`, `/run/udev:/run/udev:ro`, `/dev/sdb:/dev/sdb`, `/dev/sdc:/dev/sdc`, `/dev/nvme0n1:/dev/nvme0n1`; keine Appdaten ausserhalb `/mnt/user/...` |
|
||||||
| `glances` | System-/Container-Monitoring | `ops/glances/docker-compose.yml` | `https://glances.kaleschke.info` | Docker socket, rootfs, Traefik + Authelia | kein kritischer Zustand | Tier 3, rebuildbar | ja + Authelia | Rootfs und Docker-Socket Mounts |
|
| `speedtest-tracker` | Speedtest-Monitoring | `ops/speedtest/docker-compose.yml` | `https://speedtest.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/speedtest-tracker/config` | Tier 3, `speedtest-tracker.sqlite.dump` | ja + Authelia | `APP_KEY`, `ADMIN_PASSWORD` Stack ENV |
|
||||||
| `scrutiny` | Laufwerks-/SMART-Monitoring | `ops/scrutiny/docker-compose.yml` | `https://scrutiny.kaleschke.info` | Device mounts, Traefik + Authelia | `/mnt/user/appdata/scrutiny/config`, `/mnt/user/appdata/scrutiny/influxdb` | Tier 3, Metrics nicht kritisch | ja + Authelia | `privileged: true` dokumentierte Ausnahme |
|
| `filebrowser` | Datei-Browser fuer Documents/Photos/Projekte | `ops/filebrowser/docker-compose.yml` | `https://files.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/filebrowser/*`, `/mnt/user/documents`, `/mnt/user/photos`, `/mnt/user/projekte` | Tier 3, `filebrowser.bolt.dump` + Share | ja + Authelia | Breiter Appdata-Mount entfernt; Secrets und Traefik-Dynamic-Config sind nicht mehr ueber Filebrowser gemountet |
|
||||||
| `speedtest-tracker` | Speedtest-Monitoring | `ops/speedtest/docker-compose.yml` | `https://speedtest.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/speedtest-tracker/config` | Tier 3 | ja + Authelia | `APP_KEY`, `ADMIN_PASSWORD` Stack ENV |
|
| `code-server` | Web-Editor / Operations Workspace | `ops/code-server/docker-compose.yml` | `https://code.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/code-server`, `/mnt/user/services/dev` | Tier 3 | ja + Authelia | Passwort ueber LSIO `FILE__PASSWORD`; Workspaces beachten |
|
||||||
| `filebrowser` | Datei-Browser fuer Appdata | `ops/filebrowser/docker-compose.yml` | `https://files.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/filebrowser/*`, breiter `/mnt/user/appdata` Mount | Tier 3 | ja + Authelia | Mounts langfristig einschraenken |
|
| `monitoring-grafana` | zentrale Observability-UI fuer Metriken, Logs und InfluxDB | `monitoring/docker-compose.yml` | `https://monitoring.kaleschke.info` | Traefik + Authelia, Prometheus, Loki, InfluxDB 3 Core | named volume `grafana_data`, Provisioning unter `monitoring/grafana/provisioning`, Dashboards unter `monitoring/grafana/dashboards` | Tier 3, named volume | ja + Authelia | Admin-Passwort ueber `monitoring_grafana_admin_password.txt`; Zielbestand: `Homelab / Availability`, `Homelab / Host Overview`, `Homelab / Containers + Logs`, `Traefik Official Standalone Dashboard`; Dashboard-Importer ist optionales `bootstrap`-Profil fuer Traefik |
|
||||||
| `code-server` | Web-Editor / Operations Workspace | `ops/code-server/docker-compose.yml` | `https://code.kaleschke.info` | Traefik + Authelia | `/mnt/user/appdata/code-server`, `/mnt/user/services/dev` | Tier 3 | ja + Authelia | `PASSWORD_FILE`; Workspaces beachten |
|
| `monitoring-prometheus` | Metrik-Speicher fuer Homelab-Monitoring | `monitoring/docker-compose.yml`, `monitoring/prometheus/prometheus.yml`, `monitoring/prometheus/alerts.yml` | intern `http://prometheus:9090` | `monitoring_net`, node-exporter, cAdvisor, Traefik-Metrics, Blackbox Exporter, Alertmanager | named volume `prometheus_data` | Tier 3, transiente Metriken mit 30 Tagen Retention | nein | Scrapes: Prometheus, node-exporter, cAdvisor, Traefik `:8082`, `blackbox-http`; Prometheus-Regeln senden an Alertmanager und von dort nach ntfy |
|
||||||
| `grafana` | Metrik-Dashboard | `ops/grafana-influxdb/docker-compose.yml` | `https://grafana.kaleschke.info` | Traefik + Authelia, InfluxDB 3 Core | `/mnt/user/appdata/grafana`, Grafana provisioning | Tier 3 | ja + Authelia | Datasource wird provisioniert, Token ueber Secret; laeuft aktuell als `user: "0"` wegen Host-Appdata-Permissions |
|
| `monitoring-alertmanager` | Alert-Routing fuer Prometheus-Regeln | `monitoring/docker-compose.yml`, `monitoring/alertmanager/alertmanager.yml` | intern `:9093` | Prometheus, ntfy Bridge | named volume `alertmanager_data` | Tier 3 | nein | sendet firing und resolved Alerts an `monitoring-alertmanager-ntfy-bridge` |
|
||||||
| `influxdb3-core` | Zeitreihen-/Metrikdaten fuer Grafana und Home Assistant | `ops/grafana-influxdb/docker-compose.yml` | LAN `8181` je `INFLUXDB_BIND_IP`, keine Public URL | Grafana, Home Assistant Writer | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | Tier 3 | nein | LAN-only Host-Port-Ausnahme; `401 Unauthorized` beim Curl ohne Token ist erwarteter Reachability-Test; laeuft aktuell als `user: "0"` wegen Host-Appdata-Permissions |
|
| `monitoring-alertmanager-ntfy-bridge` | Alertmanager-Webhook nach ntfy Push | `monitoring/docker-compose.yml`, `monitoring/alertmanager-ntfy-bridge/bridge.py` | intern `:8080` | Alertmanager, `https://ntfy.kaleschke.info/homelab-alerts` | kein kritischer Zustand | rebuildbar | nein | formatiert Alertmanager JSON als ntfy Titel, Nachricht, Priority und Tags; keine Secrets |
|
||||||
| `hermes-gateway` | Hermes Agent Gateway/API intern | `ops/hermes-agent/docker-compose.yml` | intern `8642` auf `hermes_net` | SSH Runner, LLM Provider, optional Home Assistant | `/mnt/user/appdata/hermes-agent/data`, SSH key path | Tier 3, Borg/Share | nein | kein Docker socket; terminal backend `ssh`; echte `.env` auf Host-Appdata |
|
| `monitoring-blackbox-exporter` | HTTP-Erreichbarkeitspruefungen als Uptime-Kuma-Ersatz | `monitoring/docker-compose.yml`, `monitoring/blackbox/blackbox.yml` | intern `:9115` | Prometheus, externe HTTPS-Ziele | kein kritischer Zustand | rebuildbar | nein | Uptime Kuma wurde 2026-05-25 nach erfolgreichem Blackbox-/Grafana-Smoke-Test entfernt |
|
||||||
| `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`; bindet intern mit `--insecure` auf `0.0.0.0`, externe Absicherung bleibt Authelia |
|
| `monitoring-loki` | Logspeicher fuer Monitoring-Stack | `monitoring/docker-compose.yml`, `monitoring/loki/loki-config.yml` | intern `http://loki:3100` | `monitoring_net`, Promtail, Grafana | named volume `loki_data` | Tier 3, transiente Logs mit 30 Tagen Retention | nein | Ersetzt den alten `ops/loki`-Stack; kein paralleler Altcontainer aktiv |
|
||||||
|
| `monitoring-promtail` | Docker-Log-Collector fuer Monitoring-Loki | `monitoring/docker-compose.yml`, `monitoring/promtail/promtail-config.yml` | intern | Docker socket read-only, Docker json-file Logs, Loki | named volume `promtail_positions` | rebuildbar | nein | Dokumentierte Host-Observability-Ausnahme: `/var/run/docker.sock:/var/run/docker.sock:ro` und `/var/lib/docker/containers:ro`; keine Appdaten, nur Log-Discovery |
|
||||||
|
| `monitoring-node-exporter` | Host-Metriken fuer Prometheus | `monitoring/docker-compose.yml` | intern `:9100` | Host `/proc`, `/sys`, `/` read-only, Prometheus | kein kritischer Zustand | rebuildbar | nein | Host-Observability-Ausnahme mit read-only Rootfs/Proc/Sys-Mounts |
|
||||||
|
| `monitoring-cadvisor` | Container-Metriken fuer Prometheus | `monitoring/docker-compose.yml` | intern `:8080` | Docker/Host read-only Mounts, Prometheus | kein kritischer Zustand | rebuildbar | nein | Host-Observability-Ausnahme fuer Container-Metriken; keine direkten Ports |
|
||||||
|
| `monitoring-influxdb3-core` | InfluxDB 3 Core fuer Home-Assistant-/Ecowitt-Langzeitdaten | `monitoring/docker-compose.yml` | LAN `8181` je `INFLUXDB_BIND_IP`, keine Public URL | Monitoring-Grafana, Home Assistant Writer | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | Tier 3 | nein | LAN-only Host-Port-Ausnahme; `user: "0"` ist fuer den lokalen Object-Store-Pfad dokumentiert; uebernimmt den bisherigen InfluxDB-Daten-/Token-Katalog; `401 Unauthorized` beim Curl ohne Token ist erwarteter Reachability-Test |
|
||||||
|
| `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 |
|
||||||
|
|
||||||
|
## Host Operations
|
||||||
|
|
||||||
|
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| `posture-check` | Host-Posture-Audit fuer Filesystem, Mover-Drift, NVMe-SMART und Fuellstand | `services/posture-check/posture-check.sh` | Unraid User-Script / Cron / Borg Pre-Hook | `findmnt`, `df`, `nvme`, optional `curl` fuer ntfy | `/mnt/user/services/posture-check/last.json` | Repo-Skript + letzter JSON-Status | nein | Muss auf dem Unraid-Host bei Boot, stuendlich und vor Borg laufen; Disk1-NTFS ist nach Disk1 Phase 2 nicht mehr erlaubt (`ALLOW_DISK1_NTFS=0` Standard); Warning/Critical alarmieren via ntfy nur bei neuer Ursache oder nach `ALERT_REPEAT_SECONDS` |
|
||||||
|
| `docker-critical-events` | Live-Alarmierung fuer Docker `die`/`oom`/`kill` Events | `services/posture-check/docker-critical-events.sh` | Unraid User-Script / Hintergrundprozess | Docker CLI, ntfy | `/mnt/user/services/posture-check/docker-critical-events-last.log` | Repo-Skript + letzter Event-Log | nein | Optional als Unraid User-Script `at array start` starten; sendet nach `homelab-alerts` |
|
||||||
|
|
||||||
## Backup- und Restore-Hinweise
|
## Backup- und Restore-Hinweise
|
||||||
|
|
||||||
@@ -79,9 +93,17 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
- Raw Live-DB-Pfade sind meist nicht der primaere Restore-Weg; bevorzugt werden Dumps plus Appdaten.
|
- Raw Live-DB-Pfade sind meist nicht der primaere Restore-Weg; bevorzugt werden Dumps plus Appdaten.
|
||||||
- Borg UI nimmt bewusst `/local/secrets` in den DR-Scope auf.
|
- Borg UI nimmt bewusst `/local/secrets` in den DR-Scope auf.
|
||||||
|
|
||||||
|
## Logging-Resilienz
|
||||||
|
|
||||||
|
- Docker-Rohlogs bleiben die erste Fallback-Ebene: `docker logs <container>` funktioniert unabhaengig von Loki/Grafana.
|
||||||
|
- Loki-Chunks liegen unter `/mnt/user/appdata/loki/data` und koennen durch Neustart des internen Loki-Stacks wieder genutzt werden.
|
||||||
|
- Host-Logs wie `/var/log/syslog` und `dmesg` bleiben die Quelle fuer Kernel-, OOM- und Docker-Daemon-Ereignisse.
|
||||||
|
- Critical-Events werden zusaetzlich ueber ntfy extern sichtbar, wenn `services/posture-check/docker-critical-events.sh` als Host-Watcher laeuft.
|
||||||
|
- Docker `json-file` Logs werden auf Unraid nativ ueber `/boot/config/docker.cfg` begrenzt. Aktiver Host-Stand: `DOCKER_LOG_ROTATION="yes"`, `DOCKER_LOG_SIZE="50m"`, `DOCKER_LOG_FILES="1"`. Keine zusaetzliche `/etc/docker/daemon.json` setzen, weil sie mit Unraids Docker-Startflags kollidieren kann.
|
||||||
|
|
||||||
## Bekannte offene Fragen
|
## Bekannte offene Fragen
|
||||||
|
|
||||||
- Authelia Repo-Baseline, Host-Config und Compose-Middlewares sollten bei Auth-Aenderungen explizit abgeglichen werden.
|
- Authelia Repo-Baseline, Host-Config und Compose-Middlewares sollten bei Auth-Aenderungen explizit abgeglichen werden.
|
||||||
- Filebrowser- und Backrest-Mounts sind breit und bewusst, aber bei zukuenftigen Hardening-Sprints Kandidaten.
|
- Filebrowser-Mounts sind breit und bewusst, aber bei zukuenftigen Hardening-Sprints Kandidaten.
|
||||||
- Scrutiny bleibt privilegiert; nur mit klarer Begruendung aendern.
|
- Scrutiny bleibt privilegiert; nur mit klarer Begruendung aendern.
|
||||||
- BentoPDF kann je nach Live-Stand vorbereitet statt produktiv sein. Hermes Dashboard ist produktiv unter `hermes.kaleschke.info`, aber noch nicht vollstaendig in Restore Matrix / DR Bootstrap eingeordnet.
|
- BentoPDF ersetzt Stirling-PDF produktiv. Hermes ist VM-seitig offen und auf dem NAS bewusst nicht gestartet.
|
||||||
|
|||||||
@@ -0,0 +1,415 @@
|
|||||||
|
# Storage Layout — KalliLab CORE
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|------|------|
|
||||||
|
| Version | 1.3 |
|
||||||
|
| Status | **Active** — bindend, commit-reif als `docs/STORAGE_LAYOUT.md` |
|
||||||
|
| Datum | 2026-05-15 |
|
||||||
|
| Geltungsbereich | Unraid-Host `Kallilabcore`, alle Pools, Array-Disks, User-Shares, Appdata-Strukturen |
|
||||||
|
| Verbindlichkeit | Bindend ab Inkraftsetzung. Abweichungen nur dokumentiert als Ausnahme (siehe Abschnitt 12) |
|
||||||
|
| Vorgänger | keiner; entstanden aus Incident 2026-05-11 (NTFS-Cache-Korruption) |
|
||||||
|
| Verwandte Docs | `HOMELAB_ARCHITECTURE_MASTER_V2.md`, `docs/REPO_MAP.md`, `docs/SERVICE_CATALOG.md`, `docs/RESTORE_MATRIX.md`, `docs/SECRETS_MAP.md`, `docs/DISASTER_RECOVERY.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Zweck
|
||||||
|
|
||||||
|
Dieses Dokument definiert die verbindliche Storage-Architektur für den KalliLab-CORE-Homelab-Stack. Es beantwortet:
|
||||||
|
|
||||||
|
- Welche physikalischen Disks gibt es, mit welchem Filesystem.
|
||||||
|
- Welche User-Shares existieren, mit welchen Cache- und Allokations-Settings.
|
||||||
|
- Wo liegen Container-Daten und in welcher Sub-Struktur.
|
||||||
|
- Welche Pfade dürfen in Compose-Dateien als Bind-Mount auftauchen, welche nie.
|
||||||
|
- Welche Backup- und Posture-Check-Pflichten daraus folgen.
|
||||||
|
|
||||||
|
Es ist **vor** jeder Storage- oder Compose-Änderung zu lesen. Wenn ein neuer Stack, ein neuer Pfad, eine neue Disk, ein neuer Share gebraucht wird und hier nicht spezifiziert ist, wird dieses Dokument **erst** ergänzt, dann implementiert.
|
||||||
|
|
||||||
|
## 2. Leitprinzipien
|
||||||
|
|
||||||
|
1. **Filesystem-Semantik ist nicht verhandelbar.** Datenbanken, Container-Persistenz und Array-Daten brauchen Linux-natives Filesystem (XFS, BTRFS, ZFS). NTFS, exFAT, FAT32 sind ausschließlich für Boot-USB und externe Austauschmedien zulässig, niemals für Cache, Array oder Pool.
|
||||||
|
2. **Recovery > Convenience.** Eine Konvention, die Restore vereinfacht, schlägt jede Konvention, die nur die Erstinstallation vereinfacht.
|
||||||
|
3. **Posture-Check als First-Class-Concern.** Jede Annahme über den Host-Zustand (FS-Typ, Mount, Permissions) muss automatisiert prüfbar sein. „Wir wissen schon, dass das stimmt" ist keine zulässige Begründung.
|
||||||
|
4. **GitOps-Konformität.** Alles, was Compose-, Share-, oder Storage-Verhalten beeinflusst, ist im Repo dokumentiert oder versioniert. Was nur im Host lebt, existiert für Recovery nicht.
|
||||||
|
5. **Pfad-Stabilität.** Pfade in Compose-Bind-Mounts bleiben stabil über Filesystem-Migrationen, Disk-Wechsel und Pool-Umzüge hinweg. Konkret: Container kennen `/mnt/user/...`-Pfade, nie `/mnt/cache/...` oder `/mnt/disk1/...`.
|
||||||
|
6. **Keine Sammelordner.** Eine Datei hat genau einen logischen Eigentümer (Stack, Share, Nutzkategorie). Mischungen erzeugen Drift und brechen Backup-Excludes.
|
||||||
|
7. **Bewusste Heterogenität nur mit Begründung.** Wenn ein Stack vom Standard abweicht (z. B. Host-Network, Privileged, Direct-Cache-Mount), ist die Abweichung in Abschnitt 12 dokumentiert mit Begründung und Risiko-Akzeptanz.
|
||||||
|
|
||||||
|
## 3. Physikalisches Layout (Soll-Zustand)
|
||||||
|
|
||||||
|
| Slot | Device | Filesystem | Größe | Zweck | Status nach Recovery |
|
||||||
|
|------|--------|------------|-------|-------|----------------------|
|
||||||
|
| Cache (Pool) | Samsung 970 EVO Plus, NVMe | **XFS** | 2 TB | Appdata, system, domains | Reformat von NTFS auf XFS, Phase 1 |
|
||||||
|
| Disk1 (Array) | HDD (Modell TBD) | **XFS** | TBD | Nutzdaten, Backups, Services | NTFS-zu-XFS-Migration Phase 2 abgeschlossen am 2026-05-25 |
|
||||||
|
| Parity | HDD (Modell TBD) | — (keine FS) | TBD | Redundanz für Array | Unverändert |
|
||||||
|
| Boot | USB-Stick | FAT32 | klein | Unraid-OS, Konfiguration | Unverändert, regelmäßig per Flash-Backup gesichert |
|
||||||
|
| Externe Backup-Platte | Wechselplatte (Modell TBD) | XFS oder ext4 | ~8 TB | Ausgelagertes Backup-Ziel, Recovery-Material | NEU, ersetzt WD MyBookLive |
|
||||||
|
|
||||||
|
**TBD-Felder werden nachgetragen** sobald `lsblk -o NAME,SIZE,MODEL,SERIAL,VENDOR /dev/sd?` und `smartctl -i` für jeden Slot einmal ausgeführt wurden. Vorschlag: dieser Detection-Lauf ist Teil der Posture-Check-Erstinstallation (§11) und das Ergebnis wird automatisch in dieses Dokument eingetragen. Kein Blocker für die Inkraftsetzung — die Festsetzung „XFS auf Cache, XFS auf Disk1" steht unabhängig von Modell/Größe.
|
||||||
|
|
||||||
|
**Begründung Filesystem-Wahl:**
|
||||||
|
|
||||||
|
- **XFS für Cache und Disk1:** Unraid-Standard, robuste fsck, gute Performance für Datenbank-Workloads, niedrige Komplexität, einheitlicher Stack vereinfacht Operations.
|
||||||
|
- **BTRFS bewusst nicht** für Single-Disk-Cache: Mehrwerte (RAID1, Snapshots) werden ohne zweite NVMe nicht realisiert; zusätzlicher COW-Overhead und Komplexität ohne Gegenwert.
|
||||||
|
- **ZFS bewusst nicht** für aktuellen Bedarf: höherer RAM-Verbrauch, in der bestehenden Topologie kein Mehrwert; spätere Migration eines dedizierten Pools auf ZFS bleibt offen (siehe Abschnitt 13).
|
||||||
|
|
||||||
|
**Eskalationspfad bei zweiter Cache-NVMe:** Migration zu BTRFS-Pool (RAID1) als bewusste Architektur-Entscheidung mit eigenem Migrations-Plan. Bis dahin: Single-XFS.
|
||||||
|
|
||||||
|
## 4. Logisches Layout — User-Shares
|
||||||
|
|
||||||
|
Verbindliche Share-Definition. Jeder Share hat genau einen primären Datenträger.
|
||||||
|
|
||||||
|
| Share | Cache-Setting | Primary Storage | Allocation | Split Level | Zweck |
|
||||||
|
|-------|---------------|-----------------|------------|-------------|-------|
|
||||||
|
| `appdata` | **only** | Cache | High Water | 1 | Container-Persistenz, niemals auf Array |
|
||||||
|
| `system` | **only** | Cache | High Water | — | Docker-Image bzw. Folder-Mode-Verzeichnis, libvirt — bewusst `only`, weil Mover-Migration der Docker-Image-Datei oder von libvirt-State zwischen FS-Typen riskant ist |
|
||||||
|
| `domains` | **only** | Cache | High Water | 1 | VM-Disk-Images — Performance und Stabilität für produktive VMs (HAOS, hermes-runner). Wenn ältere/seltene VM-Images aufs Array dürfen, bewusst auf `prefer` herabsetzen, dokumentiert pro VM |
|
||||||
|
| `isos` | yes | Cache → Array | High Water | 2 | VM-Installations-ISOs, optional |
|
||||||
|
| `services` | no | Disk1 | High Water | 2 | Gitea, Komodo-State, secrets-Pfad — **recovery-kritisch**, siehe Hinweis unter Tabelle |
|
||||||
|
| `documents` | no | Disk1 | High Water | 2 | Persönliche Dokumente |
|
||||||
|
| `photos` | no | Disk1 | High Water | 2 | Persönliche Fotos |
|
||||||
|
| `backups` | no | Disk1 | High Water | 2 | Lokale Borg-Repos, Appdata-Backup-Archive |
|
||||||
|
| `media` | no | Disk1 | High Water | 2 | Optional, Media-Files |
|
||||||
|
| `finance` | no | Disk1 | High Water | 2 | Persönlich |
|
||||||
|
| `projekte` | no | Disk1 | High Water | 2 | Persönlich |
|
||||||
|
|
||||||
|
**Erklärung der Cache-Settings:**
|
||||||
|
|
||||||
|
- `only`: Daten landen ausschließlich auf Cache, Mover ignoriert sie. Schutz vor Mover-induzierter Daten-Migration zwischen Filesystemen.
|
||||||
|
- `prefer`: Neue Daten landen auf Cache, Mover schiebt sie bei Cache-Druck auf Array.
|
||||||
|
- `yes`: Neue Daten landen auf Cache, Mover schiebt sie regelmäßig auf Array (klassisches Cache-Schreibverhalten).
|
||||||
|
- `no`: Daten umgehen Cache, gehen direkt auf Array.
|
||||||
|
|
||||||
|
**Wahl `only` für `appdata`, `system`, `domains`** ist die wichtigste Setting in diesem Dokument. Sie verhindert genau die Klasse von Vorfällen, die im Mai 2026 passiert ist: dass Container-Daten, Docker-Image-State oder VM-Disks unbemerkt zwischen Filesystemen wandern. Wenn der Cache voll wird, ist das ein Kapazitätsproblem, kein Filesystem-Problem — und wird durch zweite NVMe oder Cache-Tuning gelöst, nicht durch Mover-Migration.
|
||||||
|
|
||||||
|
**`services` ist recovery-kritisch.** Gitea (operative Source of Truth des GitOps-Stacks) und Komodo-State (Deployment-Runtime) liegen dort. Ohne `services` ist Self-Recovery nicht mehr möglich. Daraus folgt:
|
||||||
|
|
||||||
|
- `services` wird vor jedem strukturellen Eingriff am Array (Disk1-Migration Phase 2, Disk-Tausch, Pool-Umzug) **vollständig** auf zweites Medium gesichert
|
||||||
|
- `services` darf nicht auf einem als instabil bekannten Datenträger leben — Warnung in `posture-check`, Eskalation bei Disk1-FS-Anomalien
|
||||||
|
- Änderungen an Gitea-Repos, Komodo-Stacks oder secrets-Pfad nur nach erfolgreichem letzten Backup-Check (siehe §11)
|
||||||
|
- **Mirror-Backup für Gitea-Repo-Inhalte (Operator-Entscheidung 2026-05-15, verbindlich):** zusätzlich zum Standard-Borg-Lauf wird `services/gitea/git/repositories/` per separatem Mirror-Mechanismus auf ein zweites physisches Medium außerhalb Disk1 gesichert, mit Frequenz ≤ 6 h. Konkrete Implementierung (Optionen: `git bundle`-Bundles in separates Repo auf Cache, `rsync` auf externe Platte, separates Borg-Repo mit kürzerem Schedule) wird in `docs/SERVICES_RECOVERY.md` (zu erstellen) detailliert. Begründung: Gitea ist operative Source of Truth des GitOps; Verlust seit letztem Standard-Backup (bis zu 24 h) wäre für Recovery-Pfad unzumutbar.
|
||||||
|
|
||||||
|
**Erklärung Allocation-Method:** „High Water" verteilt neue Daten auf der am wenigsten gefüllten Disk bis zu einer Wasserlinie, dann auf die nächste. Praktisch unauffällig bei Single-Array-Disk; wird relevant bei Erweiterung.
|
||||||
|
|
||||||
|
**Split Level** bestimmt, wie tief Verzeichnisse über Disks aufgeteilt werden dürfen. Für `appdata` zwingend `1` — ein Stack-Verzeichnis bleibt auf einer Disk, wird nie aufgespalten.
|
||||||
|
|
||||||
|
## 5. Appdata-Sub-Struktur
|
||||||
|
|
||||||
|
Verbindliches Layout pro Stack:
|
||||||
|
|
||||||
|
```
|
||||||
|
/mnt/user/appdata/<stackname>/
|
||||||
|
├── config/ # Statische Konfiguration (yaml, conf, env-Dateien)
|
||||||
|
├── data/ # Live-Datenverzeichnis (DB-Datendir, Container-Persistenz)
|
||||||
|
├── dumps/ # Pre-Backup-Hooks schreiben hier (pg_dump, mongodump, sqlite .backup)
|
||||||
|
├── logs/ # Container-Logs, falls nicht via journald gelöst
|
||||||
|
└── README.md # Pflicht: Kurzbeschreibung, Restore-Zeiger, Sondereigenheiten
|
||||||
|
```
|
||||||
|
|
||||||
|
**Regeln:**
|
||||||
|
|
||||||
|
- Ein Verzeichnis pro Stack, kein Mischen mehrerer Stacks
|
||||||
|
- Stack-Namen sind lowercase, Bindestriche statt Unterstriche (z. B. `mail-archiver`, nicht `Mail_Archiver`)
|
||||||
|
- `data/` enthält Live-State. **Behandlung pro Stack klassifiziert in `RESTORE_MATRIX.md`:**
|
||||||
|
- **Reines DB-Datenverzeichnis** (z. B. raw Postgres-`data/`, raw Mongo-`data/`): wird **nicht** direkt gesichert, ist durch `dumps/` abgedeckt
|
||||||
|
- **Echte Nutzdaten** (z. B. Immich-Uploads, Paperless-Dokumente, Gitea-Repo-Inhalte, Vaultwarden-Attachments, Mealie-Bilder, Grafana-Dashboards/Provisioning): **werden direkt gesichert** oder über expliziten Export-Mechanismus erfasst
|
||||||
|
- **Mischformen** (z. B. ein Stack mit DB-Files und Nutzdaten im selben `data/`): pro Stack im Detail spezifiziert, mit getrennten Sub-Pfaden wo möglich
|
||||||
|
- `dumps/` enthält von Pre-Hooks erzeugte konsistente Dumps; **wird immer von Borg gesichert**
|
||||||
|
- `config/` ist die einzige Quelle der Container-Konfiguration; idealerweise versioniert oder aus Git deploybar
|
||||||
|
- `README.md` pro Stack: drei Zeilen genügen (was, warum, Restore-Zeiger)
|
||||||
|
|
||||||
|
**Ausnahmen, wenn Container das Layout nicht zulassen:** dokumentiert in `docs/SERVICE_CATALOG.md` pro Stack, mit Verweis hier.
|
||||||
|
|
||||||
|
## 6. Naming-Konventionen
|
||||||
|
|
||||||
|
| Element | Konvention | Beispiel |
|
||||||
|
|---------|------------|----------|
|
||||||
|
| Stack-Name | `kebab-case`, lowercase, keine Sonderzeichen | `vaultwarden`, `mail-archiver`, `immich-server` |
|
||||||
|
| Container-Name | identisch zu Stack-Name (oder `<stack>-<role>`), kebab-case | `postgresql-17`, `immich-postgres`, `komodo-mongo` |
|
||||||
|
| Volume-Pfad in Compose | `/mnt/user/appdata/<stack>/<sub>` | `/mnt/user/appdata/vaultwarden/data:/data` |
|
||||||
|
| Netz-Name | `frontend_net`, `backend_net`, oder `<stack>_internal` | `frontend_net`, `immich_internal` |
|
||||||
|
| Env-Datei | `<stack>.env`, im Komodo-Stack-Workspace, **nicht** im Repo | — |
|
||||||
|
|
||||||
|
**Begründung Bindestriche:** vermeiden Verwechslung mit Shell-Variablen (Unterstriche werden in env vars verwendet), passen besser zu Docker-Compose-Konventionen, bleiben URL-safe.
|
||||||
|
|
||||||
|
**Migration Bestand → kebab-case (Operator-Entscheidung 2026-05-15):** existierende Stacks mit Unterstrichen oder gemischten Konventionen (`postgresql17`, `immich_postgres`, `komodo-mongo` etc.) werden im Rahmen der Recovery-Phase (Cache-Restore Phase 1, Disk1-Migration Phase 2) auf kebab-case migriert. Pro Stack: alter Name → neuer Name in `docs/RESTORE_MATRIX.md` dokumentiert. Container-Renames erfolgen sauber per Compose-Down → Rename → Compose-Up mit neuer Identität, **nicht** im laufenden Betrieb. Netzwerk-Referenzen, Healthchecks und Inter-Stack-Abhängigkeiten werden im selben Schritt mit angepasst. Nicht-migrierte Stacks am Ende der Recovery-Phase werden in §20 als bewusste Ausnahme dokumentiert oder auf einen Folge-Termin verschoben.
|
||||||
|
|
||||||
|
## 7. Permissions-Modell
|
||||||
|
|
||||||
|
| Anwendungsfall | UID:GID | Begründung |
|
||||||
|
|----------------|---------|------------|
|
||||||
|
| Standard-Files in `appdata/` | `99:100` (`nobody:users`) | Unraid-Standard, von shfs/Mover sauber behandelt |
|
||||||
|
| Container-spezifischer User | individuell, dokumentiert | manche Images erwarten zwingend `1000:1000` o. ä. |
|
||||||
|
| `secrets/`-Verzeichnis | `0:0`, mode `0700` oder `0750` | Schutz vor versehentlichem Read |
|
||||||
|
| `logs/` | `99:100`, mode `0755` | Lesbar für Debugging, nicht weltschreibbar |
|
||||||
|
| `dumps/` | `99:100`, mode `0750` | Backup-Hook schreibt, Borg liest |
|
||||||
|
|
||||||
|
**Regel:** Permission-Setzung beim Stack-Restore ist explizit per Skript oder Init-Container, nicht „erstmal `chown -R root:root` und gucken was kaputtgeht". Dokumentiert pro Stack.
|
||||||
|
|
||||||
|
## 8. Backup-Architektur
|
||||||
|
|
||||||
|
### 8.0 Aktueller Ist-Zustand vs. Ziel-Zustand
|
||||||
|
|
||||||
|
**Operator-Entscheidung 2026-05-15:** Backrest wird abgeschaltet. **Borg ist die alleinige Backup-Technologie.** Restic-Repos werden nicht weiterentwickelt; vorhandene Restic-Daten verbleiben als historisches Material bis zur expliziten Entsorgung.
|
||||||
|
|
||||||
|
**Ist-Zustand zum Zeitpunkt der Inkraftsetzung:**
|
||||||
|
|
||||||
|
- Borg via Hetzner Storagebox: produktiv, im Vorfall 2026-05-11 als Restore-Quelle verifiziert
|
||||||
|
- Backrest/restic: **wird abgeschaltet** — Container nicht mehr starten, Repo-Daten archivieren oder löschen, Stack-Eintrag aus Compose entfernen
|
||||||
|
- Appdata-Backup-Plugin (CIFS auf WD MyBookLive): **abgeschaltet, Ziel komplett aus Setup entfernt**
|
||||||
|
- Lokales Borg-Repo auf `/mnt/user/backups/borg/`: einzurichten falls noch nicht vorhanden, als zweites Borg-Ziel neben Hetzner
|
||||||
|
|
||||||
|
**Ziel-Zustand (was dieses Dokument definiert):**
|
||||||
|
|
||||||
|
- Borg ist die einzige produktive Backup-Technologie (lokal + remote)
|
||||||
|
- Backup-Pfade, Pre-Hooks und Verifikation einheitlich über Borg
|
||||||
|
- Migrationspfad Backrest-Abschaltung pro Stack in `docs/RESTORE_MATRIX.md`
|
||||||
|
|
||||||
|
Dieses Dokument definiert die Zielarchitektur. Abweichungen zwischen Ist und Soll sind dokumentiert in `docs/RESTORE_MATRIX.md`.
|
||||||
|
|
||||||
|
### 8.1 Backup-Ziele
|
||||||
|
|
||||||
|
| Ziel | Typ | Zweck | Retention |
|
||||||
|
|------|-----|-------|-----------|
|
||||||
|
| `/mnt/user/backups/borg/` (lokal) | Borg-Repo auf Disk1 | Schneller lokaler Restore | 30 Tage täglich, 12 Monate monatlich |
|
||||||
|
| Hetzner Storagebox | Borg-Repo via SSH | Off-Site, Disaster Recovery | 90 Tage täglich, 24 Monate monatlich |
|
||||||
|
| Externe Wechselplatte | Cold Storage, manuell | Air-Gap, Recovery-Material | rotiert manuell, kein festes Schema |
|
||||||
|
|
||||||
|
**WD MyBookLive ist explizit kein Backup-Ziel mehr.** Eintrag in `docs/SECRETS_MAP.md` und allen Backup-Konfigs entfernt.
|
||||||
|
|
||||||
|
### 8.2 Backup-Inhalt
|
||||||
|
|
||||||
|
**Pflicht-Backup-Scope (Borg, beide Repos):**
|
||||||
|
|
||||||
|
- `/mnt/user/appdata/*/config/`
|
||||||
|
- `/mnt/user/appdata/*/dumps/`
|
||||||
|
- `/mnt/user/appdata/*/README.md`
|
||||||
|
- `/mnt/user/appdata/*/data/` für Stacks mit Nutzdaten in `data/` (siehe §5 und Klassifikation in `RESTORE_MATRIX.md`)
|
||||||
|
- `/mnt/user/services/`
|
||||||
|
- `/boot/config/` (Unraid-Konfiguration)
|
||||||
|
- **persönliche Daten vollständig im Scope** (Operator-Entscheidung 2026-05-15):
|
||||||
|
- `/mnt/user/documents/`
|
||||||
|
- `/mnt/user/photos/`
|
||||||
|
- `/mnt/user/finance/`
|
||||||
|
- `/mnt/user/projekte/`
|
||||||
|
- `/mnt/user/media/` (falls behalten)
|
||||||
|
|
||||||
|
Borg dedupliziert über Läufe, der erste Vollbackup ist groß, Folge-Backups inkrementell. Retention nach §8.1.
|
||||||
|
|
||||||
|
**Pflicht-Excludes (mit Vorsicht — siehe §5):**
|
||||||
|
|
||||||
|
- `**/data/` innerhalb `appdata/*/` **nur dann**, wenn `data/` ausschließlich rohes DB-Datenverzeichnis ist (durch `dumps/` abgedeckt). Stacks mit echten Nutzdaten in `data/` (Immich, Paperless, Gitea, Vaultwarden, Mealie, Grafana etc.) sind aus diesem Exclude **explizit ausgenommen** — pro Stack klassifiziert in `docs/RESTORE_MATRIX.md`. Unklassifizierte Stacks gelten als „nicht ausschließbar" — lieber zuviel sichern als versehentlich zu wenig.
|
||||||
|
- `**/cache/`, `**/tmp/`, `**/lost+found/` (jeweils generisch unbedenklich)
|
||||||
|
- Container-Image-Layer (`/var/lib/docker/`)
|
||||||
|
- VM-Disk-Images (`/mnt/user/domains/`) außer auf Operator-Entscheidung pro VM
|
||||||
|
|
||||||
|
### 8.3 Pre-Backup-Hooks pro DB-Stack
|
||||||
|
|
||||||
|
Verbindlich. Jeder Stack mit eigener DB hat einen Pre-Hook, der einen konsistenten Dump nach `appdata/<stack>/dumps/` schreibt:
|
||||||
|
|
||||||
|
| Stack-Typ | Befehl | Ziel |
|
||||||
|
|-----------|--------|------|
|
||||||
|
| Postgres | `pg_dump -Fc` oder `pg_dumpall --globals-only` | `appdata/<stack>/dumps/<dbname>.dump` |
|
||||||
|
| MariaDB/MySQL | `mariadb-dump --single-transaction` | `appdata/<stack>/dumps/<dbname>.sql` |
|
||||||
|
| MongoDB | `mongodump --archive=...` | `appdata/<stack>/dumps/mongo.archive.gz` |
|
||||||
|
| SQLite (Vaultwarden, Gitea) | `sqlite3 db .backup target.db` | `appdata/<stack>/dumps/<dbname>.sqlite` |
|
||||||
|
| Redis | `redis-cli BGSAVE` plus Kopie der `dump.rdb` | `appdata/<stack>/dumps/dump.rdb` |
|
||||||
|
|
||||||
|
**Dumps sind nur dann wertvoll, wenn sie regelmäßig gegen Restore-Test verifiziert werden.** Siehe Abschnitt 11.
|
||||||
|
|
||||||
|
### 8.4 Backup-Verifikation
|
||||||
|
|
||||||
|
| Frequenz | Prüfung | Pass-Kriterium |
|
||||||
|
|----------|---------|----------------|
|
||||||
|
| Pro Backup-Lauf | Exit-Code, Dauer im erwarteten Korridor, Archivgröße im erwarteten Korridor | alle drei grün, sonst Alarm |
|
||||||
|
| Wöchentlich | `borg check --repository-only` auf beiden Repos | fehlerfrei |
|
||||||
|
| Monatlich | Probe-Restore eines zufällig ausgewählten Stacks aus Hetzner-Repo in Wegwerf-Pfad | restored Files lesbar, DB-Dump per `pg_restore --list` lesbar |
|
||||||
|
| Quartalsweise | Voll-Restore-Drill einer kompletten App-Klasse (z. B. Gitea allein) auf Test-Pfad | App startet, Login funktioniert |
|
||||||
|
|
||||||
|
Backup-Verifikations-Ergebnisse landen in `/mnt/user/services/backup-verify/` als Log mit Zeitstempel.
|
||||||
|
|
||||||
|
## 9. Mover-Policy
|
||||||
|
|
||||||
|
- Mover läuft nicht auf einem Schedule, der parallel zu Backup-Jobs liegt
|
||||||
|
- Mover wird **niemals** für Shares mit `cache: only` aktiv
|
||||||
|
- Mover wird vor planmäßigen Reboots manuell ausgelöst und abgewartet
|
||||||
|
- Mover-Logs werden vom Posture-Check auf Anomalien geprüft
|
||||||
|
|
||||||
|
## 10. Network-Bezug zu Storage
|
||||||
|
|
||||||
|
Reine Verweis-Sektion. Authoritative Definition in `HOMELAB_ARCHITECTURE_MASTER_V2.md`.
|
||||||
|
|
||||||
|
- Datenbanken, die Storage-intensiv sind, hängen ausschließlich an `backend_net` (`internal: true`), niemals an `frontend_net`
|
||||||
|
- Backup-Container hängen entweder an `backend_net` (für DB-Connect) oder eigenem internen Netz, nicht an `frontend_net`
|
||||||
|
- Es gibt keine Storage-Pfade, die über `frontend_net`-Container exponiert werden
|
||||||
|
|
||||||
|
## 11. Posture-Check (Pflicht-Audit)
|
||||||
|
|
||||||
|
Automatisierter Check, der mindestens täglich läuft (z. B. via User-Script oder Cron im posture-check-Stack), und bei jedem Fail einen Alarm produziert.
|
||||||
|
|
||||||
|
**Zusätzlich zwingend ausgelöst:**
|
||||||
|
|
||||||
|
- **Bei jedem Boot** des Hosts (Unraid User-Script `at start`, oder systemd-äquivalent) — fängt sofort ab, wenn nach Reboot ein Mount falsch ist
|
||||||
|
- **Vor jedem Backup-Lauf** als Pre-Hook — wenn Posture-Check fehlschlägt, **wird das Backup abgebrochen und alarmiert**, statt fragwürdige Daten zu sichern und so die Backup-Historie zu kontaminieren
|
||||||
|
- **Nach jedem Mover-Lauf** als Post-Hook — verifiziert, dass Mover keine `cache: only`-Shares angefasst hat
|
||||||
|
- **Vor jeder strukturellen Änderung** (Disk-Tausch, Pool-Umzug, Format-Aktion) manuell — Soll-Ist-Abgleich vor destruktiven Aktionen
|
||||||
|
|
||||||
|
**Alarmziel (Operator-Entscheidung 2026-05-15, normalisiert 2026-05-17): ntfy.** Problem-Alerts gehen an den selbst gehosteten ntfy-Server `https://ntfy.kaleschke.info` mit dem Topic `homelab-alerts`. Optionale Erfolgsmeldungen gehen an `homelab-info`. Push-Empfänger sind die Mobil-Geräte des Operators. Optional als Folge-Erweiterung: E-Mail-Fallback über mail-archiver-Stack als persistente Trail für historische Auswertung — nicht jetzt entscheiden, kann später dazukommen.
|
||||||
|
|
||||||
|
**Pflichtprüfungen:**
|
||||||
|
|
||||||
|
| Check | Erwartung | Bei Fail |
|
||||||
|
|-------|-----------|----------|
|
||||||
|
| `findmnt -no FSTYPE /mnt/cache` | `xfs` | Sofortalarm, kritisch |
|
||||||
|
| `findmnt -no FSTYPE /mnt/disk1` | `xfs` (nach Phase 2) | Sofortalarm, kritisch |
|
||||||
|
| `mount` enthält keinen `ntfs3`- oder `fuseblk`-Eintrag auf Cache/Array | wahr | Sofortalarm |
|
||||||
|
| `nvme smart-log /dev/nvme0n1` Critical Warning | `0x00` | Sofortalarm |
|
||||||
|
| `nvme smart-log` Media Errors | `0` oder stabil zur Vorwoche | Warnung |
|
||||||
|
| D-State-Prozesse > 60s | keine | Warnung, ab 5 Min Alarm |
|
||||||
|
| iowait über 5-Min-Mittel | < 30 % | Warnung ab 50 %, Alarm ab 80 % |
|
||||||
|
| Letztes erfolgreiches Borg-Archiv (lokal) | < 26 h alt | Warnung ab 30 h, Alarm ab 48 h |
|
||||||
|
| Letztes erfolgreiches Borg-Archiv (Hetzner) | < 26 h alt | Warnung ab 30 h, Alarm ab 48 h |
|
||||||
|
| Erwartete Mindestgröße pro Borg-Archiv | matcht Profil pro Stack | Warnung |
|
||||||
|
| CIFS/NFS-Mount-Liveness (für jeden konfigurierten Remote-Mount) | `stat` mit 10s-Timeout erfolgreich | Sofortalarm |
|
||||||
|
| Anzahl laufender Container | matcht Soll aus Repo | Warnung |
|
||||||
|
|
||||||
|
**Implementierung:** Skript unter `services/posture-check/posture-check.sh` plus Kompatibilitaets-Wrapper `services/posture-check/posture_check.sh`, ausgegeben als JSON nach `/mnt/user/services/posture-check/last.json`, konsumiert von ntfy.
|
||||||
|
|
||||||
|
## 12. Hard Rules — Constitution
|
||||||
|
|
||||||
|
Diese Regeln sind nicht optional. Verstoß ist Incident, kein Feature-Request.
|
||||||
|
|
||||||
|
1. **Kein NTFS, exFAT, FAT32 auf Cache, Pool, Array.** Nur XFS, BTRFS, ZFS.
|
||||||
|
2. **Keine Compose-Bind-Mounts auf `/mnt/cache/...` oder `/mnt/disk1/...`.** Immer `/mnt/user/...`.
|
||||||
|
3. **Keine Container schreibt nach `/mnt/disks/...` (Unassigned Devices) im Dauerbetrieb.**
|
||||||
|
4. **Keine `latest`-Image-Tags in Production-Compose.** Versionspin oder bewusster `:stable`/`:lts`-Alias.
|
||||||
|
5. **Keine Secrets im Repo.** Niemals.
|
||||||
|
6. **Kein Backup-Ziel über CIFS mit Hard-Mount ohne Liveness-Check.**
|
||||||
|
7. **Keine produktive Stack-Änderung ohne Compose-Commit im Repo.**
|
||||||
|
8. **Keine Disk- oder Pool-Änderung ohne Update dieses Dokuments im selben Commit.**
|
||||||
|
9. **Kein neuer Stack ohne Eintrag in `STORAGE_LAYOUT.md` (Share/Pfad), `SERVICE_CATALOG.md` (Funktion), `RESTORE_MATRIX.md` (Restore-Pfad).**
|
||||||
|
10. **Kein Pre-Backup-Hook, der DBs nicht konsistent dumped, für Stacks mit eigener DB.**
|
||||||
|
11. **Kein produktiver Stack ohne dokumentierten Restore-Pfad in `docs/RESTORE_MATRIX.md`.** Idealerweise mit dokumentiertem Restore-Test (≤ 90 Tage alt); bei fehlendem Test mindestens schriftliche Restore-Schritte und Backup-Quelle. Stacks ohne diesen Eintrag laufen nicht produktiv — entweder dokumentieren oder abschalten.
|
||||||
|
12. **Kein Backup-Lauf ohne vorgeschalteten Posture-Check (siehe §11).** Backup auf kompromittiertem Filesystem überschreibt unter Umständen den letzten guten Stand und kontaminiert die Backup-Historie.
|
||||||
|
|
||||||
|
**Dokumentierte Host-Observability-Ausnahmen (Operator-Entscheidung 2026-05-16):**
|
||||||
|
`glances`, `scrutiny`, `monitoring-promtail`, `monitoring-node-exporter` und `monitoring-cadvisor` duerfen gezielt Host-/Device-Bind-Mounts ausserhalb `/mnt/user/...` nutzen, weil ihre Kernfunktion sonst nicht erfuellbar ist. Erlaubt sind nur die in `docs/SERVICE_CATALOG.md` pro Dienst genannten Binds. Diese Ausnahmen sind keine Datenpersistenz-Pfade und duerfen nicht fuer Appdaten, Backups oder normale Service-Konfiguration erweitert werden. Neue oder geaenderte Host-Binds brauchen eine explizite Doku-Aenderung im selben Commit.
|
||||||
|
|
||||||
|
## 13. Soft Rules — Konventionen
|
||||||
|
|
||||||
|
Erwartet, aber begründbare Abweichungen sind dokumentiert.
|
||||||
|
|
||||||
|
- Stack-Verzeichnisse werden mit der Konvention aus Abschnitt 5 angelegt
|
||||||
|
- Ein Stack hat einen `README.md` in seinem `appdata/`-Verzeichnis
|
||||||
|
- Image-Tags werden quartalsweise auf Updates geprüft, dokumentiert in einem Update-Log
|
||||||
|
- Container ohne klare Eigentümerschaft werden quartalsweise reviewed (rauswerfen oder in Doku aufnehmen)
|
||||||
|
- Neue Shares werden grundsätzlich mit Cache-Setting `no` angelegt, Promotion auf `prefer`/`only` ist bewusste Entscheidung
|
||||||
|
- Compose-Dateien sind kommentiert, wenn die Konfiguration nicht selbsterklärend ist
|
||||||
|
|
||||||
|
## 14. Anti-Patterns mit Begründung
|
||||||
|
|
||||||
|
| Anti-Pattern | Warum Verboten |
|
||||||
|
|--------------|----------------|
|
||||||
|
| Direkter `/mnt/cache/X`-Bind-Mount in Compose | Pfad verschwindet, wenn Share-Setting auf Array migriert wird; bricht beim ersten Mover-Lauf |
|
||||||
|
| `chown -R 1000:1000 /mnt/user/appdata` blanket | Bricht alle Stacks, die andere UIDs erwarten; bei Mover-Lauf werden Permissions ggf. überschrieben |
|
||||||
|
| Backup direkt von Live-DB-Datendir | Inkonsistent unter Last; WAL-Divergenz nach Restore möglich |
|
||||||
|
| `latest`-Tags in Production | Reproduzierbarkeit weg; Rollback nur per Glück möglich |
|
||||||
|
| Mehrere Stacks im selben Appdata-Verzeichnis | Backup-Excludes greifen falsch; Restore betrifft Nachbar-Stack |
|
||||||
|
| Cache-Filesystem ohne automatisierten Posture-Check | Genau der Vorfall vom 2026-05-11 |
|
||||||
|
| Hard-mounted CIFS auf wackelige Geräte | D-State-Prozesse können den ganzen Host blockieren |
|
||||||
|
| Secret in Compose-Env (statt `env_file`) | Secret im Repo lesbar; bei `git log -p` für immer drin |
|
||||||
|
| Stack ohne dokumentierten Restore-Pfad | Bei Recovery muss improvisiert werden; das war der Auslöser dieses Dokuments |
|
||||||
|
|
||||||
|
## 15. Migrations- und Wachstumspfade
|
||||||
|
|
||||||
|
### 15.1 NTFS-zu-XFS-Migration (Cache, Phase 1)
|
||||||
|
|
||||||
|
Behandelt in `docs/DISASTER_RECOVERY.md`, Sektion „Cache Recovery 2026-05".
|
||||||
|
|
||||||
|
### 15.2 NTFS-zu-XFS-Migration (Disk1, Phase 2)
|
||||||
|
|
||||||
|
Folgeprojekt nach mindestens 7 Tagen stabilem Cache-Betrieb. Eigener Plan in `docs/DISASTER_RECOVERY.md`. Grobschritte:
|
||||||
|
|
||||||
|
1. Selektive Sicherung aller Disk1-Inhalte auf zweite Disk oder Hetzner
|
||||||
|
2. Hash-Verifikation der Sicherung
|
||||||
|
3. Disk1 entfernen aus Array (oder leer, wenn Daten temporär auf zweiter Disk)
|
||||||
|
4. Disk1 als XFS in Unraid-GUI neu formatieren
|
||||||
|
5. Daten zurückspielen
|
||||||
|
6. Parity neu bauen
|
||||||
|
7. Posture-Check grün
|
||||||
|
|
||||||
|
### 15.3 Erweiterung um zweite Cache-NVMe
|
||||||
|
|
||||||
|
Trigger: Cache-Auslastung > 70 %, oder Wunsch nach RAID1 für Appdata.
|
||||||
|
|
||||||
|
Pfad: BTRFS-Pool aus zwei NVMe (RAID1) als Ersatz für Single-XFS-Cache. Migration über Pool-Wechsel mit zwischengeschalteter Image-Sicherung. Eigenes Migrations-Dokument vor Beginn.
|
||||||
|
|
||||||
|
### 15.4 Erweiterung um zweite Array-Disk
|
||||||
|
|
||||||
|
Trigger: Disk1 > 80 % voll, oder Performance-Bedarf.
|
||||||
|
|
||||||
|
Pfad: zweite Daten-Disk dem Array hinzufügen, Allocation-Method überprüfen, Split-Level pro Share validieren. Parity-Disk muss ≥ größte Daten-Disk bleiben.
|
||||||
|
|
||||||
|
### 15.5 Multi-Host (Hermes-Skalierung)
|
||||||
|
|
||||||
|
Wenn Hermes-Worker auf weiteren Hosts skaliert: dieser Storage-Layout-Plan gilt zunächst nur für `Kallilabcore`. Weitere Hosts brauchen eigenes Storage-Layout-Dokument. Gemeinsamer Storage (NFS, iSCSI, S3-API) ist ein eigener Architektur-Schritt mit eigenem Dokument.
|
||||||
|
|
||||||
|
## 16. Glossar
|
||||||
|
|
||||||
|
| Begriff | Bedeutung |
|
||||||
|
|---------|-----------|
|
||||||
|
| **Cache (Pool)** | Schneller Pool (NVMe), nicht Teil des Arrays, ohne Parity-Schutz, für Performance-kritische Daten |
|
||||||
|
| **Array** | Klassische Unraid-Daten-Disks plus Parity, paritätsgeschützt, langsamer |
|
||||||
|
| **shfs** | Unraid-User-Share-FUSE-Filesystem; vereinigt Cache und Array unter `/mnt/user/` |
|
||||||
|
| **Mover** | Unraid-Service, der Daten zwischen Cache und Array gemäß Cache-Setting verschiebt |
|
||||||
|
| **Cache-Setting** | Pro Share: `only`, `prefer`, `yes`, `no` — steuert Mover-Verhalten |
|
||||||
|
| **Allocation Method** | High Water / Most Free / Fill Up — wie Daten auf Array-Disks verteilt werden |
|
||||||
|
| **Split Level** | Maximale Verzeichnistiefe, in der Unraid Inhalte über Disks aufteilen darf |
|
||||||
|
| **Pre-Backup-Hook** | Skript, das vor jedem Backup einen konsistenten DB-Dump erzeugt |
|
||||||
|
| **Posture-Check** | Automatisierter Audit, der Host-Realität gegen Soll-Zustand prüft |
|
||||||
|
| **Constitution** | Hard Rules in Abschnitt 12, nicht verhandelbar |
|
||||||
|
|
||||||
|
## 17. Referenzen
|
||||||
|
|
||||||
|
- `HOMELAB_ARCHITECTURE_MASTER_V2.md` — Gesamtarchitektur, Netze, Traefik, GitOps
|
||||||
|
- `docs/REPO_MAP.md` — Repo-Struktur, wo welcher Stack lebt
|
||||||
|
- `docs/SERVICE_CATALOG.md` — Pro Stack: Funktion, Abhängigkeiten, Eigenheiten
|
||||||
|
- `docs/RESTORE_MATRIX.md` — Pro Stack: Restore-Quelle und -Verfahren
|
||||||
|
- `docs/SECRETS_MAP.md` — Pro Secret: Speicherort, Rotation, Recovery
|
||||||
|
- `docs/DISASTER_RECOVERY.md` — Konkrete Recovery-Pläne, inkl. NTFS-Migration
|
||||||
|
- `docs/GITOPS_DRIFT_RUNBOOK.md` — Drift-Erkennung und -Behebung
|
||||||
|
- `docs/AI_CONTEXT.md` — Kontext für AI-Assistenten
|
||||||
|
|
||||||
|
## 18. Changelog
|
||||||
|
|
||||||
|
| Version | Datum | Änderung | Autor |
|
||||||
|
|---------|-------|----------|-------|
|
||||||
|
| 1.0 (Draft) | 2026-05-15 | Initialfassung nach Incident 2026-05-11. Ursprung: NTFS-Cache-Korruption, fehlende Posture-Checks, ungeplante Backup-Strategie. | Operator + AI-Assistenten |
|
||||||
|
| 1.1 (Draft) | 2026-05-15 | Operator-Review-Feedback eingearbeitet: `system`/`domains` auf `only`, `services` als recovery-kritisch markiert, `data/`-Behandlung pro Stack klassifiziert (statt blanket Exclude), Backup-Tooling Ist/Soll explizit getrennt, Posture-Check zusätzlich bei Boot/vor Backup/nach Mover, Hard Rules 11+12 ergänzt (Restore-Pfad-Pflicht, Posture-vor-Backup), Alarmziel-Optionen benannt, Review-Items in eigene Sektion §20 verschoben. | Operator + AI-Assistenten |
|
||||||
|
| 1.2 (Draft) | 2026-05-15 | Operator-Entscheidungen #3, #4, #6, #9, #11 eingearbeitet: Backrest abgeschaltet (Borg alleinige Backup-Technologie), persönliche Daten vollständig im Pflicht-Backup-Scope, ntfy als Alarmziel verbindlich, kebab-case-Migration im Rahmen der Recovery-Phase, Mirror-Backup für Gitea-Repo-Inhalte als verbindliche Spec (Implementierung in `SERVICES_RECOVERY.md` zu detaillieren). Offen: Items #1, #2, #5, #7, #8, #10. | Operator + AI-Assistenten |
|
||||||
|
| 1.3 (Draft) | 2026-05-15 | Operator-Entscheidungen #1, #7, #8 eingearbeitet: Disk-Größen/-Modelle als Deferred via Posture-Check-Detection (kein Blocker), optionale Stacks (Filebrowser, code-server, Speedtest, Scrutiny, Uptime-Kuma) bleiben im Layout und sind produktiv, Network-Verweis auf MASTER_V2 bestätigt. Damit alle akuten Items entschieden. Verbleibend: Items #2, #5, #10 (Retention, Schwellen-Kalibrierung, RESTORE_MATRIX-Klassifikation) — alle als Folge-Aufgaben über Inkraftsetzung hinaus, kein Commit-Blocker. | Operator + AI-Assistenten |
|
||||||
|
|
||||||
|
## 19. Inkraftsetzung
|
||||||
|
|
||||||
|
Dieses Dokument tritt in Kraft mit dem Commit der finalen Fassung in `master`-Branch des Repos `homelab-infra`. Ab Inkraftsetzung gelten alle Hard Rules; Soft Rules werden im Rahmen der laufenden Recovery-Arbeit etabliert; Posture-Check muss innerhalb von 14 Tagen nach Inkraftsetzung produktiv laufen.
|
||||||
|
|
||||||
|
Erste Audit-Review dieses Dokuments: spätestens 90 Tage nach Inkraftsetzung. Danach jährlich oder bei jeder strukturellen Änderung des Storage-Layouts.
|
||||||
|
|
||||||
|
## 20. Open Review Items (vor finalem Commit zu entscheiden)
|
||||||
|
|
||||||
|
Diese Sektion dokumentiert offene Operator-Entscheidungen und Lücken. **Vor Statuswechsel von Draft auf Active ist jeder Punkt entweder eingearbeitet oder bewusst als „bleibt offen" mit Verweis auf Folge-Issue/-Doc markiert.**
|
||||||
|
|
||||||
|
| Nr. | Item | Status | Verantwortung |
|
||||||
|
|-----|------|--------|---------------|
|
||||||
|
| 1 | Disk-Größen und Modelle in §3 (Disk1, Parity, externe Backup-Platte) | **DEFERRED 2026-05-15** — wird automatisch via Posture-Check-Detection-Lauf ergänzt; Disk1-Filesystem ist seit 2026-05-25 XFS | Operator + Posture-Check |
|
||||||
|
| 2 | Retention-Werte in §8.1 (Borg-Repos lokal/remote) — abhängig von tatsächlicher Storage-Kapazität | Vorschlag steht, anzupassen | Operator |
|
||||||
|
| 3 | Backup-Tooling: Backrest abschalten, Borg alleinige Backup-Technologie | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §8.0) |
|
||||||
|
| 4 | Backup-Scope für persönliche Daten: `documents`, `photos`, `finance`, `projekte` (und `media` falls behalten) **vollständig** im Pflicht-Scope | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §8.2) |
|
||||||
|
| 5 | Posture-Check-Schwellen in §11 — Vorschlag konservativ, nach erstem Monitoring kalibrieren | Vorschlag steht | Operator nach 30 Tagen |
|
||||||
|
| 6 | Alarmziel: ntfy als primärer Push-Kanal, Mail-Fallback als Folge-Erweiterung optional | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §11) |
|
||||||
|
| 7 | Optionale Stacks (Filebrowser, code-server, Speedtest, Scrutiny, Uptime-Kuma) — bleiben im Layout, sind produktiv im Scope, folgen den Standard-Konventionen aus §5 und Backup-Pflichten aus §8.2 | **ENTSCHIEDEN 2026-05-15** | erledigt |
|
||||||
|
| 8 | Verweis auf `HOMELAB_ARCHITECTURE_MASTER_V2.md` für Network-Architektur (§10) — Net-Architektur steht dort authoritativ, Verweis ist ausreichend | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §10) |
|
||||||
|
| 9 | Naming-Konvention: kebab-case durchziehen, Migration im Rahmen der Recovery-Phase | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §6); pro Stack in RESTORE_MATRIX.md zu dokumentieren |
|
||||||
|
| 10 | Pro-Stack-Klassifizierung in `RESTORE_MATRIX.md` (DB-Typ, Nutzdaten in `data/`, Dump-Verfahren, letzter Restore-Test, kebab-case-Migrationsname) — als Folge-Aufgabe aus Hard Rule §12.11 | Folge-Aufgabe | Operator + Recovery-Phase |
|
||||||
|
| 11 | Mirror-Backup für `services/gitea/git/repositories/` auf zweites Medium, ≤ 6 h Frequenz, konkrete Implementierung in `docs/SERVICES_RECOVERY.md` zu erstellen | **ENTSCHIEDEN 2026-05-15**, Implementierungs-Doc offen | Operator + Folge-Doc |
|
||||||
|
|
||||||
|
Wenn alle 11 Punkte bearbeitet sind und der Operator die Datei reviewed hat, wird sie als `docs/STORAGE_LAYOUT.md` (ohne `.draft`) committed und Status auf `Active` gesetzt.
|
||||||
+23
-18
@@ -76,6 +76,9 @@ Vor lokaler Arbeit:
|
|||||||
Nach lokaler Arbeit:
|
Nach lokaler Arbeit:
|
||||||
|
|
||||||
1. Aenderungen pruefen
|
1. Aenderungen pruefen
|
||||||
|
2. bei Compose-/Backup-/Restore-Aenderungen relevante manuelle Repo-Checks ausfuehren
|
||||||
|
- `ops/policy-checks/check_repo.ps1`
|
||||||
|
- `ops/restore-tests/check-restore-freshness.ps1` oder gezielte Restore-Checks
|
||||||
2. Commit mit sauberer Nachricht
|
2. Commit mit sauberer Nachricht
|
||||||
3. `Push origin`
|
3. `Push origin`
|
||||||
4. Komodo-Webhook im Hinterkopf behalten
|
4. Komodo-Webhook im Hinterkopf behalten
|
||||||
@@ -112,6 +115,23 @@ Komodo ist in diesem Setup:
|
|||||||
- Pushes koennen automatisch einen Komodo-Deploy ausloesen
|
- Pushes koennen automatisch einen Komodo-Deploy ausloesen
|
||||||
- wenn Komodo und Git voneinander abweichen, gewinnt Git
|
- wenn Komodo und Git voneinander abweichen, gewinnt Git
|
||||||
|
|
||||||
|
### Pflicht bei neuen Komodo-Stacks
|
||||||
|
|
||||||
|
Jeder neue produktive Komodo-Stack, der aus `Micha/homelab-infra` deployed wird, braucht einen aktiven Gitea-Webhook auf die aktuelle Komodo-Stack-ID.
|
||||||
|
|
||||||
|
Pflichtschritte beim Anlegen:
|
||||||
|
|
||||||
|
1. Stack in Komodo aus Gitea anlegen
|
||||||
|
2. `webhook_enabled` in Komodo aktivieren
|
||||||
|
3. passenden Gitea-Webhook fuer die aktuelle Stack-ID anlegen
|
||||||
|
4. Gitea-Hook gegen `http://komodo-core:9120/listener/github/stack/<stack-id>/deploy` pruefen
|
||||||
|
5. einen Push oder Test-Delivery ausloesen und `last_status`/Komodo-Deploy pruefen
|
||||||
|
6. Ausnahmen explizit dokumentieren
|
||||||
|
|
||||||
|
**Regel:** Kein neuer produktiver GitOps-Stack ohne funktionierenden Gitea->Komodo-Webhook. Bewusste Ausnahmen muessen im selben Aenderungsblock dokumentiert werden, inklusive Grund und Alternativ-Deploy-Weg.
|
||||||
|
|
||||||
|
Der Standardfall nutzt den globalen `KOMODO_WEBHOOK_SECRET` aus der Komodo-Host-`.env`, ausser Komodo zeigt fuer den Stack explizit ein eigenes per-Stack-Secret.
|
||||||
|
|
||||||
### Ausnahme: Komodo-Zugangsmodell
|
### Ausnahme: Komodo-Zugangsmodell
|
||||||
|
|
||||||
Komodo bleibt **bewusst** ohne zentrale Traefik-ForwardAuth-Middleware.
|
Komodo bleibt **bewusst** ohne zentrale Traefik-ForwardAuth-Middleware.
|
||||||
@@ -298,29 +318,14 @@ Nach jeder erfolgreichen Migration oder relevanten Aenderung muessen diese Datei
|
|||||||
- `docs/MIGRATION_LOG.md`
|
- `docs/MIGRATION_LOG.md`
|
||||||
- `docs/SECRETS_MAP.md`
|
- `docs/SECRETS_MAP.md`
|
||||||
- `docs/ROLLBACK.md`
|
- `docs/ROLLBACK.md`
|
||||||
|
- `docs/SERVICES_RECOVERY.md` falls `/mnt/user/services`, Gitea, Komodo oder Host-Automation betroffen sind
|
||||||
|
- `docs/HARDWARE_INVENTORY.md` und `docs/CAPACITY_AND_LIFECYCLE.md` falls Hardware, Disks, Cache, RAM oder USV betroffen sind
|
||||||
|
- `docs/NETWORK_INVENTORY.md` und `docs/EXTERNAL_DEPENDENCIES.md` falls Router, DNS, Tailscale, Portfreigaben oder Provider betroffen sind
|
||||||
- `HOMELAB_ARCHITECTURE_MASTER_V2.md` falls Architektur betroffen ist
|
- `HOMELAB_ARCHITECTURE_MASTER_V2.md` falls Architektur betroffen ist
|
||||||
- `docs/GITOPS_DRIFT_RUNBOOK.md` falls GitOps-/Komodo-/Runtime-Drift betroffen ist
|
- `docs/GITOPS_DRIFT_RUNBOOK.md` falls GitOps-/Komodo-/Runtime-Drift betroffen ist
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sprint-Regel
|
|
||||||
|
|
||||||
Jede Migration wird als Sprint behandelt. Ein Sprint umfasst immer:
|
|
||||||
|
|
||||||
1. Ist-Zustand pruefen
|
|
||||||
2. Zielzustand definieren
|
|
||||||
3. Compose-Datei im Repo anpassen
|
|
||||||
4. lokal synchronisieren und aendern
|
|
||||||
5. Commit + Push
|
|
||||||
6. Deploy ueber Komodo
|
|
||||||
7. Test
|
|
||||||
8. Doku aktualisieren
|
|
||||||
9. Sprint abschliessen
|
|
||||||
|
|
||||||
> Nie mehrere kritische Dienste gleichzeitig aendern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rollback-Regel
|
## Rollback-Regel
|
||||||
|
|
||||||
Jede Aenderung muss rueckrollbar sein. Vor jedem Deploy muss klar sein:
|
Jede Aenderung muss rueckrollbar sein. Vor jedem Deploy muss klar sein:
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Post-Install: Als Erstes Codex wieder startklar machen
|
||||||
|
|
||||||
|
Ziel: Nach der frischen Windows-Installation zuerst wieder mit Codex/ChatGPT weiterarbeiten koennen, bevor der restliche Wiederaufbau startet.
|
||||||
|
|
||||||
|
## 1. Internet herstellen
|
||||||
|
|
||||||
|
1. Windows starten.
|
||||||
|
2. LAN/WLAN verbinden.
|
||||||
|
3. Falls kein Internet vorhanden ist:
|
||||||
|
- externe Backup-HDD anschliessen
|
||||||
|
- Treiber aus `H:\Windows-Neuaufsetzen-Backup\13_Treiber_Windows` installieren
|
||||||
|
- besonders LAN/WLAN/Chipsatz pruefen
|
||||||
|
|
||||||
|
## 2. Browser starten
|
||||||
|
|
||||||
|
1. Microsoft Edge oeffnen.
|
||||||
|
2. Bei Microsoft/ChatGPT/Codex anmelden, je nachdem welche Variante genutzt wird.
|
||||||
|
3. Diese Datei auf H: oeffnen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
H:\Windows-Neuaufsetzen-Backup\POSTINSTALL_ERSTES_ZIEL_CODEX.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Basiswerkzeuge installieren
|
||||||
|
|
||||||
|
PowerShell als normaler Benutzer oeffnen.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
winget install --exact --id Git.Git --source winget --accept-package-agreements --accept-source-agreements
|
||||||
|
winget install --exact --id Microsoft.VisualStudioCode --source winget --accept-package-agreements --accept-source-agreements
|
||||||
|
winget install --exact --id OpenJS.NodeJS.LTS --source winget --accept-package-agreements --accept-source-agreements
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional danach UniGetUI:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
winget install --exact --id Devolutions.UniGetUI --source winget --accept-package-agreements --accept-source-agreements
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. SSH und Git-Konfiguration zurueckholen
|
||||||
|
|
||||||
|
Backup-HDD muss angeschlossen sein.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.ssh" | Out-Null
|
||||||
|
Copy-Item -Path "H:\Windows-Neuaufsetzen-Backup\09_Programme_Settings_Lizenzen\ssh\*" -Destination "$env:USERPROFILE\.ssh" -Force
|
||||||
|
Copy-Item -Path "H:\Windows-Neuaufsetzen-Backup\09_Programme_Settings_Lizenzen\git\.gitconfig" -Destination "$env:USERPROFILE\.gitconfig" -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
SSH-Key-Rechte pruefen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh -T git@github.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls Gitea genutzt wird, stattdessen oder zusaetzlich den Gitea-Host testen.
|
||||||
|
|
||||||
|
## 5. Homelab-Repo wieder verfuegbar machen
|
||||||
|
|
||||||
|
Wenn `G:\Gitea_Clone\homelab-infra` noch existiert:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd /d G:\Gitea_Clone\homelab-infra
|
||||||
|
git status
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls das Repo neu geklont werden muss:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
New-Item -ItemType Directory -Force -Path G:\Gitea_Clone | Out-Null
|
||||||
|
cd /d G:\Gitea_Clone
|
||||||
|
git clone <DEIN_GITEA_ODER_GITHUB_REPO_URL> homelab-infra
|
||||||
|
cd homelab-infra
|
||||||
|
git status
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Codex-Kontext wieder aufnehmen
|
||||||
|
|
||||||
|
Wichtige Dateien:
|
||||||
|
|
||||||
|
```text
|
||||||
|
H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\installierte_programme_lesbar.md
|
||||||
|
H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\kritische_programme_lizenz_check.md
|
||||||
|
H:\Windows-Neuaufsetzen-Backup\09_Programme_Settings_Lizenzen\keys_exporte\banking4_license_private.txt
|
||||||
|
G:\Gitea_Clone\homelab-infra\docs\windows-neuaufsetzen-masterplan.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann Codex/ChatGPT sagen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Windows ist frisch installiert. Bitte hilf mir mit dem Post-Install-Wiederaufbau anhand von H:\Windows-Neuaufsetzen-Backup und dem Masterplan.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Danach erst Programme wiederherstellen
|
||||||
|
|
||||||
|
Reihenfolge:
|
||||||
|
|
||||||
|
1. Banking4 installieren und Lizenz aus `banking4_license_private.txt` nutzen.
|
||||||
|
2. Banking4-Datentresor aus `H:\Windows-Neuaufsetzen-Backup\07_Banking_Finanzen\Banking4_Datentresor_explizit` oeffnen.
|
||||||
|
3. Microsoft 365 ueber Microsoft-Konto installieren.
|
||||||
|
4. WISO Steuer installieren und Steuerdateien aus `WISO_Steuer_Dokumente` pruefen.
|
||||||
|
5. Restliche Programme mit UniGetUI/WinGet/Installern wieder aufbauen.
|
||||||
|
|
||||||
@@ -0,0 +1,550 @@
|
|||||||
|
# Windows neu aufsetzen: Masterplan ohne Datenverlust
|
||||||
|
|
||||||
|
Stand: 2026-05-07
|
||||||
|
|
||||||
|
Ziel: Windows sauber neu installieren, die Datenträgerstruktur bereinigen und wichtige Daten sicher erhalten.
|
||||||
|
|
||||||
|
Grundregel: Vor dem Löschen, Formatieren oder Neuinstallieren müssen mindestens zwei geprüfte Kopien der wichtigen Daten existieren.
|
||||||
|
|
||||||
|
## Aktueller Arbeitsstand
|
||||||
|
|
||||||
|
Stand: 2026-05-07, 15:00 Uhr
|
||||||
|
|
||||||
|
Erledigt:
|
||||||
|
|
||||||
|
- Backup-Ziel `H:\Windows-Neuaufsetzen-Backup` auf externer 8-TB-HDD erstellt.
|
||||||
|
- Inventarlisten exportiert nach `H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen`.
|
||||||
|
- Installierte Programme inventarisiert: 161 Eintraege.
|
||||||
|
- Aktuelle Benutzerordner von `C:\Users\michi` gesichert: Desktop, Documents, Pictures, Videos, Downloads, Music.
|
||||||
|
- `C:\Users\michi\.ssh` und `C:\Users\michi\.gitconfig` gesichert.
|
||||||
|
- Alte Standardordner aus `D:\Users\Baerchen` gesichert, soweit vorhanden.
|
||||||
|
- Persoenliche/auffaellige Ordner von `F:` gesichert: `BMW Leasing`, `Marina Handy 2025`, `Marina Handy Backup`.
|
||||||
|
- Relevante Ordner von `G:` gesichert: `Gitea_Clone`, `open-webui`, `Treiber`.
|
||||||
|
- WSL-Distributionen exportiert: `Ubuntu.tar`, `docker-desktop.tar`.
|
||||||
|
- Browserprofile gesichert: Chrome und Edge.
|
||||||
|
- Kritische Programmdaten zusaetzlich gesichert:
|
||||||
|
- Banking4/Subsembly: `C:\Users\michi\AppData\Local\Subsembly`
|
||||||
|
- WISO/Buhl: `C:\Users\michi\AppData\Local\Buhl`, `C:\Users\michi\AppData\Local\Buhl Data Service GmbH`, `C:\ProgramData\Buhl Data Service GmbH`
|
||||||
|
- WISO-Steuerdateien: `C:\Users\michi\Documents\steuer`
|
||||||
|
- Banking-Exporte vom Desktop: `C:\Users\michi\Desktop\Banking`
|
||||||
|
- Registry-Exports fuer Subsembly und Microsoft Office erstellt; Buhl-Registry-Suchlisten erstellt.
|
||||||
|
- Banking4-Lizenzdaten separat gesichert: `H:\Windows-Neuaufsetzen-Backup\09_Programme_Settings_Lizenzen\keys_exporte\banking4_license_private.txt`.
|
||||||
|
- Aktueller Banking4-Datentresor separat gesichert: `H:\Windows-Neuaufsetzen-Backup\07_Banking_Finanzen\Banking4_Datentresor_explizit\Mein Datentresor.sub`.
|
||||||
|
- Office-Aktivierungsstatus exportiert: lokal als Microsoft 365/Office16 O365 Home Premium Grace/Notifications sichtbar, daher Microsoft-Konto/Abonnement manuell pruefen.
|
||||||
|
- Lesbare Programmlisten erstellt:
|
||||||
|
- `installierte_programme_lesbar.md`
|
||||||
|
- `kritische_programme_lizenz_check.md`
|
||||||
|
- UniGetUI/Keyfinder-Empfehlungen dokumentiert: `keyfinder_tools_recommendation.md`.
|
||||||
|
- Robocopy-Summenzeilen geprueft: keine Kopierfehler in den bekannten Backup-Jobs.
|
||||||
|
- Verifikationslisten erstellt:
|
||||||
|
- `backup_verification_known_data.csv`
|
||||||
|
- `backup_verification_browser_profiles.csv`
|
||||||
|
|
||||||
|
Noch offen:
|
||||||
|
|
||||||
|
- Manuelle Screenshots in `H:\Windows-Neuaufsetzen-Backup\14_Screenshots` ablegen.
|
||||||
|
- BitLocker-Status mit Adminrechten pruefen.
|
||||||
|
- Passwortmanager, 2FA-Recovery-Codes und Browser-Sync manuell pruefen.
|
||||||
|
- Banking4-Speicherort explizit pruefen.
|
||||||
|
- Banking4 im Programm selbst oeffnen und aktuellen Datentresor/Backup-Export bestaetigen. Der Key und der Datentresor sind bereits lokal auf H: gesichert.
|
||||||
|
- WISO Steuer 2026 oeffnen und Lizenz/Buhl-Konto sowie Speicherorte der Steuerdateien bestaetigen.
|
||||||
|
- Microsoft-Konto fuer M365 pruefen: Office-Webkonto/Abonnement, Installationsrecht, OneDrive-Sync.
|
||||||
|
- Optional Keyfinder-Lauf durchfuehren und Ergebnisse lokal auf H: speichern.
|
||||||
|
- `G:\Ollama` bewusst entscheiden: nicht gesichert, ca. 40,9 GB lokale Modell-/Cache-Daten.
|
||||||
|
- D:, F: und G: vor dem spaeteren Loeschen noch einmal in Ruhe final bestaetigen.
|
||||||
|
|
||||||
|
## Zielentscheidung: Neues Windows auf Datentraeger 0
|
||||||
|
|
||||||
|
Entscheidung vom 2026-05-07: Das neue Windows soll auf `Datentraeger 0` installiert werden.
|
||||||
|
|
||||||
|
Aktueller Zustand laut Datentraegerverwaltung:
|
||||||
|
|
||||||
|
| Datentraeger | Aktuelles Laufwerk | Groesse | Inhalt/Zweck |
|
||||||
|
|---|---:|---:|---|
|
||||||
|
| Datentraeger 0 | D: | ca. 167 GB | Alte Windows SSD |
|
||||||
|
| Datentraeger 1 | E: | ca. 167 GB | Blizzard Games |
|
||||||
|
| Datentraeger 2 | C: und F: | ca. 931 GB | aktuelles Windows + 980SSD-Partition |
|
||||||
|
| Datentraeger 3 | G: | ca. 931 GB | M2 SSD / Daten |
|
||||||
|
| Datentraeger 4 | H: | ca. 7,45 TB | externe Backup-HDD |
|
||||||
|
|
||||||
|
Bewertung:
|
||||||
|
|
||||||
|
- Machbar, wenn `Datentraeger 0` als reines Windows-/Programme-Laufwerk genutzt wird.
|
||||||
|
- Nicht ideal fuer sehr viele Programme/Games, weil nur ca. 167 GB vorhanden sind.
|
||||||
|
- Vorteil: Die aktuelle Windows-SSD auf `Datentraeger 2` bleibt waehrend der Migration zunaechst erhalten.
|
||||||
|
- Wichtig: Bei der Installation duerfen ausschliesslich Partitionen auf `Datentraeger 0` geloescht werden.
|
||||||
|
|
||||||
|
Empfohlenes Installationsverhalten:
|
||||||
|
|
||||||
|
1. Externe Backup-HDD `H:` vor der Windows-Installation abziehen.
|
||||||
|
2. Wenn praktisch moeglich: andere interne Datentraeger fuer die Installation abziehen oder im UEFI deaktivieren.
|
||||||
|
3. Im Windows-Setup `Benutzerdefiniert` waehlen.
|
||||||
|
4. `Datentraeger 0` anhand der Groesse ca. 167 GB identifizieren.
|
||||||
|
5. Nur auf `Datentraeger 0` alle Partitionen loeschen:
|
||||||
|
- 499 MB Wiederherstellung
|
||||||
|
- 100 MB nicht zugeordnet bleibt egal
|
||||||
|
- D: Alte Windows SSD
|
||||||
|
- 640 MB Wiederherstellung
|
||||||
|
6. Den dadurch komplett nicht zugeordneten Speicher auf `Datentraeger 0` auswaehlen.
|
||||||
|
7. Windows installieren lassen.
|
||||||
|
|
||||||
|
Nicht loeschen:
|
||||||
|
|
||||||
|
- `Datentraeger 1` / E: Blizzard Games
|
||||||
|
- `Datentraeger 2` / C: und F:
|
||||||
|
- `Datentraeger 3` / G:
|
||||||
|
- `Datentraeger 4` / H:
|
||||||
|
|
||||||
|
Nach der Installation:
|
||||||
|
|
||||||
|
- Bootreihenfolge im UEFI auf die neue Windows-Installation auf `Datentraeger 0` setzen.
|
||||||
|
- Altes Windows auf `Datentraeger 2` erst loeschen, wenn das neue System mehrere Tage stabil laeuft.
|
||||||
|
|
||||||
|
## UniGetUI fuer den Wiederaufbau
|
||||||
|
|
||||||
|
UniGetUI ist fuer den Wiederaufbau sinnvoll, aber nicht fuer Lizenz-Keys.
|
||||||
|
|
||||||
|
Nutzen:
|
||||||
|
|
||||||
|
- Programme ueber WinGet/Scoop/Chocolatey/Pip/NPM suchen und installieren.
|
||||||
|
- Updates zentral verwalten.
|
||||||
|
- Paketlisten importieren/exportieren.
|
||||||
|
|
||||||
|
Grenzen:
|
||||||
|
|
||||||
|
- Banking4, WISO Steuer und Microsoft 365 wurden im `winget export` nicht als sauber wiederinstallierbare Pakete abgedeckt.
|
||||||
|
- Lizenzkeys werden durch UniGetUI nicht gesichert.
|
||||||
|
|
||||||
|
Vorhanden:
|
||||||
|
|
||||||
|
- WinGet-Export: `H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\winget-export.json`
|
||||||
|
|
||||||
|
Nach Neuinstallation:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
winget install --exact --id Devolutions.UniGetUI --source winget
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach kann die WinGet-Liste optional importiert werden:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
winget import --import-file "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\winget-export.json" --accept-package-agreements --accept-source-agreements
|
||||||
|
```
|
||||||
|
|
||||||
|
Empfehlung: Nicht alles blind importieren. Erst Basisprogramme installieren, dann die exportierte Liste als Orientierung nutzen.
|
||||||
|
|
||||||
|
## Laufwerksannahmen
|
||||||
|
|
||||||
|
Diese Zuordnung muss vor dem Start geprüft werden.
|
||||||
|
|
||||||
|
| Laufwerk | Vermutung | Behandlung |
|
||||||
|
|---|---|---|
|
||||||
|
| C: | aktuelles Windows | sichern, danach neu aufsetzen |
|
||||||
|
| D: | alte Windows-SSD oder Altbestand | erst analysieren, nicht blind löschen |
|
||||||
|
| E: | Blizzard / Games | wahrscheinlich neu ladbar, Saves prüfen |
|
||||||
|
| F: | 980SSD, fast leer | liegt auf derselben physischen Samsung 980 PRO wie C: |
|
||||||
|
| G: | M.2 SSD, stark belegt | erst analysieren, wichtige Daten sichern |
|
||||||
|
| H: | externe 8-TB-HDD | Backup-Ziel |
|
||||||
|
|
||||||
|
Wichtige Erkenntnis aus dem Inventar vom 2026-05-07: `C:` und `F:` sind Partitionen auf derselben Samsung SSD 980 PRO 1TB. Wenn diese SSD als Ziel fuer die Neuinstallation genutzt wird, muss besonders sauber entschieden werden, welche Partitionen geloescht werden. `F:` ist kein eigener physischer Datentraeger.
|
||||||
|
|
||||||
|
## Phase 1: Backup-Struktur auf H: anlegen
|
||||||
|
|
||||||
|
Zielordner:
|
||||||
|
|
||||||
|
```text
|
||||||
|
H:\Windows-Neuaufsetzen-Backup\
|
||||||
|
|-- 01_Desktop
|
||||||
|
|-- 02_Dokumente
|
||||||
|
|-- 03_Bilder
|
||||||
|
|-- 04_Videos
|
||||||
|
|-- 05_Downloads_wichtig
|
||||||
|
|-- 06_Projekte
|
||||||
|
|-- 07_Banking_Finanzen
|
||||||
|
|-- 08_Browser_Lesezeichen_Profile
|
||||||
|
|-- 09_Programme_Settings_Lizenzen
|
||||||
|
|-- 10_Games_Savegames
|
||||||
|
|-- 11_Homelab_NAS_Doku
|
||||||
|
|-- 12_Exportierte_Listen
|
||||||
|
|-- 13_Treiber_Windows
|
||||||
|
|-- 14_Screenshots
|
||||||
|
|-- 15_Musik
|
||||||
|
`-- 99_Unsortiert_von_D_F_G
|
||||||
|
```
|
||||||
|
|
||||||
|
PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$BackupRoot = "H:\Windows-Neuaufsetzen-Backup"
|
||||||
|
$Folders = @(
|
||||||
|
"01_Desktop",
|
||||||
|
"02_Dokumente",
|
||||||
|
"03_Bilder",
|
||||||
|
"04_Videos",
|
||||||
|
"05_Downloads_wichtig",
|
||||||
|
"06_Projekte",
|
||||||
|
"07_Banking_Finanzen",
|
||||||
|
"08_Browser_Lesezeichen_Profile",
|
||||||
|
"09_Programme_Settings_Lizenzen",
|
||||||
|
"10_Games_Savegames",
|
||||||
|
"11_Homelab_NAS_Doku",
|
||||||
|
"12_Exportierte_Listen",
|
||||||
|
"13_Treiber_Windows",
|
||||||
|
"14_Screenshots",
|
||||||
|
"15_Musik",
|
||||||
|
"99_Unsortiert_von_D_F_G"
|
||||||
|
)
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $BackupRoot | Out-Null
|
||||||
|
$Folders | ForEach-Object {
|
||||||
|
New-Item -ItemType Directory -Force -Path (Join-Path $BackupRoot $_) | Out-Null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop-Punkt: H: ist sichtbar, beschreibbar und hat genug freien Speicher.
|
||||||
|
|
||||||
|
## Phase 2: Inventar exportieren
|
||||||
|
|
||||||
|
### Installierte Programme
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$BackupRoot = "H:\Windows-Neuaufsetzen-Backup"
|
||||||
|
|
||||||
|
Get-ItemProperty `
|
||||||
|
HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*, `
|
||||||
|
HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* |
|
||||||
|
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate |
|
||||||
|
Where-Object { $_.DisplayName } |
|
||||||
|
Sort-Object DisplayName |
|
||||||
|
Export-Csv "$BackupRoot\12_Exportierte_Listen\installierte_programme.csv" -NoTypeInformation -Encoding UTF8
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional zusätzlich:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
winget list > "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\winget-list.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Laufwerksübersicht
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-Volume |
|
||||||
|
Sort-Object DriveLetter |
|
||||||
|
Select-Object DriveLetter, FileSystemLabel, FileSystem, Size, SizeRemaining |
|
||||||
|
Export-Csv "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\laufwerke.csv" -NoTypeInformation -Encoding UTF8
|
||||||
|
|
||||||
|
Get-Disk |
|
||||||
|
Select-Object Number, FriendlyName, SerialNumber, HealthStatus, Size, PartitionStyle |
|
||||||
|
Export-Csv "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\datentraeger.csv" -NoTypeInformation -Encoding UTF8
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows-Aktivierung
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wmic path softwarelicensingservice get OA3xOriginalProductKey > "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\windows_oem_key.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
Zusätzlich Screenshot speichern:
|
||||||
|
|
||||||
|
- Windows-Aktivierung
|
||||||
|
- Datenträgerverwaltung
|
||||||
|
- Apps & Features
|
||||||
|
- Gerätemanager
|
||||||
|
- Netzwerkadapter
|
||||||
|
|
||||||
|
Stop-Punkt: Programmliste, Laufwerkslisten und wichtige Screenshots liegen auf H:.
|
||||||
|
|
||||||
|
## Phase 3: Muss-Daten sichern
|
||||||
|
|
||||||
|
Die folgenden Daten haben Priorität.
|
||||||
|
|
||||||
|
### Benutzerordner
|
||||||
|
|
||||||
|
Passe `<Benutzername>` an.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$User = "C:\Users\<Benutzername>"
|
||||||
|
$BackupRoot = "H:\Windows-Neuaufsetzen-Backup"
|
||||||
|
|
||||||
|
robocopy "$User\Desktop" "$BackupRoot\01_Desktop" /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:"$BackupRoot\12_Exportierte_Listen\backup_desktop.log"
|
||||||
|
robocopy "$User\Documents" "$BackupRoot\02_Dokumente" /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:"$BackupRoot\12_Exportierte_Listen\backup_dokumente.log"
|
||||||
|
robocopy "$User\Pictures" "$BackupRoot\03_Bilder" /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:"$BackupRoot\12_Exportierte_Listen\backup_bilder.log"
|
||||||
|
robocopy "$User\Videos" "$BackupRoot\04_Videos" /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:"$BackupRoot\12_Exportierte_Listen\backup_videos.log"
|
||||||
|
robocopy "$User\Music" "$BackupRoot\15_Musik" /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:"$BackupRoot\12_Exportierte_Listen\backup_music.log"
|
||||||
|
```
|
||||||
|
|
||||||
|
Downloads nicht blind komplett übernehmen. Erst wichtige Installer, PDFs, ZIPs, Rechnungen, Exporte und persönliche Dateien aussortieren.
|
||||||
|
|
||||||
|
### Kritische versteckte Daten
|
||||||
|
|
||||||
|
Prüfen und bei Bedarf sichern:
|
||||||
|
|
||||||
|
| Pfad | Warum |
|
||||||
|
|---|---|
|
||||||
|
| `C:\Users\<Benutzername>\.ssh` | SSH Keys |
|
||||||
|
| `C:\Users\<Benutzername>\.gitconfig` | Git-Konfiguration |
|
||||||
|
| `C:\Users\<Benutzername>\AppData\Roaming` | wichtige App-Einstellungen |
|
||||||
|
| `C:\Users\<Benutzername>\AppData\Local` | Browserprofile, App-Daten |
|
||||||
|
| `C:\ProgramData` | gemeinsame App-Daten, Lizenzen |
|
||||||
|
| `C:\Users\<Benutzername>\Documents\Outlook-Dateien` | PST/Outlook-Archive |
|
||||||
|
|
||||||
|
Empfehlung: AppData nur sichern, später aber nicht komplett zurückkopieren.
|
||||||
|
|
||||||
|
## Phase 4: Spezialdaten prüfen
|
||||||
|
|
||||||
|
Diese Daten sind leicht zu übersehen.
|
||||||
|
|
||||||
|
- Banking4-Daten, Exporte, Tresore
|
||||||
|
- Passwortmanager-Backups oder lokale Datenbanken
|
||||||
|
- 2FA-Recovery-Codes
|
||||||
|
- Browser-Lesezeichen-Export
|
||||||
|
- lokale Spielstände ohne Cloud-Sync
|
||||||
|
- Steuerunterlagen
|
||||||
|
- Verträge und Rechnungen
|
||||||
|
- GPG/PGP Keys
|
||||||
|
- Zertifikate
|
||||||
|
- VPN-Profile
|
||||||
|
- API-Keys und `.env` Dateien
|
||||||
|
- Homelab-, NAS- und Router-Dokumentation
|
||||||
|
- Docker Desktop Daten
|
||||||
|
- WSL-Distributionen
|
||||||
|
- virtuelle Maschinen
|
||||||
|
|
||||||
|
### WSL exportieren, falls genutzt
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wsl --list --verbose > "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen\wsl-distros.txt"
|
||||||
|
wsl --export <DistroName> "H:\Windows-Neuaufsetzen-Backup\09_Programme_Settings_Lizenzen\<DistroName>.tar"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 5: D:, F: und G: analysieren
|
||||||
|
|
||||||
|
Nicht löschen. Erst suchen.
|
||||||
|
|
||||||
|
PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$BackupRoot = "H:\Windows-Neuaufsetzen-Backup"
|
||||||
|
$SearchRoots = @("D:\", "F:\", "G:\")
|
||||||
|
$Patterns = @(
|
||||||
|
"Users",
|
||||||
|
"Windows.old",
|
||||||
|
"Dokumente",
|
||||||
|
"Documents",
|
||||||
|
"Bilder",
|
||||||
|
"Pictures",
|
||||||
|
"Desktop",
|
||||||
|
"Downloads",
|
||||||
|
"Projekte",
|
||||||
|
"Projects",
|
||||||
|
"Backup",
|
||||||
|
"NAS",
|
||||||
|
"Git",
|
||||||
|
"Python",
|
||||||
|
"Banking",
|
||||||
|
"Steuern",
|
||||||
|
"Vertraege",
|
||||||
|
"Verträge"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($Root in $SearchRoots) {
|
||||||
|
if (Test-Path $Root) {
|
||||||
|
Get-ChildItem -Path $Root -Directory -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object FullName, LastWriteTime |
|
||||||
|
Export-Csv "$BackupRoot\12_Exportierte_Listen\top_level_$($Root[0]).csv" -NoTypeInformation -Encoding UTF8
|
||||||
|
|
||||||
|
foreach ($Pattern in $Patterns) {
|
||||||
|
Get-ChildItem -Path $Root -Recurse -Directory -ErrorAction SilentlyContinue -Filter "*$Pattern*" |
|
||||||
|
Select-Object FullName, LastWriteTime |
|
||||||
|
Export-Csv "$BackupRoot\12_Exportierte_Listen\fundstellen_$($Root[0])_$Pattern.csv" -NoTypeInformation -Encoding UTF8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Alles Unklare zuerst nach `H:\Windows-Neuaufsetzen-Backup\99_Unsortiert_von_D_F_G\` sichern.
|
||||||
|
|
||||||
|
Stop-Punkt: Für D:, F: und G: ist klar, was wichtig ist, was neu installiert werden kann und was später gelöscht werden darf.
|
||||||
|
|
||||||
|
## Phase 6: Treiber und Installationsmedien vorbereiten
|
||||||
|
|
||||||
|
Vorher herunterladen und auf H: speichern:
|
||||||
|
|
||||||
|
- Windows 11 Media Creation Tool
|
||||||
|
- Mainboard-Chipsatztreiber
|
||||||
|
- LAN-Treiber
|
||||||
|
- WLAN-Treiber
|
||||||
|
- GPU-Treiber
|
||||||
|
- Audio-Treiber
|
||||||
|
- Bluetooth-Treiber, falls relevant
|
||||||
|
- Drucker-/Scanner-Treiber, falls relevant
|
||||||
|
|
||||||
|
Minimum: LAN/WLAN-Treiber müssen offline verfügbar sein.
|
||||||
|
|
||||||
|
Stop-Punkt: Windows-USB-Stick funktioniert und Netzwerk-Treiber liegen auf H:.
|
||||||
|
|
||||||
|
## Phase 7: Backup prüfen
|
||||||
|
|
||||||
|
Pflichtprüfung:
|
||||||
|
|
||||||
|
- H: abziehen und wieder anschließen
|
||||||
|
- mehrere Bilder öffnen
|
||||||
|
- mehrere PDFs öffnen
|
||||||
|
- Office-Dateien öffnen
|
||||||
|
- Projektordner prüfen
|
||||||
|
- Banking-/Finanzdaten prüfen
|
||||||
|
- SSH-Key-Ordner prüfen
|
||||||
|
- Programmliste öffnen
|
||||||
|
- Robocopy-Logs auf Fehler prüfen
|
||||||
|
|
||||||
|
Optional Ordnergrößen vergleichen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-ChildItem "C:\Users\<Benutzername>\Documents" -Recurse -Force -ErrorAction SilentlyContinue |
|
||||||
|
Measure-Object -Property Length -Sum
|
||||||
|
|
||||||
|
Get-ChildItem "H:\Windows-Neuaufsetzen-Backup\02_Dokumente" -Recurse -Force -ErrorAction SilentlyContinue |
|
||||||
|
Measure-Object -Property Length -Sum
|
||||||
|
```
|
||||||
|
|
||||||
|
Go/No-Go:
|
||||||
|
|
||||||
|
- Go: wichtige Daten sind auf H: lesbar und mindestens die kritischsten Daten existieren zusätzlich auf NAS, Cloud oder zweiter Platte.
|
||||||
|
- No-Go: unbekannte Daten auf D:, F: oder G:, fehlende Browser-/Passwort-/2FA-Sicherung, unklarer Banking4-Speicherort, kein funktionierender Netzwerk-Treiber.
|
||||||
|
|
||||||
|
## Phase 8: Ziel-SSD für Windows festlegen
|
||||||
|
|
||||||
|
Empfehlung:
|
||||||
|
|
||||||
|
- Eine schnelle, zuverlässige SSD als neues C:
|
||||||
|
- Games, Daten und Projekte getrennt halten
|
||||||
|
- Alte Windows-SSD erst später löschen
|
||||||
|
|
||||||
|
Vermutlicher Kandidat:
|
||||||
|
|
||||||
|
- Samsung SSD 980 PRO 1TB, falls sie bewusst komplett als neue System-SSD neu partitioniert werden soll
|
||||||
|
- WDC WDS100T2B0C 1TB, falls die aktuelle M.2-Datenplatte nach vollstaendiger Sicherung als neues Systemlaufwerk dienen soll
|
||||||
|
- nicht einfach `F:` auswaehlen, ohne die physische SSD-Struktur zu beachten, da `F:` und `C:` auf derselben SSD liegen
|
||||||
|
|
||||||
|
Vor der Installation ideal:
|
||||||
|
|
||||||
|
- Nur Ziel-SSD angeschlossen lassen
|
||||||
|
- Backup-HDD H: abziehen
|
||||||
|
- andere interne Laufwerke abziehen, falls praktisch möglich
|
||||||
|
|
||||||
|
Das verhindert, dass Windows Bootpartitionen auf dem falschen Datenträger ablegt.
|
||||||
|
|
||||||
|
## Phase 9: Windows neu installieren
|
||||||
|
|
||||||
|
Installation:
|
||||||
|
|
||||||
|
1. Vom Windows-USB-Stick booten.
|
||||||
|
2. Benutzerdefinierte Installation wählen.
|
||||||
|
3. Ziel-SSD eindeutig identifizieren.
|
||||||
|
4. Nur auf der Ziel-SSD alte Partitionen löschen.
|
||||||
|
5. Nicht zugeordneten Speicher auf der Ziel-SSD auswählen.
|
||||||
|
6. Windows installieren.
|
||||||
|
|
||||||
|
Nicht anfassen:
|
||||||
|
|
||||||
|
- externe Backup-HDD
|
||||||
|
- Datenlaufwerke
|
||||||
|
- alte Windows-SSD, solange sie nicht final geprüft wurde
|
||||||
|
|
||||||
|
## Phase 10: Ersteinrichtung
|
||||||
|
|
||||||
|
Direkt nach der Installation:
|
||||||
|
|
||||||
|
- Windows Update vollständig laufen lassen
|
||||||
|
- Chipsatztreiber installieren
|
||||||
|
- GPU-Treiber installieren
|
||||||
|
- LAN/WLAN prüfen
|
||||||
|
- Windows-Aktivierung prüfen
|
||||||
|
- Laufwerksbuchstaben sauber vergeben
|
||||||
|
- Windows Defender und Firewall prüfen
|
||||||
|
- BitLocker bewusst aktivieren oder deaktiviert lassen
|
||||||
|
- Wiederherstellungspunkt erstellen
|
||||||
|
|
||||||
|
Basisprogramme:
|
||||||
|
|
||||||
|
- Browser
|
||||||
|
- Passwortmanager
|
||||||
|
- 7-Zip
|
||||||
|
- Office oder LibreOffice
|
||||||
|
- Banking4
|
||||||
|
- Git
|
||||||
|
- VS Code / Codex / Dev-Tools
|
||||||
|
- Docker Desktop / WSL, falls benötigt
|
||||||
|
- Trading-/Finanztools
|
||||||
|
- Drucker/Scanner
|
||||||
|
- Steam / Battle.net
|
||||||
|
|
||||||
|
## Phase 11: Daten kontrolliert zurückholen
|
||||||
|
|
||||||
|
Zuerst:
|
||||||
|
|
||||||
|
- Dokumente
|
||||||
|
- Bilder
|
||||||
|
- Projekte
|
||||||
|
- Finanzen
|
||||||
|
- Desktop
|
||||||
|
- wichtige Downloads
|
||||||
|
- SSH Keys
|
||||||
|
- Browser-Lesezeichen
|
||||||
|
|
||||||
|
Danach gezielt:
|
||||||
|
|
||||||
|
- einzelne App-Konfigurationen
|
||||||
|
- Spielstände
|
||||||
|
- WSL-Distributionen
|
||||||
|
- Docker-Daten
|
||||||
|
- Outlook/PST
|
||||||
|
|
||||||
|
Nicht tun:
|
||||||
|
|
||||||
|
- `AppData` komplett zurückkopieren
|
||||||
|
- alte Windows-Ordner zurückmischen
|
||||||
|
- Programme aus alten Ordnern starten statt neu installieren
|
||||||
|
|
||||||
|
## Phase 12: Alte Datenträger bereinigen
|
||||||
|
|
||||||
|
Erst nach mehreren Tagen stabiler Nutzung:
|
||||||
|
|
||||||
|
- D: alte Windows-SSD final prüfen
|
||||||
|
- alte Benutzerordner gezielt archivieren oder löschen
|
||||||
|
- alte Windows-/Recovery-/EFI-Partitionen nur löschen, wenn sicher nicht davon gebootet wird
|
||||||
|
- Games-Laufwerke neu strukturieren
|
||||||
|
- Datenlaufwerke sinnvoll benennen
|
||||||
|
|
||||||
|
Zielstruktur:
|
||||||
|
|
||||||
|
| Laufwerk | Zweck |
|
||||||
|
|---|---|
|
||||||
|
| C: | Windows + Programme |
|
||||||
|
| D: | Daten / Projekte |
|
||||||
|
| E: | Games |
|
||||||
|
| F: | Arbeits-SSD / schnelle Daten |
|
||||||
|
| H: | Backup extern |
|
||||||
|
|
||||||
|
## Finale Checkliste vor dem Löschen
|
||||||
|
|
||||||
|
- [ ] Backup-Struktur auf H: erstellt
|
||||||
|
- [ ] Programmliste exportiert
|
||||||
|
- [ ] Laufwerksliste exportiert
|
||||||
|
- [ ] Windows-Aktivierung dokumentiert
|
||||||
|
- [ ] Benutzerordner gesichert
|
||||||
|
- [ ] Browser-Lesezeichen exportiert oder Sync geprüft
|
||||||
|
- [ ] Passwortmanager geprüft
|
||||||
|
- [ ] 2FA-Recovery-Codes gesichert
|
||||||
|
- [ ] SSH/API/GPG/Zertifikate gesichert
|
||||||
|
- [ ] Banking4-Speicherort geprüft und gesichert
|
||||||
|
- [ ] Homelab-/NAS-Doku gesichert
|
||||||
|
- [ ] D:, F: und G: analysiert
|
||||||
|
- [ ] Unklare Daten nach `99_Unsortiert_von_D_F_G` kopiert
|
||||||
|
- [ ] LAN/WLAN-Treiber auf H: gespeichert
|
||||||
|
- [ ] Windows-USB-Stick erstellt
|
||||||
|
- [ ] Backup-Dateien stichprobenartig geöffnet
|
||||||
|
- [ ] Kritische Daten zusätzlich auf NAS, Cloud oder zweiter Platte gesichert
|
||||||
|
- [ ] Ziel-SSD eindeutig festgelegt
|
||||||
|
|
||||||
|
Erst wenn alle Punkte erledigt sind, ist die Neuinstallation freigegeben.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
BASE_DOMAIN=kaleschke.info
|
BASE_DOMAIN=kaleschke.info
|
||||||
TRAEFIK_DOMAIN=traefik.kaleschke.info
|
TRAEFIK_DOMAIN=traefik.kaleschke.info
|
||||||
AUTH_DOMAIN=auth.kaleschke.info
|
AUTH_DOMAIN=auth.kaleschke.info
|
||||||
HOME_DOMAIN=home.kaleschke.info
|
GLANCE_DOMAIN=glance.kaleschke.info
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "53:53/tcp"
|
- "53:53/tcp"
|
||||||
- "53:53/udp"
|
- "53:53/udp"
|
||||||
- "8082:80"
|
- "100.80.98.33:8082:80"
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
services:
|
||||||
|
plex:
|
||||||
|
image: plexinc/pms-docker:1.43.1.10611-1e34174b1@sha256:8b5bcdf7b506fe051aa1a0a0d464efdb3ad8c0fb1f8a4dfb27a8c489b609920c
|
||||||
|
container_name: plex
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
PLEX_UID: "99"
|
||||||
|
PLEX_GID: "100"
|
||||||
|
CHANGE_CONFIG_DIR_OWNERSHIP: "true"
|
||||||
|
PLEX_CLAIM: ${PLEX_CLAIM:-}
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/appdata/plex/config:/config
|
||||||
|
- /mnt/user/appdata/plex/transcode:/transcode
|
||||||
|
- /mnt/user/media:/data:ro
|
||||||
|
- /mnt/user/photos:/photos:ro
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
@@ -3,6 +3,8 @@ services:
|
|||||||
image: ghcr.io/qdm12/ddns-updater:latest@sha256:ee16ab4f6203bf9e5b0925d38a0b4ebf2d9f23771f933cfb2f5a2dbd5f9a2f88
|
image: ghcr.io/qdm12/ddns-updater:latest@sha256:ee16ab4f6203bf9e5b0925d38a0b4ebf2d9f23771f933cfb2f5a2dbd5f9a2f88
|
||||||
container_name: ddns-updater
|
container_name: ddns-updater
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
networks:
|
networks:
|
||||||
- frontend_net
|
- frontend_net
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7.4-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
|
||||||
container_name: Redis
|
container_name: Redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Monitoring Stack
|
||||||
|
|
||||||
|
Zielzustand: ein zentraler Observability-Stack fuer KalliLab CORE.
|
||||||
|
|
||||||
|
## Enthaltene Dienste
|
||||||
|
|
||||||
|
- `monitoring-grafana`: zentrale UI unter `https://monitoring.kaleschke.info`
|
||||||
|
- `monitoring-prometheus`: Metriken mit 30 Tagen Retention
|
||||||
|
- `monitoring-alertmanager`: Alert-Routing fuer Prometheus-Regeln
|
||||||
|
- `monitoring-alertmanager-ntfy-bridge`: uebersetzt Alertmanager-Webhooks zu ntfy-Pushes
|
||||||
|
- `monitoring-loki`: Container-Logs mit 30 Tagen Retention
|
||||||
|
- `monitoring-promtail`: Docker-Log-Discovery ueber read-only Docker-Socket
|
||||||
|
- `monitoring-node-exporter`: Host-Metriken
|
||||||
|
- `monitoring-cadvisor`: Container-Metriken
|
||||||
|
- `monitoring-blackbox-exporter`: externe HTTP-Erreichbarkeit als Uptime-Kuma-Ersatz
|
||||||
|
- `monitoring-influxdb3-core`: InfluxDB 3 Core fuer Home-Assistant-/Ecowitt-Langzeitdaten
|
||||||
|
|
||||||
|
Die alten Pfade `ops/loki` und `ops/grafana-influxdb` wurden am 2026-05-26 aus dem aktiven Repo entfernt. Rollback erfolgt bei Bedarf ueber Git-Historie, nicht ueber parallel gepflegte Compose-Verzeichnisse.
|
||||||
|
|
||||||
|
Live-Stand 2026-05-25: die zehn `monitoring-*` Container laufen produktiv, die alten Container `grafana`, `influxdb3-core`, `loki` und `alloy` sind in Docker nicht mehr vorhanden. Uptime Kuma ist durch Blackbox Exporter, Prometheus-Alerts und das Dashboard `Homelab / Availability` abgeloest.
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
|
||||||
|
Vor dem Deploy muessen diese Host-Dateien existieren:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt
|
||||||
|
/mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt
|
||||||
|
/mnt/user/appdata/secrets/influxdb3_admin_token.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Alle Dateien mit Rechten `600` anlegen. Werte niemals ins Git schreiben.
|
||||||
|
|
||||||
|
`monitoring-influxdb3-core` uebernimmt bewusst `/mnt/user/appdata/influxdb3/data` und `/mnt/user/appdata/influxdb3/plugins` vom bisherigen Grafana/Influx-Stack, damit Home-Assistant-/Ecowitt-Historie und Token-Katalog erhalten bleiben.
|
||||||
|
|
||||||
|
## Stack Environment
|
||||||
|
|
||||||
|
Default ist sicher lokal:
|
||||||
|
|
||||||
|
```env
|
||||||
|
INFLUXDB_BIND_IP=127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn Home Assistant aus der VM schreiben soll, in Komodo fuer den `monitoring`-Stack setzen:
|
||||||
|
|
||||||
|
```env
|
||||||
|
INFLUXDB_BIND_IP=192.168.178.58
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
1. Secrets anlegen. Erledigt.
|
||||||
|
2. Alten `ops/loki`-Stack stoppen, wenn `monitoring-loki` und `monitoring-promtail` live gehen. Erledigt.
|
||||||
|
3. Alten `ops/grafana-influxdb`-Stack stoppen, bevor `monitoring-influxdb3-core` den LAN-Port `192.168.178.58:8181` uebernimmt. Erledigt.
|
||||||
|
4. `monitoring` via Komodo deployen und `INFLUXDB_BIND_IP=192.168.178.58` erst setzen, wenn der Altcontainer den Port freigegeben hat. Erledigt.
|
||||||
|
5. Alte Repo-Verzeichnisse `ops/loki` und `ops/grafana-influxdb` entfernen. Erledigt.
|
||||||
|
6. Optionales Dashboard-Bootstrap-Profil einmalig ausfuehren.
|
||||||
|
7. Home Assistant Writer gegen `http://192.168.178.58:8181/` pruefen; `401 Unauthorized` ohne Token ist erwartbar.
|
||||||
|
|
||||||
|
## Smoke-Tests
|
||||||
|
|
||||||
|
- `https://monitoring.kaleschke.info` leitet zu Authelia.
|
||||||
|
- Grafana-Datasources `Prometheus`, `Loki` und `InfluxDB 3 Core` testen erfolgreich.
|
||||||
|
- Prometheus Targets: `prometheus`, `node-exporter`, `cadvisor`, `traefik`, `blackbox-http`.
|
||||||
|
- Alertmanager ist erreichbar und sendet ueber `monitoring-alertmanager-ntfy-bridge` nach `https://ntfy.kaleschke.info/homelab-alerts`.
|
||||||
|
- Loki zeigt Container-Logs mit Labels `container`, `compose_project`, `compose_service`.
|
||||||
|
- InfluxDB 3 Core enthaelt die Datenbank `homelab`.
|
||||||
|
|
||||||
|
## Abloesestand
|
||||||
|
|
||||||
|
- Dozzle bleibt abgeloest: `Homelab / Containers + Logs` ersetzt Live-Logs und Error-Rate.
|
||||||
|
- Glances erst stoppen, wenn `Homelab / Host Overview` und `Homelab / Containers + Logs` fuer CPU, RAM, Disk, Network, Container-CPU und Container-RAM passen.
|
||||||
|
- Uptime Kuma ist entfernt; `Homelab / Availability`, Blackbox Exporter und Prometheus-Alerts sind der Zielzustand fuer HTTP-Verfuegbarkeit.
|
||||||
|
- Dashboard-Zielbestand: `Homelab / Availability`, `Homelab / Containers + Logs`, `Homelab / Host Overview`, `Traefik Official Standalone Dashboard`.
|
||||||
|
|
||||||
|
## Alerting
|
||||||
|
|
||||||
|
Prometheus wertet `monitoring/prometheus/alerts.yml` aus und sendet an `monitoring-alertmanager`.
|
||||||
|
Alertmanager routet alle Alerts an den ntfy-Bridge-Container.
|
||||||
|
Der Bridge-Container postet nach `https://ntfy.kaleschke.info/homelab-alerts`.
|
||||||
|
|
||||||
|
Blackbox-HTTP-Alerts unterscheiden zwischen einem einzelnen kaputten Endpoint und einem externen Connectivity-Problem:
|
||||||
|
|
||||||
|
- `HomelabExternalConnectivityDown` feuert, wenn mindestens 5 Public-Endpoints gleichzeitig fuer 8 Minuten nicht erreichbar sind. Das deckt WAN-, DNS- oder Provider-Ausfaelle ab, inklusive laengerer DSL-Reconnects.
|
||||||
|
- `HomelabEndpointDown` feuert fuer einzelne Endpoints erst nach 8 Minuten und wird unterdrueckt, solange der Sammelalert aktiv ist. Dadurch erzeugt ein Telekom-24h-Reconnect keine ntfy-Flut pro Domain.
|
||||||
|
|
||||||
|
Test:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS http://alertmanager-ntfy-bridge:8080/healthz
|
||||||
|
```
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
|
|
||||||
|
NTFY_URL = os.environ.get("NTFY_URL", "https://ntfy.kaleschke.info/homelab-alerts")
|
||||||
|
|
||||||
|
|
||||||
|
def priority_for(status, severity):
|
||||||
|
if status == "resolved":
|
||||||
|
return "2"
|
||||||
|
if severity == "critical":
|
||||||
|
return "5"
|
||||||
|
if severity == "warning":
|
||||||
|
return "4"
|
||||||
|
return "3"
|
||||||
|
|
||||||
|
|
||||||
|
def tags_for(status, severity):
|
||||||
|
if status == "resolved":
|
||||||
|
return "white_check_mark"
|
||||||
|
if severity == "critical":
|
||||||
|
return "rotating_light"
|
||||||
|
if severity == "warning":
|
||||||
|
return "warning"
|
||||||
|
return "information_source"
|
||||||
|
|
||||||
|
|
||||||
|
def alert_message(alert):
|
||||||
|
labels = alert.get("labels", {})
|
||||||
|
annotations = alert.get("annotations", {})
|
||||||
|
status = alert.get("status", "firing")
|
||||||
|
severity = labels.get("severity", "info")
|
||||||
|
alertname = labels.get("alertname", "Alert")
|
||||||
|
target = labels.get("instance") or labels.get("service") or labels.get("mountpoint") or "homelab"
|
||||||
|
summary = annotations.get("summary") or alertname
|
||||||
|
description = annotations.get("description") or ""
|
||||||
|
|
||||||
|
title = f"{status.upper()} {severity}: {alertname}"
|
||||||
|
lines = [
|
||||||
|
summary,
|
||||||
|
f"Target: {target}",
|
||||||
|
]
|
||||||
|
if description and description != summary:
|
||||||
|
lines.append(description)
|
||||||
|
return title, "\n".join(lines), priority_for(status, severity), tags_for(status, severity)
|
||||||
|
|
||||||
|
|
||||||
|
def send_ntfy(title, message, priority, tags):
|
||||||
|
req = urllib.request.Request(
|
||||||
|
NTFY_URL,
|
||||||
|
data=message.encode("utf-8"),
|
||||||
|
headers={
|
||||||
|
"Title": title,
|
||||||
|
"Priority": priority,
|
||||||
|
"Tags": tags,
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=15) as response:
|
||||||
|
response.read()
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == "/healthz":
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b"ok\n")
|
||||||
|
return
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path != "/alertmanager":
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
|
||||||
|
length = int(self.headers.get("Content-Length", "0"))
|
||||||
|
payload = json.loads(self.rfile.read(length) or b"{}")
|
||||||
|
alerts = payload.get("alerts", [])
|
||||||
|
sent = 0
|
||||||
|
|
||||||
|
for alert in alerts:
|
||||||
|
title, message, priority, tags = alert_message(alert)
|
||||||
|
try:
|
||||||
|
send_ntfy(title, message, priority, tags)
|
||||||
|
sent += 1
|
||||||
|
except urllib.error.URLError as exc:
|
||||||
|
print(f"ntfy send failed: {exc}", file=sys.stderr, flush=True)
|
||||||
|
self.send_response(502)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b"ntfy send failed\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"sent {sent} ntfy notifications", flush=True)
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(f"sent {sent}\n".encode("utf-8"))
|
||||||
|
|
||||||
|
def log_message(self, fmt, *args):
|
||||||
|
print(fmt % args, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
server = ThreadingHTTPServer(("0.0.0.0", 8080), Handler)
|
||||||
|
print(f"alertmanager ntfy bridge listening on :8080 -> {NTFY_URL}", flush=True)
|
||||||
|
server.serve_forever()
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
global:
|
||||||
|
resolve_timeout: 5m
|
||||||
|
|
||||||
|
route:
|
||||||
|
receiver: ntfy-homelab
|
||||||
|
group_by:
|
||||||
|
- alertname
|
||||||
|
- severity
|
||||||
|
group_wait: 20s
|
||||||
|
group_interval: 5m
|
||||||
|
repeat_interval: 4h
|
||||||
|
routes:
|
||||||
|
- receiver: ntfy-homelab
|
||||||
|
matchers:
|
||||||
|
- severity="critical"
|
||||||
|
group_wait: 10s
|
||||||
|
group_interval: 2m
|
||||||
|
repeat_interval: 30m
|
||||||
|
|
||||||
|
receivers:
|
||||||
|
- name: ntfy-homelab
|
||||||
|
webhook_configs:
|
||||||
|
- url: http://alertmanager-ntfy-bridge:8080/alertmanager
|
||||||
|
send_resolved: true
|
||||||
|
max_alerts: 10
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
modules:
|
||||||
|
http_reachable:
|
||||||
|
prober: http
|
||||||
|
timeout: 10s
|
||||||
|
http:
|
||||||
|
valid_status_codes:
|
||||||
|
- 200
|
||||||
|
- 204
|
||||||
|
- 301
|
||||||
|
- 302
|
||||||
|
- 303
|
||||||
|
- 307
|
||||||
|
- 308
|
||||||
|
- 401
|
||||||
|
- 403
|
||||||
|
valid_http_versions:
|
||||||
|
- HTTP/1.1
|
||||||
|
- HTTP/2.0
|
||||||
|
follow_redirects: true
|
||||||
|
preferred_ip_protocol: ip4
|
||||||
|
|
||||||
|
tcp_connect:
|
||||||
|
prober: tcp
|
||||||
|
timeout: 5s
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
services:
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:v3.7.3
|
||||||
|
container_name: monitoring-prometheus
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --config.file=/etc/prometheus/prometheus.yml
|
||||||
|
- --storage.tsdb.path=/prometheus
|
||||||
|
- --storage.tsdb.retention.time=30d
|
||||||
|
- --web.enable-lifecycle
|
||||||
|
volumes:
|
||||||
|
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
|
- ./prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
|
||||||
|
- prometheus_data:/prometheus
|
||||||
|
networks:
|
||||||
|
- monitoring_net
|
||||||
|
expose:
|
||||||
|
- "9090"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
depends_on:
|
||||||
|
- alertmanager
|
||||||
|
- blackbox-exporter
|
||||||
|
- node-exporter
|
||||||
|
- cadvisor
|
||||||
|
|
||||||
|
alertmanager:
|
||||||
|
image: prom/alertmanager:v0.28.1
|
||||||
|
container_name: monitoring-alertmanager
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --config.file=/etc/alertmanager/alertmanager.yml
|
||||||
|
- --storage.path=/alertmanager
|
||||||
|
volumes:
|
||||||
|
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
|
||||||
|
- alertmanager_data:/alertmanager
|
||||||
|
networks:
|
||||||
|
- monitoring_net
|
||||||
|
expose:
|
||||||
|
- "9093"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
alertmanager-ntfy-bridge:
|
||||||
|
image: python:3.13-alpine
|
||||||
|
container_name: monitoring-alertmanager-ntfy-bridge
|
||||||
|
restart: unless-stopped
|
||||||
|
dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
environment:
|
||||||
|
NTFY_URL: https://ntfy.kaleschke.info/homelab-alerts
|
||||||
|
command:
|
||||||
|
- python
|
||||||
|
- /app/bridge.py
|
||||||
|
volumes:
|
||||||
|
- ./alertmanager-ntfy-bridge/bridge.py:/app/bridge.py:ro
|
||||||
|
networks:
|
||||||
|
- monitoring_net
|
||||||
|
expose:
|
||||||
|
- "8080"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
blackbox-exporter:
|
||||||
|
image: prom/blackbox-exporter:v0.27.0
|
||||||
|
container_name: monitoring-blackbox-exporter
|
||||||
|
restart: unless-stopped
|
||||||
|
dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
command:
|
||||||
|
- --config.file=/etc/blackbox_exporter/blackbox.yml
|
||||||
|
volumes:
|
||||||
|
- ./blackbox/blackbox.yml:/etc/blackbox_exporter/blackbox.yml:ro
|
||||||
|
networks:
|
||||||
|
- monitoring_net
|
||||||
|
expose:
|
||||||
|
- "9115"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
loki:
|
||||||
|
image: grafana/loki:3.7.2
|
||||||
|
container_name: monitoring-loki
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- -config.file=/etc/loki/loki-config.yml
|
||||||
|
volumes:
|
||||||
|
- ./loki/loki-config.yml:/etc/loki/loki-config.yml:ro
|
||||||
|
- loki_data:/loki
|
||||||
|
networks:
|
||||||
|
- monitoring_net
|
||||||
|
expose:
|
||||||
|
- "3100"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
promtail:
|
||||||
|
image: grafana/promtail:3.6.10
|
||||||
|
container_name: monitoring-promtail
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- -config.file=/etc/promtail/promtail-config.yml
|
||||||
|
volumes:
|
||||||
|
- ./promtail/promtail-config.yml:/etc/promtail/promtail-config.yml:ro
|
||||||
|
- promtail_positions:/positions
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||||
|
networks:
|
||||||
|
- monitoring_net
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
depends_on:
|
||||||
|
- loki
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:12.4.3
|
||||||
|
container_name: monitoring-grafana
|
||||||
|
restart: unless-stopped
|
||||||
|
dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
environment:
|
||||||
|
GF_SERVER_ROOT_URL: https://monitoring.kaleschke.info/
|
||||||
|
GF_SECURITY_ADMIN_USER: admin
|
||||||
|
GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/monitoring_grafana_admin_password
|
||||||
|
GF_USERS_ALLOW_SIGN_UP: "false"
|
||||||
|
GF_AUTH_ANONYMOUS_ENABLED: "false"
|
||||||
|
entrypoint:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
export GRAFANA_INFLUXDB_TOKEN="$$(cat /run/secrets/monitoring_grafana_influxdb_token)"
|
||||||
|
exec /run.sh
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||||
|
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||||
|
networks:
|
||||||
|
- monitoring_net
|
||||||
|
- frontend_net
|
||||||
|
secrets:
|
||||||
|
- monitoring_grafana_admin_password
|
||||||
|
- monitoring_grafana_influxdb_token
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
depends_on:
|
||||||
|
- prometheus
|
||||||
|
- loki
|
||||||
|
- influxdb3-core
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.docker.network=frontend_net
|
||||||
|
- traefik.http.routers.monitoring-grafana.rule=Host(`monitoring.kaleschke.info`)
|
||||||
|
- traefik.http.routers.monitoring-grafana.entrypoints=websecure
|
||||||
|
- traefik.http.routers.monitoring-grafana.tls=true
|
||||||
|
- traefik.http.routers.monitoring-grafana.tls.certresolver=le
|
||||||
|
- traefik.http.routers.monitoring-grafana.middlewares=authelia@file,secure-headers@file
|
||||||
|
- traefik.http.services.monitoring-grafana.loadbalancer.server.port=3000
|
||||||
|
|
||||||
|
grafana-dashboard-importer:
|
||||||
|
image: python:3.13-alpine
|
||||||
|
container_name: monitoring-grafana-dashboard-importer
|
||||||
|
restart: "no"
|
||||||
|
profiles:
|
||||||
|
- bootstrap
|
||||||
|
dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
networks:
|
||||||
|
- monitoring_net
|
||||||
|
- frontend_net
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
depends_on:
|
||||||
|
- grafana
|
||||||
|
secrets:
|
||||||
|
- monitoring_grafana_admin_password
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
python - <<'PY'
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
grafana_url = "http://grafana:3000"
|
||||||
|
with open("/run/secrets/monitoring_grafana_admin_password", encoding="utf-8") as secret:
|
||||||
|
password = secret.read().strip()
|
||||||
|
auth = base64.b64encode(f"admin:{password}".encode()).decode()
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Basic {auth}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
def request(path, payload=None, timeout=20):
|
||||||
|
data = None if payload is None else json.dumps(payload).encode()
|
||||||
|
req = urllib.request.Request(f"{grafana_url}{path}", data=data, headers=headers)
|
||||||
|
if payload is not None:
|
||||||
|
req.method = "POST"
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as response:
|
||||||
|
body = response.read()
|
||||||
|
return json.loads(body.decode() or "{}") if body else {}
|
||||||
|
|
||||||
|
def import_dashboard(payload, dashboard_id):
|
||||||
|
for attempt in range(1, 7):
|
||||||
|
try:
|
||||||
|
return request("/api/dashboards/import", payload)
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
body = exc.read().decode(errors="replace")[:300]
|
||||||
|
if attempt == 6:
|
||||||
|
raise RuntimeError(f"Dashboard {dashboard_id} import failed: {exc.code} {body}") from exc
|
||||||
|
print(f"Dashboard {dashboard_id} import attempt {attempt} failed: HTTP {exc.code} {body}")
|
||||||
|
time.sleep(5)
|
||||||
|
except Exception as exc:
|
||||||
|
if attempt == 6:
|
||||||
|
raise
|
||||||
|
print(f"Dashboard {dashboard_id} import attempt {attempt} failed: {exc}")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
for _ in range(60):
|
||||||
|
try:
|
||||||
|
request("/api/health", timeout=5)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
time.sleep(2)
|
||||||
|
else:
|
||||||
|
raise SystemExit("Grafana did not become ready in time")
|
||||||
|
|
||||||
|
dashboards = [
|
||||||
|
(17346, "Prometheus"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def fetch_dashboard(dashboard_id):
|
||||||
|
url = f"https://grafana.com/api/dashboards/{dashboard_id}/revisions/latest/download"
|
||||||
|
for attempt in range(1, 7):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=30) as response:
|
||||||
|
return json.loads(response.read().decode())
|
||||||
|
except Exception as exc:
|
||||||
|
if attempt == 6:
|
||||||
|
raise
|
||||||
|
print(f"Dashboard {dashboard_id} download attempt {attempt} failed: {exc}")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
for dashboard_id, default_datasource in dashboards:
|
||||||
|
dashboard = fetch_dashboard(dashboard_id)
|
||||||
|
|
||||||
|
inputs = []
|
||||||
|
for item in dashboard.get("__inputs", []):
|
||||||
|
plugin_id = item.get("pluginId", "").lower()
|
||||||
|
value = "Loki" if plugin_id == "loki" or default_datasource == "Loki" else "Prometheus"
|
||||||
|
inputs.append({
|
||||||
|
"name": item.get("name"),
|
||||||
|
"type": item.get("type", "datasource"),
|
||||||
|
"pluginId": item.get("pluginId", "prometheus"),
|
||||||
|
"value": value,
|
||||||
|
})
|
||||||
|
|
||||||
|
import_dashboard({
|
||||||
|
"dashboard": dashboard,
|
||||||
|
"overwrite": True,
|
||||||
|
"inputs": inputs,
|
||||||
|
}, dashboard_id)
|
||||||
|
print(f"Imported Grafana dashboard {dashboard_id}")
|
||||||
|
PY
|
||||||
|
echo "Dashboard import complete."
|
||||||
|
|
||||||
|
node-exporter:
|
||||||
|
image: prom/node-exporter:v1.9.1
|
||||||
|
container_name: monitoring-node-exporter
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --path.procfs=/host/proc
|
||||||
|
- --path.sysfs=/host/sys
|
||||||
|
- --path.rootfs=/rootfs
|
||||||
|
- --collector.filesystem.mount-points-exclude=^/(dev|proc|sys|run|var/lib/docker/.+|var/lib/containers/storage/.+)($|/)
|
||||||
|
volumes:
|
||||||
|
- /proc:/host/proc:ro
|
||||||
|
- /sys:/host/sys:ro
|
||||||
|
- /:/rootfs:ro
|
||||||
|
networks:
|
||||||
|
- monitoring_net
|
||||||
|
expose:
|
||||||
|
- "9100"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
cadvisor:
|
||||||
|
image: ghcr.io/google/cadvisor:v0.53.0
|
||||||
|
container_name: monitoring-cadvisor
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- --docker_only=true
|
||||||
|
- --housekeeping_interval=30s
|
||||||
|
- --store_container_labels=false
|
||||||
|
volumes:
|
||||||
|
- /:/rootfs:ro
|
||||||
|
- /var/run:/var/run:ro
|
||||||
|
- /sys:/sys:ro
|
||||||
|
- /var/lib/docker:/var/lib/docker:ro
|
||||||
|
- /dev/disk:/dev/disk:ro
|
||||||
|
networks:
|
||||||
|
- monitoring_net
|
||||||
|
expose:
|
||||||
|
- "8080"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
influxdb3-core:
|
||||||
|
image: influxdb:3.9.1-core@sha256:1d58c8b9ac90153ae3a020ede2810c8284933dda50ac71e7573389ab6f012128
|
||||||
|
container_name: monitoring-influxdb3-core
|
||||||
|
user: "0"
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${INFLUXDB_BIND_IP:-127.0.0.1}:8181:8181"
|
||||||
|
command:
|
||||||
|
- influxdb3
|
||||||
|
- serve
|
||||||
|
- --node-id=kallilabcore
|
||||||
|
- --object-store=file
|
||||||
|
- --data-dir=/var/lib/influxdb3/data
|
||||||
|
- --plugin-dir=/var/lib/influxdb3/plugins
|
||||||
|
- --admin-token-file=/run/secrets/influxdb3_admin_token
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/appdata/influxdb3/data:/var/lib/influxdb3/data
|
||||||
|
- /mnt/user/appdata/influxdb3/plugins:/var/lib/influxdb3/plugins
|
||||||
|
secrets:
|
||||||
|
- influxdb3_admin_token
|
||||||
|
networks:
|
||||||
|
- monitoring_net
|
||||||
|
- monitoring_influx_lan
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
networks:
|
||||||
|
monitoring_net:
|
||||||
|
name: monitoring_net
|
||||||
|
driver: bridge
|
||||||
|
monitoring_influx_lan:
|
||||||
|
driver: bridge
|
||||||
|
frontend_net:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
prometheus_data:
|
||||||
|
alertmanager_data:
|
||||||
|
loki_data:
|
||||||
|
promtail_positions:
|
||||||
|
grafana_data:
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
monitoring_grafana_admin_password:
|
||||||
|
file: /mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt
|
||||||
|
monitoring_grafana_influxdb_token:
|
||||||
|
file: /mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt
|
||||||
|
influxdb3_admin_token:
|
||||||
|
file: /mnt/user/appdata/secrets/influxdb3_admin_token.json
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
{
|
||||||
|
"uid": "homelab-availability",
|
||||||
|
"title": "Homelab / Availability",
|
||||||
|
"tags": ["homelab", "blackbox", "uptime"],
|
||||||
|
"timezone": "browser",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "30s",
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"name": "target",
|
||||||
|
"label": "Target",
|
||||||
|
"type": "query",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"query": "label_values(probe_success{job=\"blackbox-http\"}, instance)",
|
||||||
|
"refresh": 1,
|
||||||
|
"includeAll": true,
|
||||||
|
"multi": true,
|
||||||
|
"allValue": ".+",
|
||||||
|
"current": {
|
||||||
|
"selected": true,
|
||||||
|
"text": "All",
|
||||||
|
"value": "$__all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Endpoints up",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 5, "w": 6, "x": 0, "y": 0},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "sum(probe_success{job=\"blackbox-http\", instance=~\"${target:regex}\"})"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{"color": "red", "value": null},
|
||||||
|
{"color": "green", "value": 1}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "stat",
|
||||||
|
"title": "Endpoints down",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 5, "w": 6, "x": 6, "y": 0},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "count(probe_success{job=\"blackbox-http\", instance=~\"${target:regex}\"}) - sum(probe_success{job=\"blackbox-http\", instance=~\"${target:regex}\"})"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"unit": "short",
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{"color": "green", "value": null},
|
||||||
|
{"color": "red", "value": 1}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"options": {"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Probe duration",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "probe_duration_seconds{job=\"blackbox-http\", instance=~\"${target:regex}\"}",
|
||||||
|
"legendFormat": "{{instance}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {"defaults": {"unit": "s"}, "overrides": []},
|
||||||
|
"options": {"legend": {"displayMode": "list", "placement": "bottom"}}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Availability",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 5},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "probe_success{job=\"blackbox-http\", instance=~\"${target:regex}\"}",
|
||||||
|
"legendFormat": "{{instance}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {"defaults": {"unit": "bool"}, "overrides": []},
|
||||||
|
"options": {"legend": {"displayMode": "list", "placement": "bottom"}}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "table",
|
||||||
|
"title": "HTTP status",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 9, "w": 24, "x": 0, "y": 13},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "probe_http_status_code{job=\"blackbox-http\", instance=~\"${target:regex}\"}",
|
||||||
|
"format": "table",
|
||||||
|
"instant": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"transformations": [
|
||||||
|
{
|
||||||
|
"id": "organize",
|
||||||
|
"options": {
|
||||||
|
"excludeByName": {"Time": true, "__name__": true, "job": true},
|
||||||
|
"renameByName": {"Value": "status_code", "instance": "target"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"uid": "homelab-containers-logs",
|
||||||
|
"title": "Homelab / Containers + Logs",
|
||||||
|
"tags": ["homelab", "cadvisor", "loki", "dozzle-replacement"],
|
||||||
|
"timezone": "browser",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "30s",
|
||||||
|
"time": {
|
||||||
|
"from": "now-1h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"name": "service",
|
||||||
|
"label": "Service",
|
||||||
|
"type": "query",
|
||||||
|
"datasource": "Loki",
|
||||||
|
"query": "label_values(compose_service)",
|
||||||
|
"refresh": 1,
|
||||||
|
"includeAll": true,
|
||||||
|
"multi": true,
|
||||||
|
"allValue": ".+",
|
||||||
|
"current": {
|
||||||
|
"selected": true,
|
||||||
|
"text": "All",
|
||||||
|
"value": "$__all"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search",
|
||||||
|
"label": "Regex Match",
|
||||||
|
"type": "textbox",
|
||||||
|
"query": ".+",
|
||||||
|
"current": {
|
||||||
|
"selected": true,
|
||||||
|
"text": ".+",
|
||||||
|
"value": ".+"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Container CPU",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "sum by (name) (rate(container_cpu_usage_seconds_total{name!=\"\"}[5m]))",
|
||||||
|
"legendFormat": "{{name}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {"defaults": {"unit": "cores"}, "overrides": []}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Container memory",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "sum by (name) (container_memory_working_set_bytes{name!=\"\"})",
|
||||||
|
"legendFormat": "{{name}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {"defaults": {"unit": "bytes"}, "overrides": []}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Log error rate",
|
||||||
|
"datasource": "Loki",
|
||||||
|
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 8},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "sum by (compose_service) (count_over_time({compose_service=~\"${service:regex}\", container=~\".+\"} |~ \"(?i)error|exception|panic|fatal|traceback|oom|killed\" [$__interval]))",
|
||||||
|
"legendFormat": "{{compose_service}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "logs",
|
||||||
|
"title": "Live logs",
|
||||||
|
"datasource": "Loki",
|
||||||
|
"gridPos": {"h": 14, "w": 24, "x": 0, "y": 16},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "{compose_service=~\"${service:regex}\", container=~\".+\"} |~ \"$search\""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"showLabels": false,
|
||||||
|
"showTime": true,
|
||||||
|
"sortOrder": "Descending",
|
||||||
|
"wrapLogMessage": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"uid": "homelab-host-overview",
|
||||||
|
"title": "Homelab / Host Overview",
|
||||||
|
"tags": ["homelab", "node-exporter", "glances-replacement"],
|
||||||
|
"timezone": "browser",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"version": 1,
|
||||||
|
"refresh": "30s",
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "CPU usage",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "100 - (avg(rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
|
||||||
|
"legendFormat": "CPU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {"defaults": {"unit": "percent", "min": 0, "max": 100}, "overrides": []}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Memory usage",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "100 * (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes))",
|
||||||
|
"legendFormat": "Memory"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {"defaults": {"unit": "percent", "min": 0, "max": 100}, "overrides": []}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Filesystem usage",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 9, "w": 12, "x": 0, "y": 8},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "100 * (1 - node_filesystem_avail_bytes{fstype!~\"tmpfs|overlay\"} / node_filesystem_size_bytes{fstype!~\"tmpfs|overlay\"})",
|
||||||
|
"legendFormat": "{{mountpoint}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {"defaults": {"unit": "percent", "min": 0, "max": 100}, "overrides": []},
|
||||||
|
"options": {"legend": {"displayMode": "list", "placement": "bottom"}}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Network throughput",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 9, "w": 12, "x": 12, "y": 8},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "rate(node_network_receive_bytes_total{device!~\"lo|veth.*|br-.*|docker.*\"}[5m])",
|
||||||
|
"legendFormat": "{{device}} RX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"refId": "B",
|
||||||
|
"expr": "rate(node_network_transmit_bytes_total{device!~\"lo|veth.*|br-.*|docker.*\"}[5m])",
|
||||||
|
"legendFormat": "{{device}} TX"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {"defaults": {"unit": "Bps"}, "overrides": []},
|
||||||
|
"options": {"legend": {"displayMode": "list", "placement": "bottom"}}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Disk IO",
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 17},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"expr": "rate(node_disk_read_bytes_total[5m])",
|
||||||
|
"legendFormat": "{{device}} read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"refId": "B",
|
||||||
|
"expr": "rate(node_disk_written_bytes_total[5m])",
|
||||||
|
"legendFormat": "{{device}} write"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fieldConfig": {"defaults": {"unit": "Bps"}, "overrides": []}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: Homelab Imports
|
||||||
|
orgId: 1
|
||||||
|
folder: Homelab
|
||||||
|
type: file
|
||||||
|
disableDeletion: false
|
||||||
|
updateIntervalSeconds: 300
|
||||||
|
allowUiUpdates: true
|
||||||
|
options:
|
||||||
|
# Grafana.com dashboard IDs are imported by the compose one-shot importer.
|
||||||
|
path: /var/lib/grafana/dashboards
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Prometheus
|
||||||
|
type: prometheus
|
||||||
|
access: proxy
|
||||||
|
url: http://prometheus:9090
|
||||||
|
isDefault: true
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
timeInterval: 15s
|
||||||
|
|
||||||
|
- name: Loki
|
||||||
|
type: loki
|
||||||
|
access: proxy
|
||||||
|
url: http://loki:3100
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
maxLines: 1000
|
||||||
|
|
||||||
|
- name: InfluxDB 3 Core
|
||||||
|
uid: monitoring-influxdb3-core
|
||||||
|
type: influxdb
|
||||||
|
access: proxy
|
||||||
|
url: http://influxdb3-core:8181
|
||||||
|
editable: false
|
||||||
|
jsonData:
|
||||||
|
version: SQL
|
||||||
|
dbName: homelab
|
||||||
|
httpMode: POST
|
||||||
|
insecureGrpc: true
|
||||||
|
secureJsonData:
|
||||||
|
token: $GRAFANA_INFLUXDB_TOKEN
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
auth_enabled: false
|
||||||
|
|
||||||
|
server:
|
||||||
|
http_listen_port: 3100
|
||||||
|
grpc_listen_port: 9096
|
||||||
|
|
||||||
|
common:
|
||||||
|
instance_addr: 127.0.0.1
|
||||||
|
path_prefix: /loki
|
||||||
|
storage:
|
||||||
|
filesystem:
|
||||||
|
chunks_directory: /loki/chunks
|
||||||
|
rules_directory: /loki/rules
|
||||||
|
replication_factor: 1
|
||||||
|
ring:
|
||||||
|
kvstore:
|
||||||
|
store: inmemory
|
||||||
|
|
||||||
|
query_range:
|
||||||
|
results_cache:
|
||||||
|
cache:
|
||||||
|
embedded_cache:
|
||||||
|
enabled: true
|
||||||
|
max_size_mb: 100
|
||||||
|
|
||||||
|
schema_config:
|
||||||
|
configs:
|
||||||
|
- from: 2026-05-16
|
||||||
|
store: tsdb
|
||||||
|
object_store: filesystem
|
||||||
|
schema: v13
|
||||||
|
index:
|
||||||
|
prefix: index_
|
||||||
|
period: 24h
|
||||||
|
|
||||||
|
limits_config:
|
||||||
|
retention_period: 720h
|
||||||
|
allow_structured_metadata: true
|
||||||
|
ingestion_rate_mb: 16
|
||||||
|
ingestion_burst_size_mb: 32
|
||||||
|
|
||||||
|
compactor:
|
||||||
|
working_directory: /loki/compactor
|
||||||
|
compaction_interval: 10m
|
||||||
|
retention_enabled: true
|
||||||
|
retention_delete_delay: 2h
|
||||||
|
delete_request_store: filesystem
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
groups:
|
||||||
|
- name: homelab-availability
|
||||||
|
rules:
|
||||||
|
- alert: HomelabExternalConnectivityDown
|
||||||
|
expr: sum(probe_success{job="blackbox-http"} == 0) >= 5
|
||||||
|
for: 8m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "External connectivity appears down"
|
||||||
|
description: "At least 5 public homelab endpoints are unreachable. Likely WAN, DNS, or provider issue."
|
||||||
|
|
||||||
|
- alert: HomelabEndpointDown
|
||||||
|
expr: (probe_success{job="blackbox-http"} == 0) unless on() (sum(probe_success{job="blackbox-http"} == 0) >= 5)
|
||||||
|
for: 8m
|
||||||
|
labels:
|
||||||
|
severity: critical
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }} is not reachable"
|
||||||
|
description: "Blackbox probe failed for {{ $labels.instance }}."
|
||||||
|
|
||||||
|
- alert: HomelabEndpointSlow
|
||||||
|
expr: probe_duration_seconds{job="blackbox-http"} > 5
|
||||||
|
for: 5m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "{{ $labels.instance }} is slow"
|
||||||
|
description: "Blackbox probe duration is above 5 seconds for {{ $labels.instance }}."
|
||||||
|
|
||||||
|
- name: homelab-host
|
||||||
|
rules:
|
||||||
|
- alert: HomelabDiskAlmostFull
|
||||||
|
expr: 100 * (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}) > 85
|
||||||
|
for: 10m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Disk usage high on {{ $labels.mountpoint }}"
|
||||||
|
description: "{{ $labels.mountpoint }} is above 85% used."
|
||||||
|
|
||||||
|
- alert: HomelabHighMemoryUsage
|
||||||
|
expr: 100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 90
|
||||||
|
for: 10m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Memory usage high"
|
||||||
|
description: "Host memory usage is above 90%."
|
||||||
|
|
||||||
|
- alert: HomelabTraefik5xx
|
||||||
|
expr: sum(increase(traefik_service_requests_total{code=~"5.."}[5m])) by (service) >= 5
|
||||||
|
for: 2m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Traefik 5xx responses for {{ $labels.service }}"
|
||||||
|
description: "Traefik reports at least 5 5xx responses for {{ $labels.service }} within 5 minutes."
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
evaluation_interval: 15s
|
||||||
|
external_labels:
|
||||||
|
site: kallilabcore
|
||||||
|
|
||||||
|
rule_files:
|
||||||
|
- /etc/prometheus/alerts.yml
|
||||||
|
|
||||||
|
alerting:
|
||||||
|
alertmanagers:
|
||||||
|
- static_configs:
|
||||||
|
- targets:
|
||||||
|
- alertmanager:9093
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: prometheus
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- prometheus:9090
|
||||||
|
|
||||||
|
- job_name: node-exporter
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- node-exporter:9100
|
||||||
|
|
||||||
|
- job_name: cadvisor
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- cadvisor:8080
|
||||||
|
|
||||||
|
- job_name: traefik
|
||||||
|
metrics_path: /metrics
|
||||||
|
static_configs:
|
||||||
|
# Traefik exposes Prometheus metrics internally on its metrics entrypoint.
|
||||||
|
- targets:
|
||||||
|
- traefik:8082
|
||||||
|
|
||||||
|
- job_name: blackbox-http
|
||||||
|
metrics_path: /probe
|
||||||
|
params:
|
||||||
|
module:
|
||||||
|
- http_reachable
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- https://monitoring.kaleschke.info
|
||||||
|
- https://auth.kaleschke.info
|
||||||
|
- https://git.kaleschke.info
|
||||||
|
- https://komodo.kaleschke.info
|
||||||
|
- https://glance.kaleschke.info
|
||||||
|
- https://paperless.kaleschke.info
|
||||||
|
- https://paperless-gpt.kaleschke.info
|
||||||
|
- https://immich.kaleschke.info
|
||||||
|
- https://mealie.kaleschke.info
|
||||||
|
- https://vault.kaleschke.info
|
||||||
|
- https://cloud.kaleschke.info
|
||||||
|
- https://ntfy.kaleschke.info
|
||||||
|
- https://borg.kaleschke.info
|
||||||
|
- https://files.kaleschke.info
|
||||||
|
- https://code.kaleschke.info
|
||||||
|
- https://glances.kaleschke.info
|
||||||
|
- https://scrutiny.kaleschke.info
|
||||||
|
- https://speedtest.kaleschke.info
|
||||||
|
- https://pdf.kaleschke.info
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels:
|
||||||
|
- __address__
|
||||||
|
target_label: __param_target
|
||||||
|
- source_labels:
|
||||||
|
- __param_target
|
||||||
|
target_label: instance
|
||||||
|
- target_label: __address__
|
||||||
|
replacement: blackbox-exporter:9115
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
server:
|
||||||
|
http_listen_port: 9080
|
||||||
|
grpc_listen_port: 0
|
||||||
|
|
||||||
|
positions:
|
||||||
|
filename: /positions/positions.yml
|
||||||
|
|
||||||
|
clients:
|
||||||
|
- url: http://loki:3100/loki/api/v1/push
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: docker
|
||||||
|
docker_sd_configs:
|
||||||
|
- host: unix:///var/run/docker.sock
|
||||||
|
refresh_interval: 10s
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels:
|
||||||
|
- __meta_docker_container_name
|
||||||
|
regex: /(.+)
|
||||||
|
target_label: container
|
||||||
|
- source_labels:
|
||||||
|
- __meta_docker_container_log_stream
|
||||||
|
target_label: stream
|
||||||
|
- source_labels:
|
||||||
|
- __meta_docker_container_label_com_docker_compose_project
|
||||||
|
target_label: compose_project
|
||||||
|
- source_labels:
|
||||||
|
- __meta_docker_container_label_com_docker_compose_service
|
||||||
|
target_label: compose_service
|
||||||
|
# Docker json-file logs live under /var/lib/docker/containers/<id>/<id>-json.log.
|
||||||
|
- source_labels:
|
||||||
|
- __meta_docker_container_id
|
||||||
|
target_label: __path__
|
||||||
|
replacement: /var/lib/docker/containers/$1/$1-json.log
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
services:
|
|
||||||
backrest:
|
|
||||||
image: ghcr.io/garethgeorge/backrest:latest@sha256:f4d34bd6fa985d13bdb6c01c5d8727e07708899afa9567d800808357d77b9fb0
|
|
||||||
container_name: backrest
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
- TZ=Europe/Berlin
|
|
||||||
- BACKREST_DATA=/data
|
|
||||||
- BACKREST_CONFIG=/config/config.json
|
|
||||||
- XDG_CACHE_HOME=/cache
|
|
||||||
- TMPDIR=/tmp
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- /mnt/user/appdata/backrest/data:/data
|
|
||||||
- /mnt/user/appdata/backrest/config:/config
|
|
||||||
- /mnt/user/appdata/backrest/cache:/cache
|
|
||||||
- /mnt/user/appdata/backrest/tmp:/tmp
|
|
||||||
- /mnt/user/appdata/backrest/ssh:/root/.ssh
|
|
||||||
- /mnt/user/appdata:/source/appdata:ro
|
|
||||||
- /mnt/remotes/192.168.178.86/Public/backrest-repos:/repos/wd
|
|
||||||
- /mnt/user/documents:/source/user/documents:ro
|
|
||||||
- /mnt/user/finance:/source/user/finance:ro
|
|
||||||
- /mnt/user/photos:/source/user/photos:ro
|
|
||||||
- /mnt/user/services:/source/user/services:ro
|
|
||||||
|
|
||||||
dns:
|
|
||||||
- 1.1.1.1
|
|
||||||
- 8.8.8.8
|
|
||||||
|
|
||||||
networks:
|
|
||||||
- backend_net
|
|
||||||
- frontend_net
|
|
||||||
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.docker.network=frontend_net
|
|
||||||
- traefik.http.routers.backrest.rule=Host(`backrest.kaleschke.info`)
|
|
||||||
- traefik.http.routers.backrest.entrypoints=websecure
|
|
||||||
- traefik.http.routers.backrest.tls=true
|
|
||||||
- traefik.http.routers.backrest.tls.certresolver=le
|
|
||||||
- traefik.http.routers.backrest.middlewares=authelia@file,secure-headers@file
|
|
||||||
- traefik.http.services.backrest.loadbalancer.server.port=9898
|
|
||||||
|
|
||||||
networks:
|
|
||||||
backend_net:
|
|
||||||
external: true
|
|
||||||
frontend_net:
|
|
||||||
external: true
|
|
||||||
@@ -57,7 +57,6 @@ Der technische Scope für `critical_infra` ist in `all-important-sources.txt` fe
|
|||||||
- `/local/secrets`
|
- `/local/secrets`
|
||||||
- `/local/appdata/authelia/config`
|
- `/local/appdata/authelia/config`
|
||||||
- `/local/appdata/traefik`
|
- `/local/appdata/traefik`
|
||||||
- `/local/appdata/homepage`
|
|
||||||
- `/local/appdata/ntfy`
|
- `/local/appdata/ntfy`
|
||||||
- `/local/appdata/paperless-gpt`
|
- `/local/appdata/paperless-gpt`
|
||||||
- `/local/appdata/tailscale`
|
- `/local/appdata/tailscale`
|
||||||
@@ -78,7 +77,6 @@ Der technische Scope für `critical_infra` ist in `all-important-sources.txt` fe
|
|||||||
| Mail-archiver | DataProtection-Keys | Shared PostgreSQL Dump vorhanden | Ja | gut |
|
| Mail-archiver | DataProtection-Keys | Shared PostgreSQL Dump vorhanden | Ja | gut |
|
||||||
| Authelia | Config + Secrets | Shared PostgreSQL Dump vorgesehen / erzeugt | Ja | gut |
|
| Authelia | Config + Secrets | Shared PostgreSQL Dump vorgesehen / erzeugt | Ja | gut |
|
||||||
| Traefik | dynamische Config + Let's Encrypt | keine separate DB | Ja | gut |
|
| Traefik | dynamische Config + Let's Encrypt | keine separate DB | Ja | gut |
|
||||||
| Homepage | Config + Bilder | keine separate DB | Ja | gut |
|
|
||||||
| ntfy | Datei-Daten | keine separate DB | Ja | gut |
|
| ntfy | Datei-Daten | keine separate DB | Ja | gut |
|
||||||
| Paperless-GPT | lokale Daten / Prompts | keine separate DB | Ja | gut |
|
| Paperless-GPT | lokale Daten / Prompts | keine separate DB | Ja | gut |
|
||||||
| Tailscale | State-Verzeichnis | keine separate DB | Ja | gut |
|
| Tailscale | State-Verzeichnis | keine separate DB | Ja | gut |
|
||||||
@@ -143,7 +141,7 @@ Seit dem ersten erfolgreichen Lauf wurde der Zielzustand weiter konkret umgesetz
|
|||||||
- Ein Restore-Smoke-Test war erfolgreich:
|
- Ein Restore-Smoke-Test war erfolgreich:
|
||||||
- `postgresql17-globals.sql` wurde wiederhergestellt
|
- `postgresql17-globals.sql` wurde wiederhergestellt
|
||||||
- `gitea.db` wurde wiederhergestellt
|
- `gitea.db` wurde wiederhergestellt
|
||||||
- `ntfy` und Uptime Kuma sind für Borg-Monitoring eingerichtet.
|
- `ntfy` bleibt fuer Borg-Alerts eingerichtet; Uptime Kuma wurde 2026-05-25 durch Blackbox/Prometheus/Grafana abgeloest.
|
||||||
- `Firefly`, `Firefly-Fints` und `Semaphore` wurden aus Git und vom Homelab entfernt.
|
- `Firefly`, `Firefly-Fints` und `Semaphore` wurden aus Git und vom Homelab entfernt.
|
||||||
|
|
||||||
## Was aktuell bewusst nicht als Problem gewertet wird
|
## Was aktuell bewusst nicht als Problem gewertet wird
|
||||||
|
|||||||
+23
-24
@@ -1,6 +1,6 @@
|
|||||||
# Borg Backup Scope for KalliLabcore
|
# Borg Backup Scope for KalliLabcore
|
||||||
|
|
||||||
Stand: 2026-05-04
|
Stand: 2026-05-16
|
||||||
|
|
||||||
This file defines the target state for replacing Backrest with Borg in this homelab.
|
This file defines the target state for replacing Backrest with Borg in this homelab.
|
||||||
|
|
||||||
@@ -11,66 +11,62 @@ Use Borg as the single backup system for:
|
|||||||
- critical file-backed application data
|
- critical file-backed application data
|
||||||
- secrets, keys, and reverse-proxy state
|
- secrets, keys, and reverse-proxy state
|
||||||
- database dumps generated before each Borg backup
|
- database dumps generated before each Borg backup
|
||||||
|
- Unraid flash configuration artifacts generated before each Borg backup
|
||||||
|
|
||||||
Do not back up raw live database storage directories as the primary recovery artifact.
|
Do not back up raw live database storage directories as the primary recovery artifact.
|
||||||
|
|
||||||
## Strategy
|
## Strategy
|
||||||
|
|
||||||
1. A pre-backup dump script runs on the host and writes fresh dumps to `/mnt/user/backups/borg/dumps/latest`.
|
1. A pre-backup dump script runs on the host and writes fresh dumps plus `unraid-flash-config.tar.gz` to `/mnt/user/backups/borg/dumps/latest`.
|
||||||
2. Borg backs up `/local/borg-dumps` plus the critical mounted paths below.
|
2. Borg backs up `/local/borg-dumps` plus the critical mounted paths below.
|
||||||
3. Borg retention handles history; the dump directory itself keeps only the latest artifacts.
|
3. Borg retention handles history; the dump directory itself keeps only the latest artifacts.
|
||||||
|
|
||||||
The inclusion of `/local/secrets` is intentional: Borg is expected to cover disaster recovery for selected secret material as part of the current homelab restore strategy.
|
The inclusion of `/local/secrets` is intentional: Borg is expected to cover disaster recovery for selected secret material as part of the current homelab restore strategy.
|
||||||
|
The Unraid flash configuration archive is intentional as well and must be treated as secret backup material.
|
||||||
|
|
||||||
## Service Inventory
|
## Service Inventory
|
||||||
|
|
||||||
| Service | Recovery Method | What Borg Should Capture |
|
| Service | Recovery Method | What Borg Should Capture |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Vaultwarden | file data | `/local/appdata/vaultwarden` |
|
| Vaultwarden | SQLite dump + file data | `/local/borg-dumps`, `/local/appdata/vaultwarden` |
|
||||||
| Paperless | DB dump + file data | `/local/borg-dumps`, `/local/appdata/paperless-ngx/data`, `/local/paperless/media`, `/local/paperless/export`, `/local/paperless/consume` |
|
| Paperless | DB dump + file data | `/local/borg-dumps`, `/local/appdata/paperless-ngx/data`, `/local/paperless/media`, `/local/paperless/export`, `/local/paperless/consume` |
|
||||||
| Immich | DB dump + file data | `/local/borg-dumps`, `/local/immich/upload`, `/local/immich/external` |
|
| Immich | DB dump + file data | `/local/borg-dumps`, `/local/immich/upload`, `/local/immich/external` |
|
||||||
| Gitea | file data (SQLite inside `/data`) | `/local/gitea/data` |
|
| Gitea | SQLite dump + file data | `/local/borg-dumps`, `/local/gitea/data` |
|
||||||
| Mealie | DB dump + file data | `/local/borg-dumps`, `/local/appdata/mealie/data` |
|
| Mealie | DB dump + file data | `/local/borg-dumps`, `/local/appdata/mealie/data` |
|
||||||
| Mail-archiver | shared Postgres dump + data protection keys | `/local/borg-dumps`, `/local/appdata/mailarchiver/data-protection-keys` |
|
| Mail-archiver | shared Postgres dump + data protection keys | `/local/borg-dumps`, `/local/appdata/mailarchiver/data-protection-keys` |
|
||||||
| Authelia | shared Postgres dump + config + secrets | `/local/borg-dumps`, `/local/appdata/authelia/config`, `/local/secrets` |
|
| Authelia | shared Postgres dump + config + secrets | `/local/borg-dumps`, `/local/appdata/authelia/config`, `/local/secrets` |
|
||||||
| Traefik | file data | `/local/appdata/traefik` |
|
| Traefik | file data | `/local/appdata/traefik` |
|
||||||
| Homepage | file data | `/local/appdata/homepage` |
|
|
||||||
| ntfy | file data | `/local/appdata/ntfy` |
|
| ntfy | file data | `/local/appdata/ntfy` |
|
||||||
| Paperless-GPT | file data | `/local/appdata/paperless-gpt` |
|
| Paperless-GPT | file data | `/local/appdata/paperless-gpt` |
|
||||||
| Tailscale | file data | `/local/appdata/tailscale` |
|
| Tailscale | file data | `/local/appdata/tailscale` |
|
||||||
| AdGuard | config only | `/local/appdata/adguard/conf` |
|
| AdGuard | config only | `/local/appdata/adguard/conf` |
|
||||||
| Borg UI | self-backup | `/local/appdata/borg-ui/data` |
|
| Borg UI | SQLite dump + self-backup | `/local/borg-dumps`, `/local/appdata/borg-ui/data` |
|
||||||
| Komodo | config + Mongo dump | `/local/borg-dumps`, `/local/appdata/komodo/periphery`, `/local/appdata/komodo/core` |
|
| Komodo | config + Mongo dump | `/local/borg-dumps`, `/local/appdata/komodo/periphery`, `/local/appdata/komodo/core` |
|
||||||
| Nextcloud | raw DB path + file data | `/local/appdata/nextcloud/html`, `/local/appdata/nextcloud/postgres`, `/local/appdata/nextcloud/redis`; user data path see gap below |
|
| GitOps host automation | repo clone + Komodo workspaces + host-check state | `/local/services/homelab-infra`, `/local/services/stacks`, `/local/services/posture-check` |
|
||||||
| Grafana | file data | `/local/appdata/grafana` |
|
| Unraid OS flash | generated config archive | `/local/borg-dumps/unraid-flash-config.tar.gz` plus checksum and manifest |
|
||||||
|
| Nextcloud | DB dump + file data | `/local/borg-dumps`, `/local/appdata/nextcloud/html`, `/local/nextcloud/data` |
|
||||||
|
| Grafana | SQLite dump + file data | `/local/borg-dumps`, `/local/appdata/grafana` |
|
||||||
|
| Filebrowser | file-backed state dump + file data | `/local/borg-dumps`, `/local/appdata/filebrowser` |
|
||||||
| InfluxDB 3 Core | file data | `/local/appdata/influxdb3/data`, `/local/appdata/influxdb3/plugins` |
|
| InfluxDB 3 Core | file data | `/local/appdata/influxdb3/data`, `/local/appdata/influxdb3/plugins` |
|
||||||
| Hermes Agent | file data + SSH key | `/local/appdata/hermes-agent/data`, `/local/secrets/hermes_runner_id_ed25519` |
|
| Hermes Agent | file data + SSH key | `/local/appdata/hermes-agent/data`, `/local/secrets/hermes_runner_id_ed25519` |
|
||||||
| Backrest | file data | `/local/appdata/backrest/data`, `/local/appdata/backrest/config` |
|
|
||||||
| BentoPDF | rebuildable | no critical persistence in compose |
|
| BentoPDF | rebuildable | no critical persistence in compose |
|
||||||
|
|
||||||
## Open Decisions and Coverage Gaps
|
## Open Decisions and Coverage Gaps
|
||||||
|
|
||||||
These are deviations from the standard "DB dump first, file path second" strategy. Decide deliberately, do not silently extend.
|
These are deviations from the standard "DB dump first, file path second" strategy. Decide deliberately, do not silently extend.
|
||||||
|
|
||||||
### Nextcloud database
|
### Nextcloud
|
||||||
|
|
||||||
Recovery currently relies on the raw live DB path `/local/appdata/nextcloud/postgres`. This is inconsistent with the policy "Do not back up raw live database storage directories as the primary recovery artifact" stated below.
|
Option A umgesetzt: `pre-backup-dumps.sh` writes `nextcloud.dump` from `nextcloud-postgres`. Borg UI also mounts `/mnt/user/documents/nextcloud-data` read-only as `/local/nextcloud/data`, so database and user files are both inside scope after the Borg UI stack is recreated.
|
||||||
|
|
||||||
Open decision:
|
|
||||||
|
|
||||||
- Option A: extend `ops/borg-ui/scripts/pre-backup-dumps.sh` with a `nextcloud-postgres` dump and treat the raw path as transient.
|
|
||||||
- Option B: accept the raw path as a documented Nextcloud-specific exception.
|
|
||||||
|
|
||||||
Until decided, the raw path is what Borg sees today and is the only Nextcloud DB recovery surface.
|
|
||||||
|
|
||||||
### Nextcloud user data path is outside the borg-ui mount set
|
|
||||||
|
|
||||||
`/mnt/user/documents/nextcloud-data` is not mounted into `borg-ui` in `ops/borg-ui/docker-compose.yml`. Nextcloud user files are therefore not in the current Borg scope. Resolution requires a separate Compose change (add a read-only mount) and is not silently fixed in this scope document.
|
|
||||||
|
|
||||||
### Komodo Mongo dump
|
### Komodo Mongo dump
|
||||||
|
|
||||||
`komodo-mongo.archive.gz` was produced and verified on 2026-05-04 (`gzip -t` ok). The dump function is in place in `pre-backup-dumps.sh`. Re-verify after any Komodo or Mongo major upgrade.
|
`komodo-mongo.archive.gz` was produced and verified on 2026-05-04 (`gzip -t` ok). The dump function is in place in `pre-backup-dumps.sh`. Re-verify after any Komodo or Mongo major upgrade.
|
||||||
|
|
||||||
|
### GitOps host automation
|
||||||
|
|
||||||
|
The live Unraid User Scripts execute repo scripts from `/mnt/user/services/homelab-infra`, while Komodo keeps stack workspaces below `/mnt/user/services/stacks`. These paths are now mounted into Borg UI as `/local/services/...` and included explicitly so host-side script hotfixes, stack workspace state, and posture-check state are recoverable.
|
||||||
|
|
||||||
## Database Dumps Required
|
## Database Dumps Required
|
||||||
|
|
||||||
### Shared PostgreSQL (`postgresql17`)
|
### Shared PostgreSQL (`postgresql17`)
|
||||||
@@ -83,16 +79,21 @@ Until decided, the raw path is what Borg sees today and is the only Nextcloud DB
|
|||||||
|
|
||||||
- `mealie`
|
- `mealie`
|
||||||
- `immich`
|
- `immich`
|
||||||
|
- `nextcloud`
|
||||||
|
|
||||||
### Other Databases
|
### Other Databases
|
||||||
|
|
||||||
- Komodo MongoDB
|
- Komodo MongoDB
|
||||||
|
- SQLite: `gitea`, `vaultwarden`, `speedtest-tracker`, `borg-ui`, `grafana`
|
||||||
|
- File-backed state: `filebrowser.bolt.dump`
|
||||||
|
- Unraid flash config: `unraid-flash-config.tar.gz` plus `unraid-flash-config.tar.gz.sha256`
|
||||||
|
|
||||||
## Explicitly Not Backed Up as Raw Live DB Files
|
## Explicitly Not Backed Up as Raw Live DB Files
|
||||||
|
|
||||||
- `/mnt/user/appdata/postgresql17`
|
- `/mnt/user/appdata/postgresql17`
|
||||||
- `/mnt/user/appdata/mealie/postgres`
|
- `/mnt/user/appdata/mealie/postgres`
|
||||||
- `/mnt/user/appdata/immich_postgres`
|
- `/mnt/user/appdata/immich_postgres`
|
||||||
|
- `/mnt/user/appdata/nextcloud/postgres`
|
||||||
- `/mnt/user/appdata/komodo/mongo`
|
- `/mnt/user/appdata/komodo/mongo`
|
||||||
- `/mnt/user/appdata/redis`
|
- `/mnt/user/appdata/redis`
|
||||||
- `/mnt/user/appdata/scrutiny/influxdb`
|
- `/mnt/user/appdata/scrutiny/influxdb`
|
||||||
@@ -104,10 +105,8 @@ These are not part of the first-class Borg scope:
|
|||||||
- Plex metadata and cache
|
- Plex metadata and cache
|
||||||
- AdGuard query log
|
- AdGuard query log
|
||||||
- code-server extensions cache
|
- code-server extensions cache
|
||||||
- uptime-kuma
|
|
||||||
- scrutiny metrics history
|
- scrutiny metrics history
|
||||||
- dozzle, glances, speedtest
|
- dozzle, glances, speedtest
|
||||||
- filebrowser app state
|
|
||||||
|
|
||||||
## Suggested Retention
|
## Suggested Retention
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
/local/secrets
|
/local/secrets
|
||||||
/local/appdata/authelia/config
|
/local/appdata/authelia/config
|
||||||
/local/appdata/traefik
|
/local/appdata/traefik
|
||||||
/local/appdata/homepage
|
|
||||||
/local/appdata/ntfy
|
/local/appdata/ntfy
|
||||||
/local/appdata/paperless-gpt
|
/local/appdata/paperless-gpt
|
||||||
/local/appdata/tailscale
|
/local/appdata/tailscale
|
||||||
@@ -20,3 +19,6 @@
|
|||||||
/local/appdata/borg-ui/data
|
/local/appdata/borg-ui/data
|
||||||
/local/appdata/komodo/periphery
|
/local/appdata/komodo/periphery
|
||||||
/local/appdata/komodo/core
|
/local/appdata/komodo/core
|
||||||
|
/local/services/homelab-infra
|
||||||
|
/local/services/stacks
|
||||||
|
/local/services/posture-check
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ services:
|
|||||||
- /mnt/user/documents/scans_inbox:/local/paperless/consume:ro
|
- /mnt/user/documents/scans_inbox:/local/paperless/consume:ro
|
||||||
- /mnt/user/documents/paperless:/local/paperless/media:ro
|
- /mnt/user/documents/paperless:/local/paperless/media:ro
|
||||||
- /mnt/user/documents/paperless/export:/local/paperless/export:ro
|
- /mnt/user/documents/paperless/export:/local/paperless/export:ro
|
||||||
|
- /mnt/user/documents/nextcloud-data:/local/nextcloud/data:ro
|
||||||
- /mnt/user/photos/immich:/local/immich/upload:ro
|
- /mnt/user/photos/immich:/local/immich/upload:ro
|
||||||
- /mnt/user/photos/family_archive:/local/immich/external:ro
|
- /mnt/user/photos/family_archive:/local/immich/external:ro
|
||||||
|
- /mnt/user/services:/local/services:ro
|
||||||
- /mnt/user/services/gitea/data:/local/gitea/data:ro
|
- /mnt/user/services/gitea/data:/local/gitea/data:ro
|
||||||
- /mnt/user/appdata/borg-ui/restore:/restore
|
- /mnt/user/appdata/borg-ui/restore:/restore
|
||||||
dns:
|
dns:
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ Fresh dump artifacts are written to:
|
|||||||
|
|
||||||
Borg UI should include `/local/borg-dumps` as a backup source.
|
Borg UI should include `/local/borg-dumps` as a backup source.
|
||||||
|
|
||||||
|
The dump set also includes `unraid-flash-config.tar.gz`, a host-generated
|
||||||
|
archive of `/boot/config` plus checksum and manifest. Treat this archive as
|
||||||
|
secret backup material.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- The script is written for host execution where `docker` is available.
|
- The script is written for host execution where `docker` is available.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Unraid User Scripts Setup
|
# Unraid User Scripts Setup
|
||||||
|
|
||||||
This document describes the intended automation path for `pre-backup-dumps.sh`.
|
This document describes the intended automation path for the Borg pre-flight scripts.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
The pre-backup dump refresh should run:
|
The Borg pre-flight should run:
|
||||||
|
|
||||||
- on the Unraid host
|
- on the Unraid host
|
||||||
- through the User Scripts plugin or host cron
|
- through the User Scripts plugin or host cron
|
||||||
@@ -14,7 +14,13 @@ It should **not** be implemented as a Borg UI inline hook in the current design.
|
|||||||
|
|
||||||
## Why host-side
|
## Why host-side
|
||||||
|
|
||||||
`pre-backup-dumps.sh` currently assumes:
|
`pre-borg.sh` currently chains the host-side checks:
|
||||||
|
|
||||||
|
- `services/posture-check/posture-check.sh`
|
||||||
|
- `ops/borg-ui/scripts/pre-backup-dumps.sh` including the Unraid flash config archive
|
||||||
|
- `ops/restore-tests/check-restore-freshness.sh`
|
||||||
|
|
||||||
|
The dump step assumes:
|
||||||
|
|
||||||
- access to the host Docker daemon via `docker exec`
|
- access to the host Docker daemon via `docker exec`
|
||||||
- access to host paths under `/mnt/user/...`
|
- access to host paths under `/mnt/user/...`
|
||||||
@@ -24,24 +30,36 @@ That makes host execution simpler, more transparent, and lower-risk than giving
|
|||||||
|
|
||||||
## Recommended rollout
|
## Recommended rollout
|
||||||
|
|
||||||
1. Store the script on the host, for example at:
|
1. Use the repo clone on the host:
|
||||||
- `/mnt/user/appdata/borg-ui/scripts/pre-backup-dumps.sh`
|
- `/mnt/user/services/homelab-infra`
|
||||||
2. Make it executable:
|
2. Make the scripts executable:
|
||||||
- `chmod +x /mnt/user/appdata/borg-ui/scripts/pre-backup-dumps.sh`
|
- `chmod +x /mnt/user/services/homelab-infra/ops/borg-ui/scripts/pre-borg.sh`
|
||||||
3. Create a User Scripts entry such as:
|
- `chmod +x /mnt/user/services/homelab-infra/ops/borg-ui/scripts/pre-backup-dumps.sh`
|
||||||
|
3. Create a User Scripts entry:
|
||||||
- `borg-pre-backup-dumps`
|
- `borg-pre-backup-dumps`
|
||||||
4. Let that entry run:
|
4. Script body:
|
||||||
- on a fixed schedule before the expected Borg backup window
|
|
||||||
- or manually before ad hoc Borg runs
|
```bash
|
||||||
5. Keep Borg UI focused on backing up `/local/borg-dumps`, not on generating the dumps itself.
|
#!/bin/bash
|
||||||
|
REPO_ROOT=/mnt/user/services/homelab-infra \
|
||||||
|
DUMP_ROOT=/mnt/user/backups/borg/dumps \
|
||||||
|
ALLOW_POSTURE_WARNING=1 \
|
||||||
|
bash /mnt/user/services/homelab-infra/ops/borg-ui/scripts/pre-borg.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Schedule: daily at `04:00`, before the expected Borg backup window.
|
||||||
|
6. Keep Borg UI focused on backing up `/local/borg-dumps`, not on generating the dumps itself.
|
||||||
|
|
||||||
## Operational model
|
## Operational model
|
||||||
|
|
||||||
The intended sequence is:
|
The intended sequence is:
|
||||||
|
|
||||||
1. Host script refreshes `latest` dump artifacts.
|
1. Host wrapper checks posture.
|
||||||
2. Borg UI backs up `/local/borg-dumps` together with the rest of `critical_infra`.
|
2. Host script refreshes `latest` dump artifacts.
|
||||||
3. Borg history preserves dump history, so the host only needs to keep the most recent dump set.
|
3. Host script writes `unraid-flash-config.tar.gz` plus checksum and manifest into the same dump set.
|
||||||
|
4. Freshness check verifies expected dumps and the flash config archive.
|
||||||
|
5. Borg UI backs up `/local/borg-dumps` together with the rest of `critical_infra`.
|
||||||
|
6. Borg history preserves dump history, so the host only needs to keep the most recent dump set.
|
||||||
|
|
||||||
## Current dump target
|
## Current dump target
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
+163
@@ -68,6 +68,143 @@ dump_pg_globals() {
|
|||||||
atomic_write "$output" "$tmp"
|
atomic_write "$output" "$tmp"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dump_sqlite_file() {
|
||||||
|
source="$1"
|
||||||
|
output="$2"
|
||||||
|
label="$3"
|
||||||
|
|
||||||
|
if [ ! -f "$source" ]; then
|
||||||
|
warn "Skipping missing SQLite database for $label: $source"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp="$TMP_DIR/$(basename "$output").tmp"
|
||||||
|
log "Dumping SQLite database '$label' from $source"
|
||||||
|
rm -f "$tmp"
|
||||||
|
if ! sqlite3 "$source" ".backup $tmp"; then
|
||||||
|
warn "SQLite backup failed for $label"
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ "$(sqlite3 "$tmp" 'PRAGMA quick_check;')" != "ok" ]; then
|
||||||
|
warn "SQLite quick_check failed for $label"
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
atomic_write "$output" "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
dump_sqlite_container() {
|
||||||
|
container="$1"
|
||||||
|
db_path="$2"
|
||||||
|
output="$3"
|
||||||
|
host_source="${4:-}"
|
||||||
|
|
||||||
|
if ! need_container "$container"; then
|
||||||
|
warn "Skipping missing container: $container"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker exec "$container" sh -lc 'command -v sqlite3 >/dev/null 2>&1'; then
|
||||||
|
if [ -n "$host_source" ]; then
|
||||||
|
warn "Container $container has no sqlite3; using host-side SQLite backup for $host_source"
|
||||||
|
dump_sqlite_file "$host_source" "$output" "$container"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
warn "Skipping SQLite backup for $container because sqlite3 is missing in the container and no host fallback is configured"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
container_tmp="/tmp/$(basename "$output").bak"
|
||||||
|
tmp="$TMP_DIR/$(basename "$output").tmp"
|
||||||
|
|
||||||
|
log "Dumping SQLite database '$db_path' from $container"
|
||||||
|
rm -f "$tmp"
|
||||||
|
docker exec "$container" rm -f "$container_tmp" >/dev/null 2>&1 || true
|
||||||
|
if ! docker exec "$container" sqlite3 "$db_path" ".backup $container_tmp"; then
|
||||||
|
warn "SQLite backup failed for $container:$db_path"
|
||||||
|
docker exec "$container" rm -f "$container_tmp" >/dev/null 2>&1 || true
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
docker cp "$container:$container_tmp" "$tmp"
|
||||||
|
docker exec "$container" rm -f "$container_tmp" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
if [ "$(sqlite3 "$tmp" 'PRAGMA quick_check;')" != "ok" ]; then
|
||||||
|
warn "SQLite quick_check failed for $container:$db_path"
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
atomic_write "$output" "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
dump_file_copy() {
|
||||||
|
source="$1"
|
||||||
|
output="$2"
|
||||||
|
label="$3"
|
||||||
|
|
||||||
|
if [ ! -f "$source" ]; then
|
||||||
|
warn "Skipping missing file dump for $label: $source"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp="$TMP_DIR/$(basename "$output").tmp"
|
||||||
|
log "Copying file-backed state '$label' from $source"
|
||||||
|
rm -f "$tmp"
|
||||||
|
cp "$source" "$tmp"
|
||||||
|
atomic_write "$output" "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_unraid_flash_config() {
|
||||||
|
output="$LATEST_DIR/unraid-flash-config.tar.gz"
|
||||||
|
checksum="$LATEST_DIR/unraid-flash-config.tar.gz.sha256"
|
||||||
|
manifest="$LATEST_DIR/unraid-flash-config.manifest.txt"
|
||||||
|
tmp="$TMP_DIR/unraid-flash-config.tar.gz.tmp"
|
||||||
|
tmp_checksum="$TMP_DIR/unraid-flash-config.tar.gz.sha256.tmp"
|
||||||
|
tmp_manifest="$TMP_DIR/unraid-flash-config.manifest.txt.tmp"
|
||||||
|
|
||||||
|
if [ ! -d /boot/config ]; then
|
||||||
|
warn "Skipping Unraid flash config backup because /boot/config is missing"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Backing up Unraid flash configuration from /boot/config"
|
||||||
|
rm -f "$tmp" "$tmp_checksum" "$tmp_manifest"
|
||||||
|
|
||||||
|
tar -C /boot \
|
||||||
|
--exclude='config/plugins/*/*.txz' \
|
||||||
|
--exclude='config/plugins/*/*.tgz' \
|
||||||
|
--exclude='config/plugins/*/*.tar' \
|
||||||
|
--exclude='config/plugins/*/*.tar.*' \
|
||||||
|
--exclude='config/plugins/*/*.zip' \
|
||||||
|
--exclude='config/plugins/*/*.md5' \
|
||||||
|
-czf "$tmp" config
|
||||||
|
chmod 600 "$tmp"
|
||||||
|
atomic_write "$output" "$tmp"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "$LATEST_DIR"
|
||||||
|
sha256sum "$(basename "$output")"
|
||||||
|
) > "$tmp_checksum"
|
||||||
|
chmod 600 "$tmp_checksum"
|
||||||
|
atomic_write "$checksum" "$tmp_checksum"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf 'created_utc=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
printf 'host=%s\n' "$(hostname)"
|
||||||
|
if [ -f /etc/unraid-version ]; then
|
||||||
|
sed 's/^/unraid_/' /etc/unraid-version
|
||||||
|
fi
|
||||||
|
printf 'source=/boot/config\n'
|
||||||
|
printf 'archive=%s\n' "$(basename "$output")"
|
||||||
|
printf 'checksum=%s\n' "$(basename "$checksum")"
|
||||||
|
printf 'note=%s\n' 'Contains Unraid configuration and must be treated as secret backup material.'
|
||||||
|
printf 'excluded=%s\n' 'downloadable plugin package archives under /boot/config/plugins/*/'
|
||||||
|
} > "$tmp_manifest"
|
||||||
|
chmod 600 "$tmp_manifest"
|
||||||
|
atomic_write "$manifest" "$tmp_manifest"
|
||||||
|
}
|
||||||
|
|
||||||
dump_optional_pg_db() {
|
dump_optional_pg_db() {
|
||||||
container="$1"
|
container="$1"
|
||||||
password="$2"
|
password="$2"
|
||||||
@@ -131,6 +268,9 @@ dump_mongo_container() {
|
|||||||
|
|
||||||
main() {
|
main() {
|
||||||
need_cmd docker
|
need_cmd docker
|
||||||
|
need_cmd sqlite3
|
||||||
|
need_cmd tar
|
||||||
|
need_cmd sha256sum
|
||||||
ensure_dirs
|
ensure_dirs
|
||||||
|
|
||||||
# Shared PostgreSQL 17
|
# Shared PostgreSQL 17
|
||||||
@@ -162,9 +302,32 @@ main() {
|
|||||||
warn "Skipping missing container: immich_postgres"
|
warn "Skipping missing container: immich_postgres"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if need_container "nextcloud-postgres"; then
|
||||||
|
nextcloud_password="$(cat /mnt/user/appdata/secrets/nextcloud_postgres_password.txt)"
|
||||||
|
dump_pg_db "nextcloud-postgres" "$nextcloud_password" "nextcloud" "nextcloud" "$LATEST_DIR/nextcloud.dump"
|
||||||
|
else
|
||||||
|
warn "Skipping missing container: nextcloud-postgres"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SQLite databases
|
||||||
|
dump_sqlite_container "gitea" "/data/gitea/gitea.db" "$LATEST_DIR/gitea.sqlite.dump" "/mnt/user/services/gitea/data/gitea/gitea.db"
|
||||||
|
dump_sqlite_container "vaultwarden" "/data/db.sqlite3" "$LATEST_DIR/vaultwarden.sqlite.dump" "/mnt/user/appdata/vaultwarden/db.sqlite3"
|
||||||
|
dump_sqlite_container "speedtest-tracker" "/config/database.sqlite" "$LATEST_DIR/speedtest-tracker.sqlite.dump" "/mnt/user/appdata/speedtest-tracker/config/database.sqlite"
|
||||||
|
|
||||||
|
# Filebrowser uses file-backed app state, but this installation is not SQLite.
|
||||||
|
dump_file_copy "/mnt/user/appdata/filebrowser/database/filebrowser.db" "$LATEST_DIR/filebrowser.bolt.dump" "filebrowser"
|
||||||
|
|
||||||
|
# Additional host-side SQLite dumps for admin tooling with appdata files.
|
||||||
|
dump_sqlite_file "/mnt/user/appdata/borg-ui/data/borg.db" "$LATEST_DIR/borg-ui.sqlite" "borg-ui"
|
||||||
|
dump_sqlite_file "/mnt/user/appdata/grafana/grafana.db" "$LATEST_DIR/grafana.sqlite" "grafana"
|
||||||
|
|
||||||
# MongoDB
|
# MongoDB
|
||||||
dump_mongo_container "komodo-mongo" "$LATEST_DIR/komodo-mongo.archive.gz"
|
dump_mongo_container "komodo-mongo" "$LATEST_DIR/komodo-mongo.archive.gz"
|
||||||
|
|
||||||
|
# Unraid USB flash configuration. This is generated into the existing dump
|
||||||
|
# set so Borg carries it off-site together with the database artifacts.
|
||||||
|
backup_unraid_flash_config
|
||||||
|
|
||||||
log "Finished refreshing dump set in $LATEST_DIR"
|
log "Finished refreshing dump set in $LATEST_DIR"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="${REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}"
|
||||||
|
POSTURE_CHECK="${POSTURE_CHECK:-$REPO_ROOT/services/posture-check/posture-check.sh}"
|
||||||
|
FRESHNESS_CHECK="${FRESHNESS_CHECK:-$REPO_ROOT/ops/restore-tests/check-restore-freshness.sh}"
|
||||||
|
PRE_BACKUP_DUMPS="${PRE_BACKUP_DUMPS:-$SCRIPT_DIR/pre-backup-dumps.sh}"
|
||||||
|
NTFY_SCRIPT="${NTFY_SCRIPT:-$REPO_ROOT/ops/restore-tests/send-ntfy.sh}"
|
||||||
|
NTFY_TOPIC="${NTFY_TOPIC:-homelab-alerts}"
|
||||||
|
ALLOW_POSTURE_WARNING="${ALLOW_POSTURE_WARNING:-1}"
|
||||||
|
|
||||||
|
case "${DUMP_ROOT:-}" in
|
||||||
|
*/latest)
|
||||||
|
FRESHNESS_DUMP_ROOT="${FRESHNESS_DUMP_ROOT:-$DUMP_ROOT}"
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
FRESHNESS_DUMP_ROOT="${FRESHNESS_DUMP_ROOT:-/mnt/user/backups/borg/dumps/latest}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
FRESHNESS_DUMP_ROOT="${FRESHNESS_DUMP_ROOT:-$DUMP_ROOT/latest}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
notify_failure() {
|
||||||
|
local step="$1"
|
||||||
|
local message="$2"
|
||||||
|
if [ -x "$NTFY_SCRIPT" ]; then
|
||||||
|
"$NTFY_SCRIPT" "$NTFY_TOPIC" "Borg pre-hook failed: $step" "$message" high || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_step() {
|
||||||
|
local step="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
echo "[pre-borg] Running $step"
|
||||||
|
if "$@"; then
|
||||||
|
echo "[pre-borg] OK: $step"
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
notify_failure "$step" "Command failed with exit code $rc: $*"
|
||||||
|
exit "$rc"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "[pre-borg] Running posture-check"
|
||||||
|
if "$POSTURE_CHECK"; then
|
||||||
|
echo "[pre-borg] OK: posture-check"
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
if [ "$rc" -eq 1 ] && [ "$ALLOW_POSTURE_WARNING" = "1" ]; then
|
||||||
|
echo "[pre-borg] WARNING: posture-check returned warnings; continuing because ALLOW_POSTURE_WARNING=1"
|
||||||
|
else
|
||||||
|
notify_failure "posture-check" "Command failed with exit code $rc: $POSTURE_CHECK"
|
||||||
|
exit "$rc"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
run_step "pre-backup-dumps" "$PRE_BACKUP_DUMPS"
|
||||||
|
run_step "restore-freshness" env DUMP_ROOT="$FRESHNESS_DUMP_ROOT" "$FRESHNESS_CHECK"
|
||||||
|
|
||||||
|
echo "[pre-borg] All pre-flight checks passed"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
code-server:
|
code-server:
|
||||||
image: lscr.io/linuxserver/code-server:latest@sha256:4620adace18935dd6ca79d77e3bc1c379e21875392192f970cf5d6b0fb4aefcd
|
image: lscr.io/linuxserver/code-server:4.116.0@sha256:4620adace18935dd6ca79d77e3bc1c379e21875392192f970cf5d6b0fb4aefcd
|
||||||
container_name: code-server
|
container_name: code-server
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
@@ -10,7 +10,7 @@ services:
|
|||||||
- PUID=1000
|
- PUID=1000
|
||||||
- PGID=1000
|
- PGID=1000
|
||||||
- TZ=Europe/Berlin
|
- TZ=Europe/Berlin
|
||||||
- PASSWORD_FILE=/run/secrets/password
|
- FILE__PASSWORD=/run/secrets/password
|
||||||
- DEFAULT_WORKSPACE=/workspace
|
- DEFAULT_WORKSPACE=/workspace
|
||||||
- PWA_APPNAME=KalliLab Code
|
- PWA_APPNAME=KalliLab Code
|
||||||
|
|
||||||
@@ -18,7 +18,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /mnt/user/appdata/code-server:/config
|
- /mnt/user/appdata/code-server:/config
|
||||||
- /mnt/user/services/dev:/workspace
|
- /mnt/user/services/dev:/workspace
|
||||||
- /mnt/user/appdata/homepage:/prod/homepage
|
|
||||||
- /mnt/user/appdata/code-server/secrets/password:/run/secrets/password:ro
|
- /mnt/user/appdata/code-server/secrets/password:/run/secrets/password:ro
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
filebrowser:
|
filebrowser:
|
||||||
image: filebrowser/filebrowser:latest@sha256:4dce87308b9f9cfbcf8d0a284fc9565d2b515530a6bae2d920b388161e093f26
|
image: filebrowser/filebrowser:v2.63.2@sha256:4dce87308b9f9cfbcf8d0a284fc9565d2b515530a6bae2d920b388161e093f26
|
||||||
container_name: filebrowser
|
container_name: filebrowser
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
@@ -9,7 +9,9 @@ services:
|
|||||||
- PUID=99
|
- PUID=99
|
||||||
- PGID=100
|
- PGID=100
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/user/appdata:/srv/appdata
|
- /mnt/user/documents:/srv/documents
|
||||||
|
- /mnt/user/photos:/srv/photos
|
||||||
|
- /mnt/user/projekte:/srv/projekte
|
||||||
- /mnt/user/appdata/filebrowser/database:/database
|
- /mnt/user/appdata/filebrowser/database:/database
|
||||||
- /mnt/user/appdata/filebrowser/config:/config
|
- /mnt/user/appdata/filebrowser/config:/config
|
||||||
command: ["--database", "/database/filebrowser.db"]
|
command: ["--database", "/database/filebrowser.db"]
|
||||||
|
|||||||
@@ -0,0 +1,879 @@
|
|||||||
|
server:
|
||||||
|
proxied: true
|
||||||
|
|
||||||
|
branding:
|
||||||
|
app-name: KalliLab Dashboard
|
||||||
|
logo-text: KL
|
||||||
|
hide-footer: true
|
||||||
|
|
||||||
|
theme:
|
||||||
|
background-color: 210 20 13
|
||||||
|
primary-color: 212 100 50
|
||||||
|
positive-color: 140 70 40
|
||||||
|
negative-color: 4 78 57
|
||||||
|
contrast-multiplier: 1.25
|
||||||
|
text-saturation-multiplier: 0.9
|
||||||
|
disable-picker: false
|
||||||
|
|
||||||
|
pages:
|
||||||
|
- name: Home
|
||||||
|
slug: home
|
||||||
|
width: wide
|
||||||
|
head-widgets:
|
||||||
|
- type: search
|
||||||
|
search-engine: duckduckgo
|
||||||
|
new-tab: true
|
||||||
|
autofocus: true
|
||||||
|
placeholder: Suche im Web oder springe per Bang...
|
||||||
|
bangs:
|
||||||
|
- title: Gitea
|
||||||
|
shortcut: "!git"
|
||||||
|
url: https://git.kaleschke.info/explore/repos?q={QUERY}
|
||||||
|
- title: Paperless
|
||||||
|
shortcut: "!doc"
|
||||||
|
url: https://paperless.kaleschke.info/documents?query={QUERY}
|
||||||
|
- title: Nextcloud
|
||||||
|
shortcut: "!cloud"
|
||||||
|
url: https://cloud.kaleschke.info/apps/files/?dir=/{QUERY}
|
||||||
|
- title: Komodo
|
||||||
|
shortcut: "!komodo"
|
||||||
|
url: https://komodo.kaleschke.info
|
||||||
|
columns:
|
||||||
|
- size: small
|
||||||
|
widgets:
|
||||||
|
- type: group
|
||||||
|
widgets:
|
||||||
|
- type: custom-api
|
||||||
|
title: Day
|
||||||
|
body-type: string
|
||||||
|
skip-json-validation: true
|
||||||
|
cache: 1s
|
||||||
|
template: |
|
||||||
|
{{ $localTime := now }}
|
||||||
|
{{ $elapsedSeconds := add (mul $localTime.Hour 3600) (mul $localTime.Minute 60) | add $localTime.Second }}
|
||||||
|
{{ $dayProgress := div (mul $elapsedSeconds 100.0) 86400.0 }}
|
||||||
|
{{ $gradient := "#70a1ff" }}
|
||||||
|
{{ if gt $dayProgress 25.0 }}{{ $gradient = "#ff6b6b, #70a1ff" }}{{ end }}
|
||||||
|
{{ if gt $dayProgress 50.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df" }}{{ end }}
|
||||||
|
{{ if gt $dayProgress 75.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df, #70a1ff" }}{{ end }}
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="width: 100%; height: 12px; background: #23262f; border: 1px solid color-mix(in srgb, var(--color-text-subdue) 55%, transparent); border-radius: 10px; overflow: hidden;">
|
||||||
|
<div style="height: 100%; width: {{ $dayProgress }}%; background: linear-gradient(90deg, {{ $gradient }});"></div>
|
||||||
|
</div>
|
||||||
|
<div class="size-h1" style="margin-top: 6px;">{{ printf "%.2f" $dayProgress }}% des Tages sind vorbei</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
- type: custom-api
|
||||||
|
title: Month
|
||||||
|
body-type: string
|
||||||
|
skip-json-validation: true
|
||||||
|
cache: 1s
|
||||||
|
template: |
|
||||||
|
{{ $localTime := now }}
|
||||||
|
{{ $month := $localTime.Month }}
|
||||||
|
{{ $daysInMonth := 31 }}
|
||||||
|
{{ if eq $month 2 }}{{ $daysInMonth = 28 }}{{ end }}
|
||||||
|
{{ if or (eq $month 4) (eq $month 6) (eq $month 9) (eq $month 11) }}{{ $daysInMonth = 30 }}{{ end }}
|
||||||
|
{{ $secondsToday := add (mul $localTime.Hour 3600) (mul $localTime.Minute 60) | add $localTime.Second }}
|
||||||
|
{{ $daysElapsed := add (sub $localTime.Day 1) (div $secondsToday 86400.0) }}
|
||||||
|
{{ $monthProgress := mul (div $daysElapsed $daysInMonth) 100.0 }}
|
||||||
|
{{ $gradient := "#70a1ff" }}
|
||||||
|
{{ if gt $monthProgress 25.0 }}{{ $gradient = "#ff6b6b, #70a1ff" }}{{ end }}
|
||||||
|
{{ if gt $monthProgress 50.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df" }}{{ end }}
|
||||||
|
{{ if gt $monthProgress 75.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df, #70a1ff" }}{{ end }}
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="width: 100%; height: 12px; background: #23262f; border: 1px solid color-mix(in srgb, var(--color-text-subdue) 55%, transparent); border-radius: 10px; overflow: hidden;">
|
||||||
|
<div style="height: 100%; width: {{ $monthProgress }}%; background: linear-gradient(90deg, {{ $gradient }});"></div>
|
||||||
|
</div>
|
||||||
|
<div class="size-h1" style="margin-top: 6px;">{{ printf "%.2f" $monthProgress }}% des Monats sind vorbei</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
- type: custom-api
|
||||||
|
title: Year
|
||||||
|
body-type: string
|
||||||
|
skip-json-validation: true
|
||||||
|
cache: 1s
|
||||||
|
template: |
|
||||||
|
{{ $localTime := now }}
|
||||||
|
{{ $secondsToday := add (mul $localTime.Hour 3600) (mul $localTime.Minute 60) | add $localTime.Second }}
|
||||||
|
{{ $secondsElapsed := add (mul (sub $localTime.YearDay 1) 86400) $secondsToday }}
|
||||||
|
{{ $yearProgress := div (mul $secondsElapsed 100.0) (mul 365 86400) }}
|
||||||
|
{{ $gradient := "#70a1ff" }}
|
||||||
|
{{ if gt $yearProgress 25.0 }}{{ $gradient = "#ff6b6b, #70a1ff" }}{{ end }}
|
||||||
|
{{ if gt $yearProgress 50.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df" }}{{ end }}
|
||||||
|
{{ if gt $yearProgress 75.0 }}{{ $gradient = "#ff6b6b, #f8e71c, #7ed6df, #70a1ff" }}{{ end }}
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="width: 100%; height: 12px; background: #23262f; border: 1px solid color-mix(in srgb, var(--color-text-subdue) 55%, transparent); border-radius: 10px; overflow: hidden;">
|
||||||
|
<div style="height: 100%; width: {{ $yearProgress }}%; background: linear-gradient(90deg, {{ $gradient }});"></div>
|
||||||
|
</div>
|
||||||
|
<div class="size-h1" style="margin-top: 6px;">{{ printf "%.2f" $yearProgress }}% des Jahres sind vorbei</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
- type: clock
|
||||||
|
hour-format: 24h
|
||||||
|
show-progress: true
|
||||||
|
timezones:
|
||||||
|
- timezone: Europe/Berlin
|
||||||
|
label: Berlin
|
||||||
|
- timezone: UTC
|
||||||
|
label: UTC
|
||||||
|
|
||||||
|
- type: calendar
|
||||||
|
first-day-of-week: monday
|
||||||
|
|
||||||
|
- type: bookmarks
|
||||||
|
title: Direkte Einstiege
|
||||||
|
groups:
|
||||||
|
- title: Core
|
||||||
|
color: 212 100 50
|
||||||
|
links:
|
||||||
|
- title: Komodo
|
||||||
|
url: https://komodo.kaleschke.info
|
||||||
|
icon: sh:komodo
|
||||||
|
- title: Gitea
|
||||||
|
url: https://git.kaleschke.info
|
||||||
|
icon: si:gitea
|
||||||
|
- title: Monitoring
|
||||||
|
url: https://monitoring.kaleschke.info
|
||||||
|
icon: si:grafana
|
||||||
|
- title: Ops
|
||||||
|
color: 45 70 55
|
||||||
|
links:
|
||||||
|
- title: Borg
|
||||||
|
url: https://borg.kaleschke.info
|
||||||
|
icon: mdi:archive
|
||||||
|
- title: Glances
|
||||||
|
url: https://glances.kaleschke.info
|
||||||
|
icon: sh:glances
|
||||||
|
- title: Scrutiny
|
||||||
|
url: https://scrutiny.kaleschke.info
|
||||||
|
icon: sh:scrutiny
|
||||||
|
|
||||||
|
- size: full
|
||||||
|
widgets:
|
||||||
|
- type: server-stats
|
||||||
|
title: Server Stats
|
||||||
|
servers:
|
||||||
|
- type: local
|
||||||
|
name: Kallilabcore
|
||||||
|
hide-mountpoints-by-default: false
|
||||||
|
|
||||||
|
- type: group
|
||||||
|
widgets:
|
||||||
|
- type: custom-api
|
||||||
|
title: Immich
|
||||||
|
title-url: https://immich.kaleschke.info
|
||||||
|
cache: 10m
|
||||||
|
url: http://immich_server:2283/api/server/statistics
|
||||||
|
headers:
|
||||||
|
x-api-key: ${GLANCE_IMMICH_API_KEY}
|
||||||
|
subrequests:
|
||||||
|
storage:
|
||||||
|
url: http://immich_server:2283/api/server/storage
|
||||||
|
headers:
|
||||||
|
x-api-key: ${GLANCE_IMMICH_API_KEY}
|
||||||
|
template: |
|
||||||
|
{{ $photos := .JSON.Int "photos" }}
|
||||||
|
{{ $videos := .JSON.Int "videos" }}
|
||||||
|
{{ $usageGiB := div (toFloat (.JSON.Int "usage")) 1073741824.0 }}
|
||||||
|
{{ $storage := .Subrequest "storage" }}
|
||||||
|
{{ $storageOK := and (ge $storage.Response.StatusCode 200) (le $storage.Response.StatusCode 299) }}
|
||||||
|
{{ $percentage := 0.0 }}
|
||||||
|
{{ if $storageOK }}{{ $percentage = $storage.JSON.Float "diskUsagePercentage" }}{{ end }}
|
||||||
|
<div class="flex justify-between text-center">
|
||||||
|
<div>
|
||||||
|
<div class="color-highlight size-h3">{{ $photos | formatNumber }}</div>
|
||||||
|
<div class="size-h6 uppercase">Fotos</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="color-highlight size-h3">{{ $videos | formatNumber }}</div>
|
||||||
|
<div class="size-h6 uppercase">Videos</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="color-highlight size-h3">{{ printf "%.0f" $usageGiB }} GiB</div>
|
||||||
|
<div class="size-h6 uppercase">Medien</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="height: 8px; margin-top: 14px; border-radius: 999px; overflow: hidden; background: color-mix(in srgb, var(--color-text-subdue) 22%, transparent);">
|
||||||
|
<div style="height: 100%; width: {{ if $storageOK }}{{ printf "%.1f" $percentage }}%{{ else }}0%{{ end }}; border-radius: 999px; background: var(--color-primary);"></div>
|
||||||
|
</div>
|
||||||
|
<div class="size-h6 color-subdue" style="margin-top: 8px;">{{ if $storageOK }}{{ printf "%.1f" $percentage }}% Speicher belegt{{ else }}Speicher API nicht verfuegbar{{ end }}</div>
|
||||||
|
|
||||||
|
- type: monitor
|
||||||
|
title: Homelab Status
|
||||||
|
cache: 1m
|
||||||
|
sites:
|
||||||
|
- title: AdGuard Home
|
||||||
|
url: http://192.168.178.58:8082
|
||||||
|
check-url: http://adguard
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/adguard-home.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Authelia
|
||||||
|
url: https://auth.kaleschke.info
|
||||||
|
check-url: http://authelia:9091/api/health
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Gitea
|
||||||
|
url: https://git.kaleschke.info
|
||||||
|
check-url: http://gitea:3000/api/healthz
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Traefik
|
||||||
|
url: https://traefik.kaleschke.info
|
||||||
|
check-url: http://traefik:8082/metrics
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/traefik.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Vaultwarden
|
||||||
|
url: https://vault.kaleschke.info
|
||||||
|
check-url: http://vaultwarden/alive
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vaultwarden.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Komodo
|
||||||
|
url: https://komodo.kaleschke.info
|
||||||
|
check-url: http://komodo-core:9120
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/komodo.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Paperless-ngx
|
||||||
|
url: https://paperless.kaleschke.info
|
||||||
|
check-url: http://paperless-ngx:8000
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Paperless-GPT
|
||||||
|
url: https://paperless-gpt.kaleschke.info
|
||||||
|
check-url: http://paperless-gpt:8080
|
||||||
|
icon: mdi:robot
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Immich
|
||||||
|
url: https://immich.kaleschke.info
|
||||||
|
check-url: http://immich_server:2283/api/server/ping
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Mealie
|
||||||
|
url: https://mealie.kaleschke.info
|
||||||
|
check-url: http://mealie:9000
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mealie.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Nextcloud
|
||||||
|
url: https://cloud.kaleschke.info
|
||||||
|
check-url: http://nextcloud/status.php
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: ntfy
|
||||||
|
url: https://ntfy.kaleschke.info
|
||||||
|
check-url: http://ntfy/v1/health
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/ntfy.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Mail Archiver
|
||||||
|
url: https://mail.kaleschke.info
|
||||||
|
check-url: http://mail-archiver:5000
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mailcow.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: BentoPDF
|
||||||
|
url: https://pdf.kaleschke.info
|
||||||
|
check-url: http://bentopdf:8080
|
||||||
|
icon: mdi:file-pdf-box
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Glance
|
||||||
|
url: https://glance.kaleschke.info
|
||||||
|
check-url: http://glance:8080
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glance.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Monitoring Grafana
|
||||||
|
url: https://monitoring.kaleschke.info
|
||||||
|
check-url: http://monitoring-grafana:3000/api/health
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Glances
|
||||||
|
url: https://glances.kaleschke.info
|
||||||
|
check-url: http://glances:61208
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glances.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Scrutiny
|
||||||
|
url: https://scrutiny.kaleschke.info
|
||||||
|
check-url: http://scrutiny:8080
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/scrutiny.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Speedtest Tracker
|
||||||
|
url: https://speedtest.kaleschke.info
|
||||||
|
check-url: http://speedtest-tracker
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/speedtest-tracker.png
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Filebrowser
|
||||||
|
url: https://files.kaleschke.info
|
||||||
|
check-url: http://filebrowser
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/filebrowser.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: code-server
|
||||||
|
url: https://code.kaleschke.info
|
||||||
|
check-url: http://code-server:8443
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vscode.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Borg UI
|
||||||
|
url: https://borg.kaleschke.info
|
||||||
|
check-url: http://borg-ui:8081
|
||||||
|
icon: mdi:archive-sync
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
|
||||||
|
- size: small
|
||||||
|
widgets:
|
||||||
|
- type: custom-api
|
||||||
|
title: Internet
|
||||||
|
title-url: https://speedtest.kaleschke.info
|
||||||
|
cache: 1h
|
||||||
|
url: http://speedtest-tracker/api/v1/results/latest
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_SPEEDTEST_API_KEY}
|
||||||
|
Accept: application/json
|
||||||
|
template: |
|
||||||
|
{{ $ip := .JSON.String "external_ip" }}
|
||||||
|
{{ if eq $ip "" }}{{ $ip = .JSON.String "data.interface.externalIp" }}{{ end }}
|
||||||
|
{{ $isp := .JSON.String "isp" }}
|
||||||
|
{{ if eq $isp "" }}{{ $isp = .JSON.String "data.isp" }}{{ end }}
|
||||||
|
{{ $server := .JSON.String "server_name" }}
|
||||||
|
{{ if eq $server "" }}{{ $server = .JSON.String "data.server_name" }}{{ end }}
|
||||||
|
<div style="display: flex; flex-direction: column; align-items: center; gap: 6px; text-align: center;">
|
||||||
|
<div class="color-primary size-h2" style="font-weight: 700;">{{ if ne $ip "" }}{{ $ip }}{{ else }}WAN online{{ end }}</div>
|
||||||
|
<div class="size-h5 color-highlight">Speedtest Tracker</div>
|
||||||
|
<div class="size-h6 color-subdue" style="font-style: italic;">{{ if ne $isp "" }}{{ $isp }}{{ else }}{{ $server }}{{ end }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
- type: custom-api
|
||||||
|
title: Internet Speed
|
||||||
|
title-url: https://speedtest.kaleschke.info
|
||||||
|
cache: 1h
|
||||||
|
url: http://speedtest-tracker/api/v1/results/latest
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_SPEEDTEST_API_KEY}
|
||||||
|
Accept: application/json
|
||||||
|
subrequests:
|
||||||
|
stats:
|
||||||
|
url: http://speedtest-tracker/api/v1/stats
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer ${GLANCE_SPEEDTEST_API_KEY}
|
||||||
|
Accept: application/json
|
||||||
|
template: |
|
||||||
|
{{ $stats := .Subrequest "stats" }}
|
||||||
|
{{ $download := .JSON.Float "download" }}
|
||||||
|
{{ if eq $download 0.0 }}{{ $download = .JSON.Float "data.download" }}{{ end }}
|
||||||
|
{{ if eq $download 0.0 }}{{ $download = div (.JSON.Float "download_bits") 1000000.0 }}{{ end }}
|
||||||
|
{{ if eq $download 0.0 }}{{ $download = div (.JSON.Float "data.download_bits") 1000000.0 }}{{ end }}
|
||||||
|
{{ $upload := .JSON.Float "upload" }}
|
||||||
|
{{ if eq $upload 0.0 }}{{ $upload = .JSON.Float "data.upload" }}{{ end }}
|
||||||
|
{{ if eq $upload 0.0 }}{{ $upload = div (.JSON.Float "upload_bits") 1000000.0 }}{{ end }}
|
||||||
|
{{ if eq $upload 0.0 }}{{ $upload = div (.JSON.Float "data.upload_bits") 1000000.0 }}{{ end }}
|
||||||
|
{{ $ping := .JSON.Float "ping" }}
|
||||||
|
{{ if eq $ping 0.0 }}{{ $ping = .JSON.Float "data.ping" }}{{ end }}
|
||||||
|
{{ $downloadAvg := $stats.JSON.Float "avg_download" }}
|
||||||
|
{{ if eq $downloadAvg 0.0 }}{{ $downloadAvg = $stats.JSON.Float "data.download.avg" }}{{ end }}
|
||||||
|
{{ if eq $downloadAvg 0.0 }}{{ $downloadAvg = div ($stats.JSON.Float "data.download.avg_bits") 1000000.0 }}{{ end }}
|
||||||
|
{{ $uploadAvg := $stats.JSON.Float "avg_upload" }}
|
||||||
|
{{ if eq $uploadAvg 0.0 }}{{ $uploadAvg = $stats.JSON.Float "data.upload.avg" }}{{ end }}
|
||||||
|
{{ if eq $uploadAvg 0.0 }}{{ $uploadAvg = div ($stats.JSON.Float "data.upload.avg_bits") 1000000.0 }}{{ end }}
|
||||||
|
{{ $pingAvg := $stats.JSON.Float "avg_ping" }}
|
||||||
|
{{ if eq $pingAvg 0.0 }}{{ $pingAvg = $stats.JSON.Float "data.ping.avg" }}{{ end }}
|
||||||
|
{{ $downloadChange := percentChange $downloadAvg $download }}
|
||||||
|
{{ $uploadChange := percentChange $uploadAvg $upload }}
|
||||||
|
{{ $pingChange := percentChange $pingAvg $ping }}
|
||||||
|
<div class="flex justify-between text-center margin-block-3">
|
||||||
|
<div>
|
||||||
|
<div class="size-small {{ if lt $downloadChange 0.0 }}color-negative{{ else }}color-positive{{ end }}">{{ printf "%+.1f%%" $downloadChange }}</div>
|
||||||
|
<div class="color-highlight size-h3">{{ printf "%.1f" $download }}</div>
|
||||||
|
<div class="size-h6 color-subdue">DOWNLOAD</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="size-small {{ if lt $uploadChange 0.0 }}color-negative{{ else }}color-positive{{ end }}">{{ printf "%+.1f%%" $uploadChange }}</div>
|
||||||
|
<div class="color-highlight size-h3">{{ printf "%.1f" $upload }}</div>
|
||||||
|
<div class="size-h6 color-subdue">UPLOAD</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="size-small {{ if gt $pingChange 0.0 }}color-negative{{ else }}color-positive{{ end }}">{{ printf "%+.1f%%" $pingChange }}</div>
|
||||||
|
<div class="color-highlight size-h3">{{ printf "%.0f ms" $ping }}</div>
|
||||||
|
<div class="size-h6 color-subdue">PING</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
- type: dns-stats
|
||||||
|
title: DNS Stats
|
||||||
|
service: adguard
|
||||||
|
url: http://adguard
|
||||||
|
username: ${GLANCE_ADGUARD_USERNAME}
|
||||||
|
password: ${GLANCE_ADGUARD_PASSWORD}
|
||||||
|
|
||||||
|
- type: monitor
|
||||||
|
title: DNS und VPN
|
||||||
|
cache: 1m
|
||||||
|
sites:
|
||||||
|
- title: AdGuard Home
|
||||||
|
url: http://192.168.178.58:8082
|
||||||
|
check-url: http://adguard
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/adguard-home.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Traefik
|
||||||
|
url: https://traefik.kaleschke.info
|
||||||
|
check-url: http://traefik:8082/metrics
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/traefik.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
|
||||||
|
- type: docker-containers
|
||||||
|
title: Network Container
|
||||||
|
category: network
|
||||||
|
hide-by-default: true
|
||||||
|
sock-path: tcp://glance-docker-socket-proxy:2375
|
||||||
|
containers: &containers
|
||||||
|
traefik:
|
||||||
|
name: Traefik
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/traefik.svg
|
||||||
|
url: https://traefik.kaleschke.info
|
||||||
|
description: Reverse Proxy
|
||||||
|
category: core
|
||||||
|
hide: false
|
||||||
|
gitea:
|
||||||
|
name: Gitea
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
|
||||||
|
url: https://git.kaleschke.info
|
||||||
|
description: GitOps Origin
|
||||||
|
category: core
|
||||||
|
hide: false
|
||||||
|
authelia:
|
||||||
|
name: Authelia
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg
|
||||||
|
url: https://auth.kaleschke.info
|
||||||
|
description: ForwardAuth
|
||||||
|
category: core
|
||||||
|
hide: false
|
||||||
|
vaultwarden:
|
||||||
|
name: Vaultwarden
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vaultwarden.svg
|
||||||
|
url: https://vault.kaleschke.info
|
||||||
|
description: Password Vault
|
||||||
|
category: core
|
||||||
|
hide: false
|
||||||
|
postgresql17:
|
||||||
|
name: PostgreSQL 17
|
||||||
|
icon: si:postgresql
|
||||||
|
description: Shared DB
|
||||||
|
category: core
|
||||||
|
hide: false
|
||||||
|
Redis:
|
||||||
|
name: Redis
|
||||||
|
icon: si:redis
|
||||||
|
description: Shared Cache
|
||||||
|
category: core
|
||||||
|
hide: false
|
||||||
|
adguard:
|
||||||
|
name: AdGuard
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/adguard-home.svg
|
||||||
|
url: http://192.168.178.58:8082
|
||||||
|
description: DNS Filter
|
||||||
|
category: network
|
||||||
|
hide: false
|
||||||
|
unbound:
|
||||||
|
name: Unbound
|
||||||
|
icon: mdi:dns
|
||||||
|
description: Upstream Resolver
|
||||||
|
category: network
|
||||||
|
hide: false
|
||||||
|
Tailscale-Docker:
|
||||||
|
name: Tailscale
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/tailscale.svg
|
||||||
|
description: VPN
|
||||||
|
category: network
|
||||||
|
hide: false
|
||||||
|
ddns-updater:
|
||||||
|
name: DDNS Updater
|
||||||
|
icon: mdi:cloud-sync
|
||||||
|
description: Cloudflare DNS
|
||||||
|
category: network
|
||||||
|
hide: false
|
||||||
|
paperless-ngx:
|
||||||
|
name: Paperless-ngx
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
|
||||||
|
url: https://paperless.kaleschke.info
|
||||||
|
description: Dokumente
|
||||||
|
category: apps
|
||||||
|
hide: false
|
||||||
|
paperless-gpt:
|
||||||
|
name: Paperless-GPT
|
||||||
|
icon: mdi:robot
|
||||||
|
url: https://paperless-gpt.kaleschke.info
|
||||||
|
description: Dokumenten-KI
|
||||||
|
category: apps
|
||||||
|
hide: false
|
||||||
|
immich_server:
|
||||||
|
name: Immich
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
|
||||||
|
url: https://immich.kaleschke.info
|
||||||
|
description: Fotos und Videos
|
||||||
|
category: apps
|
||||||
|
id: immich
|
||||||
|
hide: false
|
||||||
|
immich_postgres:
|
||||||
|
name: DB
|
||||||
|
parent: immich
|
||||||
|
category: apps
|
||||||
|
hide: false
|
||||||
|
immich_redis:
|
||||||
|
name: Redis
|
||||||
|
parent: immich
|
||||||
|
category: apps
|
||||||
|
hide: false
|
||||||
|
immich_machine_learning:
|
||||||
|
name: ML
|
||||||
|
parent: immich
|
||||||
|
category: apps
|
||||||
|
hide: false
|
||||||
|
mealie:
|
||||||
|
name: Mealie
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mealie.svg
|
||||||
|
url: https://mealie.kaleschke.info
|
||||||
|
description: Rezepte
|
||||||
|
category: apps
|
||||||
|
id: mealie
|
||||||
|
hide: false
|
||||||
|
mealie-postgres:
|
||||||
|
name: DB
|
||||||
|
parent: mealie
|
||||||
|
category: apps
|
||||||
|
hide: false
|
||||||
|
nextcloud:
|
||||||
|
name: Nextcloud
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
|
||||||
|
url: https://cloud.kaleschke.info
|
||||||
|
description: Dateien und Sync
|
||||||
|
category: apps
|
||||||
|
id: nextcloud
|
||||||
|
hide: false
|
||||||
|
nextcloud-postgres:
|
||||||
|
name: DB
|
||||||
|
parent: nextcloud
|
||||||
|
category: apps
|
||||||
|
hide: false
|
||||||
|
nextcloud-redis:
|
||||||
|
name: Redis
|
||||||
|
parent: nextcloud
|
||||||
|
category: apps
|
||||||
|
hide: false
|
||||||
|
mail-archiver:
|
||||||
|
name: Mail Archiver
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mailcow.svg
|
||||||
|
url: https://mail.kaleschke.info
|
||||||
|
description: Mail-Archiv
|
||||||
|
category: apps
|
||||||
|
hide: false
|
||||||
|
ntfy:
|
||||||
|
name: ntfy
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/ntfy.svg
|
||||||
|
url: https://ntfy.kaleschke.info
|
||||||
|
description: Push Alerts
|
||||||
|
category: apps
|
||||||
|
hide: false
|
||||||
|
bentopdf:
|
||||||
|
name: BentoPDF
|
||||||
|
icon: mdi:file-pdf-box
|
||||||
|
url: https://pdf.kaleschke.info
|
||||||
|
description: PDF Tools
|
||||||
|
category: apps
|
||||||
|
hide: false
|
||||||
|
glance:
|
||||||
|
name: Glance
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glance.svg
|
||||||
|
url: https://glance.kaleschke.info
|
||||||
|
description: Homelab Uebersicht
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
glance-docker-socket-proxy:
|
||||||
|
name: Glance Socket Proxy
|
||||||
|
icon: si:docker
|
||||||
|
description: Read-only Docker API
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
monitoring-grafana:
|
||||||
|
name: Monitoring Grafana
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg
|
||||||
|
url: https://monitoring.kaleschke.info
|
||||||
|
description: Observability UI
|
||||||
|
category: ops
|
||||||
|
id: monitoring
|
||||||
|
hide: false
|
||||||
|
monitoring-prometheus:
|
||||||
|
name: Prometheus
|
||||||
|
parent: monitoring
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
monitoring-loki:
|
||||||
|
name: Loki
|
||||||
|
parent: monitoring
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
monitoring-promtail:
|
||||||
|
name: Promtail
|
||||||
|
parent: monitoring
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
monitoring-alertmanager:
|
||||||
|
name: Alertmanager
|
||||||
|
parent: monitoring
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
monitoring-alertmanager-ntfy-bridge:
|
||||||
|
name: ntfy Bridge
|
||||||
|
parent: monitoring
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
monitoring-blackbox-exporter:
|
||||||
|
name: Blackbox
|
||||||
|
parent: monitoring
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
monitoring-node-exporter:
|
||||||
|
name: Node Exporter
|
||||||
|
parent: monitoring
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
monitoring-cadvisor:
|
||||||
|
name: cAdvisor
|
||||||
|
parent: monitoring
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
monitoring-influxdb3-core:
|
||||||
|
name: InfluxDB 3
|
||||||
|
parent: monitoring
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
glances:
|
||||||
|
name: Glances
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glances.svg
|
||||||
|
url: https://glances.kaleschke.info
|
||||||
|
description: Host-Monitoring
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
scrutiny:
|
||||||
|
name: Scrutiny
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/scrutiny.svg
|
||||||
|
url: https://scrutiny.kaleschke.info
|
||||||
|
description: SMART
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
speedtest-tracker:
|
||||||
|
name: Speedtest
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/speedtest-tracker.png
|
||||||
|
url: https://speedtest.kaleschke.info
|
||||||
|
description: WAN-Messung
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
filebrowser:
|
||||||
|
name: Filebrowser
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/filebrowser.svg
|
||||||
|
url: https://files.kaleschke.info
|
||||||
|
description: Dateizugriff
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
code-server:
|
||||||
|
name: code-server
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/vscode.svg
|
||||||
|
url: https://code.kaleschke.info
|
||||||
|
description: Web IDE
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
borg-ui:
|
||||||
|
name: Borg UI
|
||||||
|
icon: mdi:archive-sync
|
||||||
|
url: https://borg.kaleschke.info
|
||||||
|
description: Backup und Restore
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
hermes-dashboard:
|
||||||
|
name: Hermes
|
||||||
|
icon: mdi:shield-sparkles
|
||||||
|
url: https://hermes.kaleschke.info
|
||||||
|
description: Ops Agent UI
|
||||||
|
category: ops
|
||||||
|
id: hermes
|
||||||
|
hide: false
|
||||||
|
hermes-gateway:
|
||||||
|
name: Gateway
|
||||||
|
parent: hermes
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
komodo-core:
|
||||||
|
name: Komodo
|
||||||
|
icon: sh:komodo
|
||||||
|
url: https://komodo.kaleschke.info
|
||||||
|
description: Stack Manager
|
||||||
|
category: ops
|
||||||
|
id: komodo
|
||||||
|
hide: false
|
||||||
|
komodo-mongo:
|
||||||
|
name: Mongo
|
||||||
|
parent: komodo
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
komodo-periphery:
|
||||||
|
name: Periphery
|
||||||
|
parent: komodo
|
||||||
|
category: ops
|
||||||
|
hide: false
|
||||||
|
|
||||||
|
- type: docker-containers
|
||||||
|
title: App Container
|
||||||
|
category: apps
|
||||||
|
hide-by-default: true
|
||||||
|
sock-path: tcp://glance-docker-socket-proxy:2375
|
||||||
|
containers: *containers
|
||||||
|
|
||||||
|
- type: docker-containers
|
||||||
|
title: Ops Container
|
||||||
|
category: ops
|
||||||
|
hide-by-default: true
|
||||||
|
sock-path: tcp://glance-docker-socket-proxy:2375
|
||||||
|
containers: *containers
|
||||||
|
|
||||||
|
- name: Infrastructure and Media
|
||||||
|
slug: infrastructure
|
||||||
|
width: wide
|
||||||
|
columns:
|
||||||
|
- size: small
|
||||||
|
widgets:
|
||||||
|
- type: bookmarks
|
||||||
|
title: Core
|
||||||
|
groups:
|
||||||
|
- title: Control Plane
|
||||||
|
color: 212 100 50
|
||||||
|
links:
|
||||||
|
- title: Komodo
|
||||||
|
url: https://komodo.kaleschke.info
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/komodo.svg
|
||||||
|
- title: Gitea
|
||||||
|
url: https://git.kaleschke.info
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
|
||||||
|
- title: Traefik
|
||||||
|
url: https://traefik.kaleschke.info
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/traefik.svg
|
||||||
|
- title: Authelia
|
||||||
|
url: https://auth.kaleschke.info
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/authelia.svg
|
||||||
|
|
||||||
|
- type: bookmarks
|
||||||
|
title: Media und Apps
|
||||||
|
groups:
|
||||||
|
- title: Apps
|
||||||
|
color: 140 70 40
|
||||||
|
links:
|
||||||
|
- title: Immich
|
||||||
|
url: https://immich.kaleschke.info
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
|
||||||
|
- title: Paperless
|
||||||
|
url: https://paperless.kaleschke.info
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
|
||||||
|
- title: Nextcloud
|
||||||
|
url: https://cloud.kaleschke.info
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
|
||||||
|
- title: Mealie
|
||||||
|
url: https://mealie.kaleschke.info
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/mealie.svg
|
||||||
|
|
||||||
|
- size: full
|
||||||
|
widgets:
|
||||||
|
- type: monitor
|
||||||
|
title: Platform Checks
|
||||||
|
cache: 1m
|
||||||
|
sites:
|
||||||
|
- title: Gitea
|
||||||
|
url: https://git.kaleschke.info
|
||||||
|
check-url: http://gitea:3000/api/healthz
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitea.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Monitoring Grafana
|
||||||
|
url: https://monitoring.kaleschke.info
|
||||||
|
check-url: http://monitoring-grafana:3000/api/health
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/grafana.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Glance
|
||||||
|
url: https://glance.kaleschke.info
|
||||||
|
check-url: http://glance:8080
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glance.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Immich
|
||||||
|
url: https://immich.kaleschke.info
|
||||||
|
check-url: http://immich_server:2283/api/server/ping
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/immich.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Paperless-ngx
|
||||||
|
url: https://paperless.kaleschke.info
|
||||||
|
check-url: http://paperless-ngx:8000
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/paperless-ngx.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
- title: Nextcloud
|
||||||
|
url: https://cloud.kaleschke.info
|
||||||
|
check-url: http://nextcloud/status.php
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/nextcloud.svg
|
||||||
|
timeout: 5s
|
||||||
|
alt-status-codes: [200, 302, 401, 403]
|
||||||
|
|
||||||
|
- type: docker-containers
|
||||||
|
title: Core Container
|
||||||
|
category: core
|
||||||
|
hide-by-default: true
|
||||||
|
sock-path: tcp://glance-docker-socket-proxy:2375
|
||||||
|
containers: *containers
|
||||||
|
|
||||||
|
- type: docker-containers
|
||||||
|
title: App Container
|
||||||
|
category: apps
|
||||||
|
hide-by-default: true
|
||||||
|
sock-path: tcp://glance-docker-socket-proxy:2375
|
||||||
|
containers: *containers
|
||||||
|
|
||||||
|
- type: docker-containers
|
||||||
|
title: Ops Container
|
||||||
|
category: ops
|
||||||
|
hide-by-default: true
|
||||||
|
sock-path: tcp://glance-docker-socket-proxy:2375
|
||||||
|
containers: *containers
|
||||||
|
|
||||||
|
- size: small
|
||||||
|
widgets:
|
||||||
|
- type: bookmarks
|
||||||
|
title: Ops
|
||||||
|
groups:
|
||||||
|
- title: Tools
|
||||||
|
color: 4 78 57
|
||||||
|
links:
|
||||||
|
- title: Glances
|
||||||
|
url: https://glances.kaleschke.info
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/glances.svg
|
||||||
|
- title: Scrutiny
|
||||||
|
url: https://scrutiny.kaleschke.info
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/scrutiny.svg
|
||||||
|
- title: Speedtest
|
||||||
|
url: https://speedtest.kaleschke.info
|
||||||
|
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/speedtest-tracker.png
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
services:
|
||||||
|
glance:
|
||||||
|
image: glanceapp/glance:v0.8.4
|
||||||
|
container_name: glance
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
GLANCE_IMMICH_API_KEY: ${GLANCE_IMMICH_API_KEY:-}
|
||||||
|
GLANCE_ADGUARD_USERNAME: ${GLANCE_ADGUARD_USERNAME:-}
|
||||||
|
GLANCE_ADGUARD_PASSWORD: ${GLANCE_ADGUARD_PASSWORD:-}
|
||||||
|
GLANCE_SPEEDTEST_API_KEY: ${GLANCE_SPEEDTEST_API_KEY:-}
|
||||||
|
volumes:
|
||||||
|
- ./config:/app/config:ro
|
||||||
|
networks:
|
||||||
|
- frontend_net
|
||||||
|
- glance_socket_net
|
||||||
|
depends_on:
|
||||||
|
- glance-docker-socket-proxy
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.docker.network=frontend_net
|
||||||
|
- traefik.http.routers.glance.rule=Host(`glance.kaleschke.info`)
|
||||||
|
- traefik.http.routers.glance.entrypoints=websecure
|
||||||
|
- traefik.http.routers.glance.tls=true
|
||||||
|
- traefik.http.routers.glance.tls.certresolver=le
|
||||||
|
- traefik.http.routers.glance.middlewares=authelia@file,secure-headers@file
|
||||||
|
- traefik.http.services.glance.loadbalancer.server.port=8080
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
glance-docker-socket-proxy:
|
||||||
|
image: tecnativa/docker-socket-proxy:v0.4.2
|
||||||
|
container_name: glance-docker-socket-proxy
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
LOG_LEVEL: warning
|
||||||
|
POST: "0"
|
||||||
|
CONTAINERS: "1"
|
||||||
|
INFO: "1"
|
||||||
|
VERSION: "1"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
networks:
|
||||||
|
- glance_socket_net
|
||||||
|
expose:
|
||||||
|
- "2375"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend_net:
|
||||||
|
external: true
|
||||||
|
glance_socket_net:
|
||||||
|
name: glance_socket_net
|
||||||
|
internal: true
|
||||||
|
driver: bridge
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Grafana + InfluxDB 3 Core
|
|
||||||
|
|
||||||
Monitoring-Stack fuer Grafana + InfluxDB 3 Core. InfluxDB bleibt ohne Public Route; interne Writer wie Home Assistant koennen ueber einen gezielt gebundenen LAN-Port schreiben.
|
|
||||||
|
|
||||||
## Quellen / Entscheidungen
|
|
||||||
|
|
||||||
- Grafana nutzt das offizielle OSS-Image `grafana/grafana:12.4.3`.
|
|
||||||
- InfluxDB nutzt `influxdb:3.9.1-core`, nicht `latest`, weil `latest` bei InfluxDB aktiv in Richtung InfluxDB 3 umgestellt wird.
|
|
||||||
- Grafana wird ueber Traefik + `authelia@file,secure-headers@file` unter `grafana.kaleschke.info` veroeffentlicht.
|
|
||||||
- InfluxDB bleibt ohne Traefik-Route. Der HTTP-Port `8181` kann fuer interne Writer wie Home Assistant ueber `INFLUXDB_BIND_IP` auf eine LAN-Adresse gebunden werden; Default ist `127.0.0.1`.
|
|
||||||
- InfluxDB haengt an zwei Compose-Netzen: `grafana_influx_internal` fuer Grafana und `grafana_influx_lan` fuer das Docker Host-Port-Publishing. Im laufenden Komodo-Stack heissen sie durch den Compose-Projektpraefix `grafana_grafana_influx_internal` und `grafana_grafana_influx_lan`. InfluxDB haengt bewusst nicht im `frontend_net`.
|
|
||||||
- Grafana provisioning legt eine SQL-Datenquelle fuer InfluxDB 3 Core mit der Datenbank `homelab` an.
|
|
||||||
- Der Grafana-Datasource-Token liegt als Secret-Datei auf dem Host und wird beim Containerstart nur containerintern in die fuer Grafana-Provisioning noetige Environment-Variable geladen.
|
|
||||||
- Home Assistant schreibt mit der InfluxDB-v2-API-Kompatibilitaet nach InfluxDB 3; Details: `docs/HOME_ASSISTANT_INFLUXDB_ECOWITT.md`.
|
|
||||||
|
|
||||||
## Initiale Einrichtung
|
|
||||||
|
|
||||||
1. Secret fuer Grafana anlegen:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
install -m 600 /dev/null /mnt/user/appdata/secrets/grafana_admin_password.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Offline-Admin-Token fuer InfluxDB 3 als JSON anlegen:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"token": "apiv3_REPLACE_WITH_STRONG_RANDOM_TOKEN",
|
|
||||||
"name": "admin",
|
|
||||||
"description": "Admin token for KalliLab InfluxDB 3 Core"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Pfad: `/mnt/user/appdata/secrets/influxdb3_admin_token.json`, Rechte `600`.
|
|
||||||
|
|
||||||
3. Grafana-Datasource-Token anlegen. Fuer InfluxDB 3 Core aktuell einen eigenen Named-Admin-Token verwenden, damit der Grafana-Zugang getrennt vom initialen Operator-/Admin-Token rotiert werden kann:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
install -m 600 /dev/null /mnt/user/appdata/secrets/grafana_influxdb_token.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Provisioning-Datei aus dem Git-Checkout auf den Host-Appdata-Pfad kopieren:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /mnt/user/appdata/grafana/provisioning/datasources
|
|
||||||
cp /mnt/user/appdata/komodo/core/repos/homelab-infra/ops/grafana-influxdb/provisioning/datasources/influxdb.yml /mnt/user/appdata/grafana/provisioning/datasources/influxdb.yml
|
|
||||||
chmod 644 /mnt/user/appdata/grafana/provisioning/datasources/influxdb.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Nach dem ersten Start die Datenbank anlegen:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec influxdb3-core influxdb3 create database homelab --token "$INFLUXDB3_AUTH_TOKEN"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Smoke-Test nach Deploy
|
|
||||||
|
|
||||||
- `https://grafana.kaleschke.info` oeffnet nach Authelia die Grafana-Loginseite.
|
|
||||||
- Grafana `Connections -> Data sources -> InfluxDB 3 Core -> Save & test` ist erfolgreich.
|
|
||||||
- InfluxDB bleibt ohne Public Route. Falls `INFLUXDB_BIND_IP` auf die LAN-IP gesetzt ist, ist Port `8181` nur im internen Netz fuer Writer wie Home Assistant erreichbar.
|
|
||||||
- `docker ps` zeigt fuer `influxdb3-core` `192.168.178.58:8181->8181/tcp` oder den per `INFLUXDB_BIND_IP` gesetzten Host.
|
|
||||||
- `ss -ltnp | grep 8181` zeigt einen Listener auf der gebundenen Host-IP.
|
|
||||||
- `curl -i http://192.168.178.58:8181/` liefert ohne Token erwartbar `401 Unauthorized`.
|
|
||||||
|
|
||||||
## Drift-Check
|
|
||||||
|
|
||||||
Wenn Komodo, Gitea und Runtime nicht zusammenpassen, zuerst `docs/GITOPS_DRIFT_RUNBOOK.md` verwenden. Besonders wichtig:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /mnt/user/services/stacks/grafana
|
|
||||||
git rev-parse --short HEAD
|
|
||||||
grep -nE "ports:|grafana_influx_lan|grafana_influx_internal" -A4 -B2 ops/grafana-influxdb/docker-compose.yml
|
|
||||||
docker inspect influxdb3-core --format '{{json .NetworkSettings.Ports}}'
|
|
||||||
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep influx
|
|
||||||
ss -ltnp | grep 8181
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
- Stack in Komodo stoppen oder Git auf den letzten Stand ohne `ops/grafana-influxdb` zuruecknehmen.
|
|
||||||
- Persistente Daten liegen unter `/mnt/user/appdata/grafana` und `/mnt/user/appdata/influxdb3`; nicht automatisch loeschen.
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
services:
|
|
||||||
grafana:
|
|
||||||
image: grafana/grafana:12.4.3@sha256:2e986801428cd689c2358605289c90ab37d2b39e24808874971f54c99bcdc412
|
|
||||||
container_name: grafana
|
|
||||||
restart: unless-stopped
|
|
||||||
user: "0"
|
|
||||||
environment:
|
|
||||||
GF_SERVER_ROOT_URL: https://grafana.kaleschke.info/
|
|
||||||
GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin_password
|
|
||||||
GF_USERS_ALLOW_SIGN_UP: "false"
|
|
||||||
GF_AUTH_ANONYMOUS_ENABLED: "false"
|
|
||||||
entrypoint:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
export GRAFANA_INFLUXDB_TOKEN="$$(cat /run/secrets/grafana_influxdb_token)"
|
|
||||||
exec /run.sh
|
|
||||||
volumes:
|
|
||||||
- /mnt/user/appdata/grafana:/var/lib/grafana
|
|
||||||
- /mnt/user/appdata/grafana/provisioning:/etc/grafana/provisioning:ro
|
|
||||||
secrets:
|
|
||||||
- grafana_admin_password
|
|
||||||
- grafana_influxdb_token
|
|
||||||
networks:
|
|
||||||
- frontend_net
|
|
||||||
- grafana_influx_internal
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
labels:
|
|
||||||
- traefik.enable=true
|
|
||||||
- traefik.docker.network=frontend_net
|
|
||||||
- traefik.http.routers.grafana.rule=Host(`grafana.kaleschke.info`)
|
|
||||||
- traefik.http.routers.grafana.entrypoints=websecure
|
|
||||||
- traefik.http.routers.grafana.tls=true
|
|
||||||
- traefik.http.routers.grafana.tls.certresolver=le
|
|
||||||
- traefik.http.routers.grafana.middlewares=authelia@file,secure-headers@file
|
|
||||||
- traefik.http.services.grafana.loadbalancer.server.port=3000
|
|
||||||
|
|
||||||
influxdb3-core:
|
|
||||||
image: influxdb:3.9.1-core@sha256:1d58c8b9ac90153ae3a020ede2810c8284933dda50ac71e7573389ab6f012128
|
|
||||||
container_name: influxdb3-core
|
|
||||||
restart: unless-stopped
|
|
||||||
user: "0"
|
|
||||||
ports:
|
|
||||||
- "${INFLUXDB_BIND_IP:-127.0.0.1}:8181:8181"
|
|
||||||
command:
|
|
||||||
- influxdb3
|
|
||||||
- serve
|
|
||||||
- --node-id=kallilabcore
|
|
||||||
- --object-store=file
|
|
||||||
- --data-dir=/var/lib/influxdb3/data
|
|
||||||
- --plugin-dir=/var/lib/influxdb3/plugins
|
|
||||||
- --admin-token-file=/run/secrets/influxdb3_admin_token
|
|
||||||
volumes:
|
|
||||||
- /mnt/user/appdata/influxdb3/data:/var/lib/influxdb3/data
|
|
||||||
- /mnt/user/appdata/influxdb3/plugins:/var/lib/influxdb3/plugins
|
|
||||||
secrets:
|
|
||||||
- influxdb3_admin_token
|
|
||||||
networks:
|
|
||||||
- grafana_influx_lan
|
|
||||||
- grafana_influx_internal
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
|
|
||||||
secrets:
|
|
||||||
grafana_admin_password:
|
|
||||||
file: /mnt/user/appdata/secrets/grafana_admin_password.txt
|
|
||||||
influxdb3_admin_token:
|
|
||||||
file: /mnt/user/appdata/secrets/influxdb3_admin_token.json
|
|
||||||
grafana_influxdb_token:
|
|
||||||
file: /mnt/user/appdata/secrets/grafana_influxdb_token.txt
|
|
||||||
|
|
||||||
networks:
|
|
||||||
frontend_net:
|
|
||||||
external: true
|
|
||||||
grafana_influx_lan:
|
|
||||||
driver: bridge
|
|
||||||
grafana_influx_internal:
|
|
||||||
internal: true
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
apiVersion: 1
|
|
||||||
|
|
||||||
prune: true
|
|
||||||
|
|
||||||
datasources:
|
|
||||||
- name: InfluxDB 3 Core
|
|
||||||
uid: influxdb3-core
|
|
||||||
type: influxdb
|
|
||||||
access: proxy
|
|
||||||
url: http://influxdb3-core:8181
|
|
||||||
isDefault: true
|
|
||||||
jsonData:
|
|
||||||
version: SQL
|
|
||||||
dbName: homelab
|
|
||||||
httpMode: POST
|
|
||||||
insecureGrpc: true
|
|
||||||
secureJsonData:
|
|
||||||
token: $GRAFANA_INFLUXDB_TOKEN
|
|
||||||
@@ -425,6 +425,81 @@ Back up at least:
|
|||||||
- `/mnt/user/appdata/hermes-agent/ssh/known_hosts`
|
- `/mnt/user/appdata/hermes-agent/ssh/known_hosts`
|
||||||
- `/mnt/user/appdata/secrets/hermes_runner_id_ed25519`
|
- `/mnt/user/appdata/secrets/hermes_runner_id_ed25519`
|
||||||
|
|
||||||
|
## Phase 7 - Ops Monitor (homelab-ops-monitor)
|
||||||
|
|
||||||
|
### Was es ist
|
||||||
|
|
||||||
|
Ein Skill + Script das Hermes zum kontextuellen Ops-Assistenten macht.
|
||||||
|
Wenn ein Service ausfaellt, bekommt er nicht eine rohe Fehlermeldung, sondern einen
|
||||||
|
angereicherten Alert: Abhaengigkeiten, letzter Backup-Dump, erster Diagnoseschritt.
|
||||||
|
|
||||||
|
### Laufzeit-Architektur (Stand 2026-05-06)
|
||||||
|
|
||||||
|
- Hermes laeuft als Docker-Container auf dem Unraid-Host (hermes-gateway, hermes_net)
|
||||||
|
- Terminal-Backend SSH-Ziel: `192.168.178.143` (dedizierte Linux-VM, Model C)
|
||||||
|
- Hermes-User auf der VM: `hermes`
|
||||||
|
- Repo-Clone auf der VM: `/srv/hermes-workspace/homelab-infra/`
|
||||||
|
- Workspace-Verzeichnis auf der VM: `/srv/hermes-workspace/`
|
||||||
|
|
||||||
|
Wichtig fuer KI-Agenten und Betreiber: Das Terminal laeuft auf der VM, nicht auf dem
|
||||||
|
Unraid-Host. `/mnt/user/...`-Pfade sind von der VM aus nicht direkt erreichbar.
|
||||||
|
Docker-CLI ist auf der VM nicht vorhanden und wird nicht benoetigt.
|
||||||
|
|
||||||
|
### Dateien
|
||||||
|
|
||||||
|
| Datei | Pfad im Repo | Pfad auf VM |
|
||||||
|
|---|---|---|
|
||||||
|
| Wissensbasis | `ops/hermes-agent/services.json` | `/srv/hermes-workspace/homelab-infra/ops/hermes-agent/services.json` |
|
||||||
|
| Health-Script | `ops/hermes-agent/scripts/check_health.py` | `/srv/hermes-workspace/homelab-infra/ops/hermes-agent/scripts/check_health.py` |
|
||||||
|
| Skill-Prompt | `ops/hermes-agent/skills/homelab-ops-monitor.md` | `/srv/hermes-workspace/homelab-infra/ops/hermes-agent/skills/homelab-ops-monitor.md` |
|
||||||
|
|
||||||
|
### check_health.py
|
||||||
|
|
||||||
|
- Keine externen Abhaengigkeiten — nur Python-Standardbibliothek (`json`, `urllib`, `ssl`)
|
||||||
|
- Kein Docker CLI, kein pip, kein Root noetig
|
||||||
|
- Prueft Services mit URL via HTTP GET (2xx/3xx/4xx = healthy, 5xx/Timeout = unhealthy)
|
||||||
|
- Interne Services ohne URL (Datenbanken, Redis) werden als `"internal"` markiert — kein Fehler
|
||||||
|
- Dump-Timestamps werden gelesen falls `/mnt/user/backups/borg/dumps/latest` erreichbar ist (optional)
|
||||||
|
- services.json wird relativ zum Script-Verzeichnis gesucht (`../services.json`)
|
||||||
|
|
||||||
|
Verwendung auf der VM:
|
||||||
|
```bash
|
||||||
|
cd /srv/hermes-workspace/homelab-infra
|
||||||
|
python3 ops/hermes-agent/scripts/check_health.py --summary # Tier 1+2
|
||||||
|
python3 ops/hermes-agent/scripts/check_health.py paperless-ngx # gezielt
|
||||||
|
python3 ops/hermes-agent/scripts/check_health.py --all # alle Tiers
|
||||||
|
```
|
||||||
|
|
||||||
|
### services.json
|
||||||
|
|
||||||
|
Maschinenlesbare Wissensbasis abgeleitet aus `docs/SERVICE_CATALOG.md`.
|
||||||
|
Enthaelt fuer jeden Service: Tier, Container-Name, Abhaengigkeiten, Dump-Dateiname,
|
||||||
|
Datenpfade, first_check-Hinweis und betriebliche Notizen.
|
||||||
|
|
||||||
|
Bei Aenderungen am Service Catalog: `services.json` und `services.yaml` parallel aktualisieren.
|
||||||
|
|
||||||
|
### Skill importieren
|
||||||
|
|
||||||
|
```
|
||||||
|
„Bitte erstelle einen neuen Skill namens homelab-ops-monitor. Lies dazu die Datei
|
||||||
|
/srv/hermes-workspace/homelab-infra/ops/hermes-agent/skills/homelab-ops-monitor.md
|
||||||
|
und lege den Skill mit diesem Inhalt an."
|
||||||
|
```
|
||||||
|
|
||||||
|
Nach Repo-Aenderungen auf der VM pullen:
|
||||||
|
```bash
|
||||||
|
cd /srv/hermes-workspace/homelab-infra && git pull
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bekannte Einschraenkungen
|
||||||
|
|
||||||
|
- Interne Services (PostgreSQL, Redis, MongoDB) koennen nicht extern geprueft werden
|
||||||
|
- Dump-Timestamps nur lesbar wenn `/mnt/user/backups/` per NFS oder Mount erreichbar ist
|
||||||
|
- Docker-Healthstatus der Container ist von der VM aus nicht pruefbar (kein Docker-Socket)
|
||||||
|
- Alerting via ntfy erfordert dass ntfy selbst healthy ist (Fallback: Telegram)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Official sources used
|
## Official sources used
|
||||||
|
|
||||||
- Repository README: <https://github.com/NousResearch/hermes-agent>
|
- Repository README: <https://github.com/NousResearch/hermes-agent>
|
||||||
|
|||||||
@@ -2,27 +2,33 @@
|
|||||||
"""
|
"""
|
||||||
check_health.py — Homelab Alert Enricher
|
check_health.py — Homelab Alert Enricher
|
||||||
=========================================
|
=========================================
|
||||||
Laedt services.yaml, prueft Docker-Health aller bekannten Abhaengigkeiten,
|
Laedt services.json, prueft Services via HTTP(S) und gibt einen
|
||||||
liest Dump-Timestamps und gibt einen strukturierten JSON-Report aus.
|
strukturierten JSON-Report aus. Hermes nutzt diesen Report fuer
|
||||||
|
angereicherte ntfy-Alerts.
|
||||||
|
|
||||||
Hermes liest diesen Report und baut daraus eine angereicherte ntfy-Nachricht.
|
Keine externen Abhaengigkeiten — nur Python-Standardbibliothek.
|
||||||
|
Kein Docker CLI, kein Root, kein pip.
|
||||||
|
|
||||||
|
Check-Strategie:
|
||||||
|
- Services MIT url: HTTP GET, 2xx/3xx = healthy, Timeout/4xx/5xx = unhealthy
|
||||||
|
- Services OHNE url: "internal" — kein externer Check moeglich
|
||||||
|
- Dump-Timestamps: werden gelesen falls /mnt/user/... erreichbar ist (optional)
|
||||||
|
|
||||||
Verwendung:
|
Verwendung:
|
||||||
python3 check_health.py # alle unhealthy Container
|
python3 check_health.py # alle Services pruefen (Tier 1+2)
|
||||||
python3 check_health.py paperless-ngx # gezielt einen Service pruefen
|
python3 check_health.py paperless-ngx # gezielt einen Service pruefen
|
||||||
python3 check_health.py --summary # Gesamtstatus als Zusammenfassung
|
python3 check_health.py --summary # Gesamtstatus Tier 1+2
|
||||||
|
python3 check_health.py --all # alle Tiers inkl. Tier 3
|
||||||
|
|
||||||
Pfad auf Host (via Komodo-Clone):
|
Pfad auf der Hermes-VM:
|
||||||
/mnt/user/services/homelab/ops/hermes-agent/scripts/check_health.py
|
/srv/hermes-workspace/homelab-infra/ops/hermes-agent/scripts/check_health.py
|
||||||
|
|
||||||
services.yaml wird relativ zum Script-Verzeichnis gesucht:
|
|
||||||
../services.yaml
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import ssl
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -31,118 +37,115 @@ from pathlib import Path
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||||
SERVICES_YAML_PATH = SCRIPT_DIR.parent / "services.yaml"
|
SERVICES_JSON_PATH = SCRIPT_DIR.parent / "services.json"
|
||||||
|
SERVICES_JSON_FALLBACK = Path("/srv/hermes-workspace/homelab-infra/ops/hermes-agent/services.json")
|
||||||
|
|
||||||
# Fallback falls das Repo unter einem anderen Pfad liegt
|
# HTTP-Check Timeout in Sekunden
|
||||||
SERVICES_YAML_FALLBACK = Path("/mnt/user/services/homelab/ops/hermes-agent/services.yaml")
|
HTTP_TIMEOUT = 8
|
||||||
|
|
||||||
# Dump-Warnschwelle in Stunden (aelter = Warnung)
|
# Dump-Verzeichnis (optional — wird uebersprungen wenn nicht erreichbar)
|
||||||
|
DUMP_BASE_PATHS = [
|
||||||
|
Path("/mnt/user/backups/borg/dumps/latest"), # Unraid direkt
|
||||||
|
Path("/opt/dumps"), # gemounteter Fallback
|
||||||
|
]
|
||||||
|
|
||||||
|
# Dump-Warnschwelle
|
||||||
DUMP_WARN_HOURS = 26
|
DUMP_WARN_HOURS = 26
|
||||||
|
|
||||||
|
# SSL-Verification (True = strikt, False = ignoriert selbstsignierte Zerts)
|
||||||
|
SSL_VERIFY = False
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Hilfsfunktionen
|
# Hilfsfunktionen
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def load_services():
|
def load_services():
|
||||||
"""Laedt services.yaml. Gibt (services_dict, meta_dict) zurueck."""
|
"""Laedt services.json ohne externe Abhaengigkeiten."""
|
||||||
try:
|
path = SERVICES_JSON_PATH if SERVICES_JSON_PATH.exists() else SERVICES_JSON_FALLBACK
|
||||||
import yaml
|
|
||||||
except ImportError:
|
|
||||||
# PyYAML nicht installiert — minimaler Fallback ueber pip
|
|
||||||
subprocess.run(
|
|
||||||
[sys.executable, "-m", "pip", "install", "pyyaml", "-q"],
|
|
||||||
check=True
|
|
||||||
)
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
path = SERVICES_YAML_PATH if SERVICES_YAML_PATH.exists() else SERVICES_YAML_FALLBACK
|
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise FileNotFoundError(f"services.yaml nicht gefunden: {path}")
|
raise FileNotFoundError(
|
||||||
|
f"services.json nicht gefunden: {path}\n"
|
||||||
with open(path) as f:
|
"Bitte 'git pull' in /srv/hermes-workspace/homelab-infra/ ausfuehren."
|
||||||
data = yaml.safe_load(f)
|
)
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
return data.get("services", {}), data.get("meta", {})
|
return data.get("services", {}), data.get("meta", {})
|
||||||
|
|
||||||
|
|
||||||
def docker_inspect(container_name: str) -> dict:
|
def http_check(url: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Gibt {'status': str, 'health': str} zurueck.
|
Fuehrt einen HTTP GET gegen url aus.
|
||||||
status: running | exited | restarting | dead | not_found | error
|
Gibt {'reachable': bool, 'status_code': int|None, 'error': str|None} zurueck.
|
||||||
health: healthy | unhealthy | starting | none | unknown
|
2xx und 3xx gelten als healthy. 401/403 auch (Service laeuft, Auth blockiert).
|
||||||
"""
|
"""
|
||||||
try:
|
ctx = ssl.create_default_context()
|
||||||
result = subprocess.run(
|
if not SSL_VERIFY:
|
||||||
[
|
ctx.check_hostname = False
|
||||||
"docker", "inspect",
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
"--format",
|
|
||||||
"{{.State.Status}}|||{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}",
|
|
||||||
container_name,
|
|
||||||
],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
return {"status": "not_found", "health": "unknown"}
|
|
||||||
|
|
||||||
parts = result.stdout.strip().split("|||")
|
try:
|
||||||
return {
|
req = urllib.request.Request(url, method="GET", headers={"User-Agent": "hermes-healthcheck/1.0"})
|
||||||
"status": parts[0].strip() if parts else "unknown",
|
with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT, context=ctx) as resp:
|
||||||
"health": parts[1].strip() if len(parts) > 1 else "none",
|
code = resp.status
|
||||||
}
|
healthy = code < 500
|
||||||
|
return {"reachable": healthy, "status_code": code, "error": None}
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
# 4xx = Service laeuft, aber Auth oder Not Found — trotzdem erreichbar
|
||||||
|
healthy = e.code < 500
|
||||||
|
return {"reachable": healthy, "status_code": e.code, "error": None}
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
return {"reachable": False, "status_code": None, "error": str(e.reason)}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"status": "error", "health": str(e)}
|
return {"reachable": False, "status_code": None, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
def is_healthy(inspect_result: dict) -> bool:
|
def check_service(service: dict) -> dict:
|
||||||
status = inspect_result.get("status", "")
|
"""
|
||||||
health = inspect_result.get("health", "")
|
Prueft einen einzelnen Service.
|
||||||
if status != "running":
|
Gibt {'healthy': bool, 'method': str, 'detail': dict} zurueck.
|
||||||
return False
|
"""
|
||||||
if health in ("unhealthy",):
|
url = service.get("url")
|
||||||
return False
|
|
||||||
return True
|
if url:
|
||||||
|
result = http_check(url)
|
||||||
|
return {
|
||||||
|
"healthy": result["reachable"],
|
||||||
|
"method": "http",
|
||||||
|
"url": url,
|
||||||
|
"status_code": result["status_code"],
|
||||||
|
"error": result["error"],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"healthy": None, # None = unbekannt (intern, kein externer Check)
|
||||||
|
"method": "internal",
|
||||||
|
"url": None,
|
||||||
|
"status_code": None,
|
||||||
|
"error": "Kein externer Check — interner Service ohne URL",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_unhealthy_containers() -> list[str]:
|
def find_dump_base() -> Path | None:
|
||||||
"""Gibt Liste aller Container zurueck die unhealthy oder nicht running sind."""
|
"""Sucht das Dump-Verzeichnis in bekannten Pfaden."""
|
||||||
try:
|
for p in DUMP_BASE_PATHS:
|
||||||
# unhealthy per healthcheck
|
if p.exists():
|
||||||
r1 = subprocess.run(
|
return p
|
||||||
["docker", "ps", "--filter", "health=unhealthy", "--format", "{{.Names}}"],
|
return None
|
||||||
capture_output=True, text=True, timeout=10,
|
|
||||||
)
|
|
||||||
# exited/dead Container die eigentlich laufen sollten
|
|
||||||
r2 = subprocess.run(
|
|
||||||
["docker", "ps", "--filter", "status=exited", "--format", "{{.Names}}"],
|
|
||||||
capture_output=True, text=True, timeout=10,
|
|
||||||
)
|
|
||||||
names = set()
|
|
||||||
for raw in (r1.stdout, r2.stdout):
|
|
||||||
for name in raw.strip().split("\n"):
|
|
||||||
name = name.strip()
|
|
||||||
if name:
|
|
||||||
names.add(name)
|
|
||||||
return sorted(names)
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def get_dump_info(dump_file: str | None, dump_base: str) -> dict | None:
|
def get_dump_info(dump_file: str | None, dump_base: Path | None) -> dict | None:
|
||||||
"""Gibt Alter und Groesse des Dump-Files zurueck (oder None wenn nicht vorhanden)."""
|
"""Liest Alter und Groesse einer Dump-Datei."""
|
||||||
if not dump_file:
|
if not dump_file or not dump_base:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
path = Path(dump_base) / dump_file
|
path = dump_base / dump_file
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
return {"file": dump_file, "exists": False, "age_hours": None, "size_mb": None}
|
return {"file": dump_file, "exists": False, "age_hours": None, "size_mb": None, "warn": False}
|
||||||
|
|
||||||
stat = path.stat()
|
stat = path.stat()
|
||||||
age_hours = round((datetime.now().timestamp() - stat.st_mtime) / 3600, 1)
|
age_hours = round((datetime.now().timestamp() - stat.st_mtime) / 3600, 1)
|
||||||
size_mb = round(stat.st_size / 1_048_576, 1)
|
size_mb = round(stat.st_size / 1_048_576, 1)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"file": dump_file,
|
"file": dump_file,
|
||||||
"exists": True,
|
"exists": True,
|
||||||
@@ -156,30 +159,28 @@ def get_dump_info(dump_file: str | None, dump_base: str) -> dict | None:
|
|||||||
# Report-Generierung
|
# Report-Generierung
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def build_service_report(service_key: str, service: dict, all_services: dict, meta: dict) -> dict:
|
def build_service_report(service_key: str, service: dict, all_services: dict) -> dict:
|
||||||
"""Erstellt einen vollstaendigen Report fuer einen einzelnen Service."""
|
"""Vollstaendiger Report fuer einen einzelnen Service inkl. Abhaengigkeiten."""
|
||||||
dump_base = meta.get("dump_base", "/mnt/user/backups/borg/dumps/latest")
|
dump_base = find_dump_base()
|
||||||
|
|
||||||
# Eigener Container-Status
|
# Eigener Status
|
||||||
own_inspect = docker_inspect(service["container_name"])
|
own = check_service(service)
|
||||||
own_healthy = is_healthy(own_inspect)
|
|
||||||
|
|
||||||
# Abhaengigkeits-Check
|
# Abhaengigkeiten pruefen
|
||||||
dep_results = {}
|
dep_results = {}
|
||||||
for dep_key in service.get("dependencies", []):
|
for dep_key in service.get("dependencies", []):
|
||||||
dep = all_services.get(dep_key)
|
dep = all_services.get(dep_key)
|
||||||
if not dep:
|
if not dep:
|
||||||
dep_results[dep_key] = {"status": "unknown_service", "health": "unknown", "healthy": False}
|
dep_results[dep_key] = {"healthy": None, "method": "unknown", "error": "Nicht in services.json"}
|
||||||
continue
|
continue
|
||||||
insp = docker_inspect(dep["container_name"])
|
|
||||||
dep_results[dep_key] = {
|
dep_results[dep_key] = {
|
||||||
**insp,
|
|
||||||
"healthy": is_healthy(insp),
|
|
||||||
"tier": dep.get("tier"),
|
"tier": dep.get("tier"),
|
||||||
"container_name": dep["container_name"],
|
"container_name": dep.get("container_name"),
|
||||||
|
**check_service(dep),
|
||||||
}
|
}
|
||||||
|
|
||||||
unhealthy_deps = [k for k, v in dep_results.items() if not v["healthy"]]
|
# Unhealthy Deps: nur die die definitiv False sind (None = intern = ignorieren)
|
||||||
|
unhealthy_deps = [k for k, v in dep_results.items() if v.get("healthy") is False]
|
||||||
|
|
||||||
# Dump-Info
|
# Dump-Info
|
||||||
dump_info = get_dump_info(service.get("dump_file"), dump_base)
|
dump_info = get_dump_info(service.get("dump_file"), dump_base)
|
||||||
@@ -189,11 +190,7 @@ def build_service_report(service_key: str, service: dict, all_services: dict, me
|
|||||||
"description": service.get("description", ""),
|
"description": service.get("description", ""),
|
||||||
"tier": service.get("tier"),
|
"tier": service.get("tier"),
|
||||||
"url": service.get("url"),
|
"url": service.get("url"),
|
||||||
"container": {
|
"status": own,
|
||||||
"name": service["container_name"],
|
|
||||||
**own_inspect,
|
|
||||||
"healthy": own_healthy,
|
|
||||||
},
|
|
||||||
"dependencies": dep_results,
|
"dependencies": dep_results,
|
||||||
"unhealthy_deps": unhealthy_deps,
|
"unhealthy_deps": unhealthy_deps,
|
||||||
"dump": dump_info,
|
"dump": dump_info,
|
||||||
@@ -203,29 +200,38 @@ def build_service_report(service_key: str, service: dict, all_services: dict, me
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_summary_report(all_services: dict, meta: dict) -> dict:
|
def build_summary_report(all_services: dict, include_tier3: bool = False) -> dict:
|
||||||
"""Prueft alle Tier-1 und Tier-2 Dienste und gibt einen Gesamtstatus zurueck."""
|
"""Prueft alle Tier-1 und Tier-2 Services (optional auch Tier-3)."""
|
||||||
results = {}
|
dump_base = find_dump_base()
|
||||||
issues = []
|
issues = []
|
||||||
|
results = {}
|
||||||
|
|
||||||
for key, svc in all_services.items():
|
for key, svc in all_services.items():
|
||||||
tier = svc.get("tier", 3)
|
tier = svc.get("tier", 3)
|
||||||
if tier > 2:
|
if not include_tier3 and tier > 2:
|
||||||
continue # Tier-3 im Summary ueberspringen
|
continue
|
||||||
|
|
||||||
|
status = check_service(svc)
|
||||||
|
healthy = status.get("healthy")
|
||||||
|
|
||||||
insp = docker_inspect(svc["container_name"])
|
|
||||||
healthy = is_healthy(insp)
|
|
||||||
results[key] = {
|
results[key] = {
|
||||||
"tier": tier,
|
"tier": tier,
|
||||||
|
"method": status["method"],
|
||||||
"healthy": healthy,
|
"healthy": healthy,
|
||||||
"status": insp["status"],
|
"status_code": status.get("status_code"),
|
||||||
"health": insp["health"],
|
"error": status.get("error"),
|
||||||
}
|
}
|
||||||
if not healthy:
|
|
||||||
issues.append({"service": key, "tier": tier, **insp})
|
|
||||||
|
|
||||||
# Dump-Checks fuer alle Dienste mit dump_file
|
# Nur echte Fehler als Issue zaehlen (None = intern, nicht pruefbar)
|
||||||
dump_base = meta.get("dump_base", "/mnt/user/backups/borg/dumps/latest")
|
if healthy is False:
|
||||||
|
issues.append({
|
||||||
|
"service": key,
|
||||||
|
"tier": tier,
|
||||||
|
"url": svc.get("url"),
|
||||||
|
**status,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Dump-Checks
|
||||||
stale_dumps = []
|
stale_dumps = []
|
||||||
for key, svc in all_services.items():
|
for key, svc in all_services.items():
|
||||||
info = get_dump_info(svc.get("dump_file"), dump_base)
|
info = get_dump_info(svc.get("dump_file"), dump_base)
|
||||||
@@ -236,13 +242,19 @@ def build_summary_report(all_services: dict, meta: dict) -> dict:
|
|||||||
"age_hours": info["age_hours"],
|
"age_hours": info["age_hours"],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
dump_available = dump_base is not None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"mode": "summary",
|
"mode": "summary",
|
||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"dump_base_found": dump_available,
|
||||||
"services_checked": len(results),
|
"services_checked": len(results),
|
||||||
"issues": issues,
|
"issues": issues,
|
||||||
"stale_dumps": stale_dumps,
|
"stale_dumps": stale_dumps,
|
||||||
"overall_healthy": len(issues) == 0 and len(stale_dumps) == 0,
|
"overall_healthy": len(issues) == 0,
|
||||||
|
"note": "Interne Services ohne URL konnten nicht geprueft werden." if any(
|
||||||
|
v["method"] == "internal" for v in results.values()
|
||||||
|
) else "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -252,60 +264,25 @@ def build_summary_report(all_services: dict, meta: dict) -> dict:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
all_services, meta = load_services()
|
all_services, _ = load_services()
|
||||||
|
|
||||||
if "--summary" in args:
|
include_all = "--all" in args
|
||||||
report = build_summary_report(all_services, meta)
|
summary_mode = "--summary" in args
|
||||||
|
|
||||||
|
if summary_mode or not args or args[0].startswith("--"):
|
||||||
|
report = build_summary_report(all_services, include_tier3=include_all)
|
||||||
print(json.dumps(report, indent=2, ensure_ascii=False))
|
print(json.dumps(report, indent=2, ensure_ascii=False))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Expliziter Service-Key als Argument
|
# Gezielter Service-Key
|
||||||
if args and not args[0].startswith("--"):
|
service_key = args[0]
|
||||||
service_key = args[0]
|
service = all_services.get(service_key)
|
||||||
service = all_services.get(service_key)
|
if not service:
|
||||||
if not service:
|
print(json.dumps({"error": f"Service '{service_key}' nicht in services.json gefunden."}))
|
||||||
print(json.dumps({"error": f"Service '{service_key}' nicht in services.yaml gefunden."}))
|
sys.exit(1)
|
||||||
sys.exit(1)
|
|
||||||
report = build_service_report(service_key, service, all_services, meta)
|
|
||||||
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
||||||
return
|
|
||||||
|
|
||||||
# Kein Argument: alle unhealthy Container automatisch finden
|
report = build_service_report(service_key, service, all_services)
|
||||||
unhealthy_names = get_unhealthy_containers()
|
print(json.dumps(report, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
if not unhealthy_names:
|
|
||||||
print(json.dumps({"status": "all_healthy", "timestamp": datetime.now().isoformat()}))
|
|
||||||
return
|
|
||||||
|
|
||||||
reports = []
|
|
||||||
for container_name in unhealthy_names:
|
|
||||||
# Container-Name auf Service-Key mappen
|
|
||||||
service_key = None
|
|
||||||
service = None
|
|
||||||
for key, svc in all_services.items():
|
|
||||||
if svc["container_name"] == container_name:
|
|
||||||
service_key = key
|
|
||||||
service = svc
|
|
||||||
break
|
|
||||||
|
|
||||||
if not service:
|
|
||||||
reports.append({
|
|
||||||
"service": container_name,
|
|
||||||
"description": "Unbekannter Container (nicht in services.yaml)",
|
|
||||||
"tier": None,
|
|
||||||
"container": {"name": container_name, "status": "unhealthy", "health": "unknown", "healthy": False},
|
|
||||||
"dependencies": {},
|
|
||||||
"unhealthy_deps": [],
|
|
||||||
"dump": None,
|
|
||||||
"first_check": "Container nicht in services.yaml — manuell pruefen",
|
|
||||||
"notes": "services.yaml aktualisieren wenn dieser Container produktiv ist",
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
|
|
||||||
reports.append(build_service_report(service_key, service, all_services, meta))
|
|
||||||
|
|
||||||
print(json.dumps(reports, indent=2, ensure_ascii=False))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,480 @@
|
|||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"dump_base": "/mnt/user/backups/borg/dumps/latest",
|
||||||
|
"appdata_base": "/mnt/user/appdata",
|
||||||
|
"secrets_path": "/mnt/user/appdata/secrets"
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"traefik": {
|
||||||
|
"description": "Zentraler Reverse Proxy, TLS, Docker-Label-Routing",
|
||||||
|
"tier": 1,
|
||||||
|
"category": "core",
|
||||||
|
"container_name": "traefik",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": "https://traefik.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/traefik/dynamic", "/mnt/user/appdata/traefik/letsencrypt"],
|
||||||
|
"first_check": "Host-Ports 80/443 erreichbar? dynamic/ korrekt auf Host synchronisiert?",
|
||||||
|
"notes": "dynamic configs werden NICHT automatisch von Komodo deployed — manueller Host-Sync noetig"
|
||||||
|
},
|
||||||
|
"adguard": {
|
||||||
|
"description": "DNS-Server / LAN DNS",
|
||||||
|
"tier": 1,
|
||||||
|
"category": "core",
|
||||||
|
"container_name": "adguard",
|
||||||
|
"dependencies": ["unbound"],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/adguard/conf", "/mnt/user/appdata/adguard/work"],
|
||||||
|
"first_check": "Port 53 erreichbar? Unbound healthy? dns_net Konnektivitaet?",
|
||||||
|
"notes": "Ports 53 und 8082 dokumentierte Host-Port-Ausnahmen"
|
||||||
|
},
|
||||||
|
"unbound": {
|
||||||
|
"description": "Upstream DNS Resolver fuer AdGuard",
|
||||||
|
"tier": 1,
|
||||||
|
"category": "core",
|
||||||
|
"container_name": "unbound",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/unbound/config"],
|
||||||
|
"first_check": "dns_net Konnektivitaet pruefen; Container-Logs auf Fehler pruefen",
|
||||||
|
"notes": "rebuildbar; isoliert in dns_net"
|
||||||
|
},
|
||||||
|
"tailscale": {
|
||||||
|
"description": "VPN / Remote-Zugang",
|
||||||
|
"tier": 1,
|
||||||
|
"category": "core",
|
||||||
|
"container_name": "tailscale",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/tailscale"],
|
||||||
|
"first_check": "Tailscale Status auf Host pruefen; State-Datei fuer Key-Renewal vorhanden?",
|
||||||
|
"notes": "network_mode: host; NET_ADMIN, NET_RAW, /dev/net/tun — dokumentierte VPN-Ausnahmen"
|
||||||
|
},
|
||||||
|
"gitea": {
|
||||||
|
"description": "Git-Server — operative Quelle der Wahrheit fuer GitOps",
|
||||||
|
"tier": 1,
|
||||||
|
"category": "core",
|
||||||
|
"container_name": "gitea",
|
||||||
|
"dependencies": ["traefik"],
|
||||||
|
"url": "https://git.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/services/gitea/data"],
|
||||||
|
"first_check": "HTTPS erreichbar? SQLite in /data intakt? SSH-Port 222 erreichbar?",
|
||||||
|
"notes": "SQLite in /data — kein separater Dump; ohne externen Mirror im DR kritisch"
|
||||||
|
},
|
||||||
|
"authelia": {
|
||||||
|
"description": "ForwardAuth — zentrale Authentifizierung fuer Admin-UIs",
|
||||||
|
"tier": 1,
|
||||||
|
"category": "security",
|
||||||
|
"container_name": "authelia",
|
||||||
|
"dependencies": ["postgresql17", "traefik"],
|
||||||
|
"url": "https://auth.kaleschke.info",
|
||||||
|
"dump_file": "postgresql17-authelia.dump",
|
||||||
|
"data_paths": ["/mnt/user/appdata/authelia/config"],
|
||||||
|
"first_check": "PostgreSQL healthy? SMTP via GMX erreichbar? Host-Config aktuell (Repo-Baseline != Host)?",
|
||||||
|
"notes": "kein Redis-Session-Backend; SMTP-Notifier GMX; Repo-Baseline muss manuell in Host-Config gemerged werden"
|
||||||
|
},
|
||||||
|
"vaultwarden": {
|
||||||
|
"description": "Passwort-Tresor",
|
||||||
|
"tier": 1,
|
||||||
|
"category": "security",
|
||||||
|
"container_name": "vaultwarden",
|
||||||
|
"dependencies": ["traefik"],
|
||||||
|
"url": "https://vault.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/vaultwarden"],
|
||||||
|
"first_check": "HTTPS erreichbar? Appdata-Volume intakt?",
|
||||||
|
"notes": "ADMIN_TOKEN_FILE; keine direkten Host-Ports"
|
||||||
|
},
|
||||||
|
"postgresql17": {
|
||||||
|
"description": "Shared PostgreSQL Cluster",
|
||||||
|
"tier": 1,
|
||||||
|
"category": "infra",
|
||||||
|
"container_name": "postgresql17",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/postgresql17"],
|
||||||
|
"first_check": "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?",
|
||||||
|
"notes": "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg"
|
||||||
|
},
|
||||||
|
"komodo-core": {
|
||||||
|
"description": "GitOps UI / API / Stack-Manager",
|
||||||
|
"tier": 1,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "komodo-core",
|
||||||
|
"dependencies": ["komodo-mongo", "gitea", "traefik"],
|
||||||
|
"url": "https://komodo.kaleschke.info",
|
||||||
|
"dump_file": "komodo-mongo.archive.gz",
|
||||||
|
"data_paths": ["/mnt/user/appdata/komodo/core"],
|
||||||
|
"first_check": "MongoDB healthy? Gitea erreichbar? komodo_net Konnektivitaet?",
|
||||||
|
"notes": "keine pauschale Authelia-ForwardAuth; Gitea DNS override konfiguriert"
|
||||||
|
},
|
||||||
|
"komodo-mongo": {
|
||||||
|
"description": "Komodo Datenbank (MongoDB)",
|
||||||
|
"tier": 1,
|
||||||
|
"category": "infra",
|
||||||
|
"container_name": "komodo-mongo",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": "komodo-mongo.archive.gz",
|
||||||
|
"data_paths": ["/mnt/user/appdata/komodo/mongo"],
|
||||||
|
"first_check": "komodo_net Konnektivitaet? Disk-Space? mongosh ping?",
|
||||||
|
"notes": "Dump-Integritaet nach Major-Upgrades pruefen"
|
||||||
|
},
|
||||||
|
"komodo-periphery": {
|
||||||
|
"description": "Komodo Host-Agent (Stack-Deployments)",
|
||||||
|
"tier": 1,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "komodo-periphery",
|
||||||
|
"dependencies": ["komodo-core"],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/komodo/periphery"],
|
||||||
|
"first_check": "Docker-Socket lesbar? /mnt/user/services gemountet? komodo_net Verbindung zu Core?",
|
||||||
|
"notes": "Docker-Socket-Ausnahme dokumentiert; /mnt/user/services Mount fuer Stack-Workspaces"
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"description": "Shared Redis Cache",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "infra",
|
||||||
|
"container_name": "redis",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/redis"],
|
||||||
|
"first_check": "backend_net Konnektivitaet? redis-cli ping erreichbar?",
|
||||||
|
"notes": "transiente Daten; bewusst nicht Backup-kritisch"
|
||||||
|
},
|
||||||
|
"paperless-ngx": {
|
||||||
|
"description": "Dokumentenmanagement",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "app",
|
||||||
|
"container_name": "paperless-ngx",
|
||||||
|
"dependencies": ["postgresql17", "redis", "traefik"],
|
||||||
|
"url": "https://paperless.kaleschke.info",
|
||||||
|
"dump_file": "postgresql17-paperless.dump",
|
||||||
|
"data_paths": [
|
||||||
|
"/mnt/user/appdata/paperless-ngx/data",
|
||||||
|
"/mnt/user/documents/paperless",
|
||||||
|
"/mnt/user/documents/scans_inbox"
|
||||||
|
],
|
||||||
|
"first_check": "Redis healthy? PostgreSQL healthy? backend_net Konnektivitaet?",
|
||||||
|
"notes": "DB/Redis Secrets als Stack ENV (keine _FILE Variante)"
|
||||||
|
},
|
||||||
|
"paperless-gpt": {
|
||||||
|
"description": "KI-Ergaenzung fuer Paperless",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "app",
|
||||||
|
"container_name": "paperless-gpt",
|
||||||
|
"dependencies": ["paperless-ngx", "traefik"],
|
||||||
|
"url": "https://paperless-gpt.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": [
|
||||||
|
"/mnt/user/appdata/paperless-gpt/data",
|
||||||
|
"/mnt/user/appdata/paperless-gpt/prompts"
|
||||||
|
],
|
||||||
|
"first_check": "Paperless API erreichbar? LLM/Ollama erreichbar? API Token gesetzt?",
|
||||||
|
"notes": "API Token als Stack ENV; abhaengig von laufendem Paperless"
|
||||||
|
},
|
||||||
|
"immich_server": {
|
||||||
|
"description": "Foto-/Video-App",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "app",
|
||||||
|
"container_name": "immich_server",
|
||||||
|
"dependencies": ["immich_postgres", "immich_redis", "immich_machine_learning", "traefik"],
|
||||||
|
"url": "https://immich.kaleschke.info",
|
||||||
|
"dump_file": "immich.dump",
|
||||||
|
"data_paths": ["/mnt/user/photos/immich", "/mnt/user/photos/family_archive"],
|
||||||
|
"first_check": "immich_postgres healthy? immich_redis healthy? ML healthy? immich_default Netz?",
|
||||||
|
"notes": "native App-Auth; externes Fotoarchiv gemountet"
|
||||||
|
},
|
||||||
|
"immich_postgres": {
|
||||||
|
"description": "Immich-Datenbank",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "infra",
|
||||||
|
"container_name": "immich_postgres",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": "immich.dump",
|
||||||
|
"data_paths": ["/mnt/user/appdata/immich_postgres"],
|
||||||
|
"first_check": "immich_default Netz? Disk-Space? pg_isready?",
|
||||||
|
"notes": "nie ins frontend_net; immich_default Netz isoliert"
|
||||||
|
},
|
||||||
|
"immich_redis": {
|
||||||
|
"description": "Immich Cache",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "infra",
|
||||||
|
"container_name": "immich_redis",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": [],
|
||||||
|
"first_check": "immich_default Netz? redis-cli ping?",
|
||||||
|
"notes": "rebuildbar; anonymes Volume — named volume als offenes TODO"
|
||||||
|
},
|
||||||
|
"immich_machine_learning": {
|
||||||
|
"description": "Immich ML (Gesichtserkennung, Suche)",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "infra",
|
||||||
|
"container_name": "immich_machine_learning",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": [],
|
||||||
|
"first_check": "immich_default Netz? model-cache Volume vorhanden?",
|
||||||
|
"notes": "rebuildbar; intern-only"
|
||||||
|
},
|
||||||
|
"mealie": {
|
||||||
|
"description": "Rezeptverwaltung",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "app",
|
||||||
|
"container_name": "mealie",
|
||||||
|
"dependencies": ["mealie-postgres", "traefik"],
|
||||||
|
"url": "https://mealie.kaleschke.info",
|
||||||
|
"dump_file": "mealie.dump",
|
||||||
|
"data_paths": ["/mnt/user/appdata/mealie/data"],
|
||||||
|
"first_check": "mealie-postgres healthy? mealie_internal Netz erreichbar?",
|
||||||
|
"notes": "App + DB in internem Netz getrennt (mealie_internal)"
|
||||||
|
},
|
||||||
|
"mealie-postgres": {
|
||||||
|
"description": "Mealie-Datenbank",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "infra",
|
||||||
|
"container_name": "mealie-postgres",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": "mealie.dump",
|
||||||
|
"data_paths": ["/mnt/user/appdata/mealie/postgres"],
|
||||||
|
"first_check": "mealie_internal Netz? Disk-Space?",
|
||||||
|
"notes": "interne DB; mealie_internal Netz"
|
||||||
|
},
|
||||||
|
"mail-archiver": {
|
||||||
|
"description": "Mail-Archivierung (IMAP)",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "app",
|
||||||
|
"container_name": "mail-archiver",
|
||||||
|
"dependencies": ["postgresql17", "authelia", "traefik"],
|
||||||
|
"url": "https://mail.kaleschke.info",
|
||||||
|
"dump_file": "postgresql17-mailarchiver.dump",
|
||||||
|
"data_paths": ["/mnt/user/appdata/mailarchiver/data-protection-keys"],
|
||||||
|
"first_check": "PostgreSQL healthy? Internet-/IMAP-Zugang? Authelia healthy?",
|
||||||
|
"notes": "Hybrid: frontend_net fuer IMAP/Internet, backend_net fuer DB"
|
||||||
|
},
|
||||||
|
"nextcloud": {
|
||||||
|
"description": "Datei-/Cloud-Dienst",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "app",
|
||||||
|
"container_name": "nextcloud",
|
||||||
|
"dependencies": ["nextcloud-postgres", "nextcloud-redis", "traefik"],
|
||||||
|
"url": "https://cloud.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": [
|
||||||
|
"/mnt/user/appdata/nextcloud/html",
|
||||||
|
"/mnt/user/documents/nextcloud-data"
|
||||||
|
],
|
||||||
|
"first_check": "nextcloud-postgres healthy? nextcloud-redis healthy? nextcloud_internal Netz?",
|
||||||
|
"notes": "native App-Auth (kein zentrales ForwardAuth); WebDAV/CardDAV beachten"
|
||||||
|
},
|
||||||
|
"nextcloud-postgres": {
|
||||||
|
"description": "Nextcloud-Datenbank",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "infra",
|
||||||
|
"container_name": "nextcloud-postgres",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/nextcloud/postgres"],
|
||||||
|
"first_check": "nextcloud_internal Netz? Disk-Space?",
|
||||||
|
"notes": "interne DB"
|
||||||
|
},
|
||||||
|
"nextcloud-redis": {
|
||||||
|
"description": "Nextcloud Cache / Locking",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "infra",
|
||||||
|
"container_name": "nextcloud-redis",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/nextcloud/redis"],
|
||||||
|
"first_check": "nextcloud_internal Netz? redis-cli ping?",
|
||||||
|
"notes": "rebuildbar"
|
||||||
|
},
|
||||||
|
"ntfy": {
|
||||||
|
"description": "Push-Benachrichtigungen (Alert-Backbone)",
|
||||||
|
"tier": 2,
|
||||||
|
"category": "app",
|
||||||
|
"container_name": "ntfy",
|
||||||
|
"dependencies": ["traefik"],
|
||||||
|
"url": "https://ntfy.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/ntfy"],
|
||||||
|
"first_check": "HTTPS erreichbar? NTFY_BEHIND_PROXY=true gesetzt? Traefik healthy?",
|
||||||
|
"notes": "KRITISCH: Ausfall bedeutet keine anderen Alerts ankommen"
|
||||||
|
},
|
||||||
|
"glance": {
|
||||||
|
"description": "Homelab-Dashboard",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "glance",
|
||||||
|
"dependencies": ["traefik"],
|
||||||
|
"url": "https://glance.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": [],
|
||||||
|
"first_check": "Traefik erreichbar? Docker-Socket-Proxy intern erreichbar? API-Tokens fuer Widgets gueltig?",
|
||||||
|
"notes": "aktives Homelab-Dashboard; Homepage wurde entfernt"
|
||||||
|
},
|
||||||
|
"monitoring-grafana": {
|
||||||
|
"description": "Zentrale Observability-UI",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "monitoring-grafana",
|
||||||
|
"dependencies": [
|
||||||
|
"monitoring-prometheus",
|
||||||
|
"monitoring-loki",
|
||||||
|
"monitoring-influxdb3-core",
|
||||||
|
"traefik"
|
||||||
|
],
|
||||||
|
"url": "https://monitoring.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["grafana_data"],
|
||||||
|
"first_check": "Authelia-Redirect? Datasources Prometheus, Loki und InfluxDB 3 Core gruen?",
|
||||||
|
"notes": "ersetzt alten Grafana-Altstand und Uptime-Kuma-Views"
|
||||||
|
},
|
||||||
|
"monitoring-influxdb3-core": {
|
||||||
|
"description": "Zeitreihen- / Metrikdaten fuer Monitoring und Home Assistant",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "monitoring-influxdb3-core",
|
||||||
|
"dependencies": ["monitoring-grafana"],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": [
|
||||||
|
"/mnt/user/appdata/influxdb3/data",
|
||||||
|
"/mnt/user/appdata/influxdb3/plugins"
|
||||||
|
],
|
||||||
|
"first_check": "LAN-Port 8181 erreichbar? 401 ohne Token = OK (erwartet). Disk-Space?",
|
||||||
|
"notes": "LAN-only Host-Port 8181; kein frontend_net; laeuft als user 0"
|
||||||
|
},
|
||||||
|
"scrutiny": {
|
||||||
|
"description": "Laufwerks- / SMART-Monitoring",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "scrutiny",
|
||||||
|
"dependencies": ["traefik"],
|
||||||
|
"url": "https://scrutiny.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": [
|
||||||
|
"/mnt/user/appdata/scrutiny/config",
|
||||||
|
"/mnt/user/appdata/scrutiny/influxdb"
|
||||||
|
],
|
||||||
|
"first_check": "Device-Mounts vorhanden? privileged=true gesetzt? Traefik erreichbar?",
|
||||||
|
"notes": "privileged: true dokumentierte Ausnahme"
|
||||||
|
},
|
||||||
|
"glances": {
|
||||||
|
"description": "System- / Container-Monitoring",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "glances",
|
||||||
|
"dependencies": ["traefik"],
|
||||||
|
"url": "https://glances.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": [],
|
||||||
|
"first_check": "Docker-Socket lesbar? rootfs gemountet? Traefik erreichbar?",
|
||||||
|
"notes": "rebuildbar; Docker-Socket und rootfs Mounts"
|
||||||
|
},
|
||||||
|
"borg-ui": {
|
||||||
|
"description": "Borg Backup- / Restore UI",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "borg-ui",
|
||||||
|
"dependencies": ["traefik"],
|
||||||
|
"url": "https://borg.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": [
|
||||||
|
"/mnt/user/appdata/borg-ui/data",
|
||||||
|
"/mnt/user/backups/borg/dumps"
|
||||||
|
],
|
||||||
|
"first_check": "Borg-Repo-Credentials vorhanden? Backup-Mounts erreichbar? Traefik healthy?",
|
||||||
|
"notes": "breite Mounts bewusst dokumentiert; /local/secrets im DR-Scope"
|
||||||
|
},
|
||||||
|
"hermes-gateway": {
|
||||||
|
"description": "Hermes Agent Gateway / AI Ops Assistant",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "hermes-gateway",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/hermes-agent/data"],
|
||||||
|
"first_check": "hermes_net:8642/health erreichbar? SSH-Key gemountet? LLM-Provider erreichbar?",
|
||||||
|
"notes": "kein Docker-Socket; SSH terminal backend; echte .env auf Host-Appdata"
|
||||||
|
},
|
||||||
|
"ddns-updater": {
|
||||||
|
"description": "Cloudflare / DDNS Aktualisierung",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "infra",
|
||||||
|
"container_name": "ddns-updater",
|
||||||
|
"dependencies": [],
|
||||||
|
"url": null,
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/ddns-updater"],
|
||||||
|
"first_check": "Internetzugang? Cloudflare API erreichbar? Config vorhanden?",
|
||||||
|
"notes": "bewusst in frontend_net weil backend_net internal ist"
|
||||||
|
},
|
||||||
|
"code-server": {
|
||||||
|
"description": "Web-Editor / Operations Workspace",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "code-server",
|
||||||
|
"dependencies": ["traefik"],
|
||||||
|
"url": "https://code.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": [
|
||||||
|
"/mnt/user/appdata/code-server",
|
||||||
|
"/mnt/user/services/dev"
|
||||||
|
],
|
||||||
|
"first_check": "Traefik erreichbar? PASSWORD_FILE lesbar?",
|
||||||
|
"notes": "PASSWORD_FILE; Workspaces bei Restore beachten"
|
||||||
|
},
|
||||||
|
"filebrowser": {
|
||||||
|
"description": "Datei-Browser fuer Appdata",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "filebrowser",
|
||||||
|
"dependencies": ["traefik"],
|
||||||
|
"url": "https://files.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/filebrowser"],
|
||||||
|
"first_check": "Appdata-Mounts erreichbar? Traefik healthy?",
|
||||||
|
"notes": "breiter /mnt/user/appdata Mount; Einschraenkung langfristig als TODO"
|
||||||
|
},
|
||||||
|
"speedtest-tracker": {
|
||||||
|
"description": "Speedtest-Monitoring",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "ops",
|
||||||
|
"container_name": "speedtest-tracker",
|
||||||
|
"dependencies": ["traefik"],
|
||||||
|
"url": "https://speedtest.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": ["/mnt/user/appdata/speedtest-tracker/config"],
|
||||||
|
"first_check": "APP_KEY gesetzt? Internetzugang fuer Speedtest vorhanden?",
|
||||||
|
"notes": "APP_KEY, ADMIN_PASSWORD als Stack ENV"
|
||||||
|
},
|
||||||
|
"bentopdf": {
|
||||||
|
"description": "PDF-Tooling",
|
||||||
|
"tier": 3,
|
||||||
|
"category": "app",
|
||||||
|
"container_name": "bentopdf",
|
||||||
|
"dependencies": ["traefik"],
|
||||||
|
"url": "https://pdf.kaleschke.info",
|
||||||
|
"dump_file": null,
|
||||||
|
"data_paths": [],
|
||||||
|
"first_check": "COOP/COEP Middleware gesetzt? Traefik healthy?",
|
||||||
|
"notes": "rebuildbar; keine kritische Persistenz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -395,55 +395,43 @@ services:
|
|||||||
# TIER 3 — Ops / Tools (Ausfall schmerzt, blockiert nichts Kritisches)
|
# TIER 3 — Ops / Tools (Ausfall schmerzt, blockiert nichts Kritisches)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
homepage:
|
glance:
|
||||||
description: Start-Dashboard
|
description: Homelab-Dashboard
|
||||||
tier: 3
|
tier: 3
|
||||||
category: ops
|
category: ops
|
||||||
container_name: homepage
|
container_name: glance
|
||||||
dependencies:
|
dependencies:
|
||||||
- traefik
|
- traefik
|
||||||
url: https://home.kaleschke.info
|
url: https://glance.kaleschke.info
|
||||||
dump_file: null
|
dump_file: null
|
||||||
data_paths:
|
data_paths: []
|
||||||
- /mnt/user/appdata/homepage
|
first_check: "Traefik erreichbar? Docker-Socket-Proxy intern erreichbar? API-Tokens fuer Widgets gueltig?"
|
||||||
first_check: "Traefik erreichbar? Docker-Socket read-only lesbar? API-Tokens gueltig?"
|
notes: "aktives Homelab-Dashboard; Homepage wurde entfernt"
|
||||||
notes: "Docker socket read-only; viele API Tokens in Config"
|
|
||||||
|
|
||||||
uptime-kuma:
|
monitoring-grafana:
|
||||||
description: Monitoring / Uptime Checks
|
description: Zentrale Observability-UI
|
||||||
tier: 3
|
tier: 3
|
||||||
category: ops
|
category: ops
|
||||||
container_name: UptimeKuma
|
container_name: monitoring-grafana
|
||||||
dependencies:
|
dependencies:
|
||||||
|
- monitoring-prometheus
|
||||||
|
- monitoring-loki
|
||||||
|
- monitoring-influxdb3-core
|
||||||
- traefik
|
- traefik
|
||||||
url: https://uptime.kaleschke.info
|
url: https://monitoring.kaleschke.info
|
||||||
dump_file: null
|
dump_file: null
|
||||||
data_paths:
|
data_paths:
|
||||||
- /mnt/user/appdata/uptime-kuma
|
- grafana_data
|
||||||
first_check: "Datenbank-Volume intakt? Traefik erreichbar?"
|
first_check: "Authelia-Redirect? Datasources Prometheus, Loki und InfluxDB 3 Core gruen?"
|
||||||
notes: "Monitore nach Restore manuell pruefen"
|
notes: "ersetzt alten Grafana-Altstand und Uptime-Kuma-Views"
|
||||||
|
|
||||||
grafana:
|
monitoring-influxdb3-core:
|
||||||
description: Metrik-Dashboard
|
description: Zeitreihen- / Metrikdaten fuer Monitoring und Home Assistant
|
||||||
tier: 3
|
tier: 3
|
||||||
category: ops
|
category: ops
|
||||||
container_name: grafana
|
container_name: monitoring-influxdb3-core
|
||||||
dependencies:
|
dependencies:
|
||||||
- influxdb3-core
|
- monitoring-grafana
|
||||||
- traefik
|
|
||||||
url: https://grafana.kaleschke.info
|
|
||||||
dump_file: null
|
|
||||||
data_paths:
|
|
||||||
- /mnt/user/appdata/grafana
|
|
||||||
first_check: "influxdb3-core healthy? Datasource-Token in Secret gesetzt? Provisioning-Konfig vorhanden?"
|
|
||||||
notes: "laeuft als user 0 wegen Host-Appdata-Permissions (dokumentiert); Datasource wird provisioniert"
|
|
||||||
|
|
||||||
influxdb3-core:
|
|
||||||
description: Zeitreihen- / Metrikdaten fuer Grafana und Home Assistant
|
|
||||||
tier: 3
|
|
||||||
category: ops
|
|
||||||
container_name: influxdb3-core
|
|
||||||
dependencies: []
|
|
||||||
url: null
|
url: null
|
||||||
dump_file: null
|
dump_file: null
|
||||||
data_paths:
|
data_paths:
|
||||||
@@ -495,20 +483,6 @@ services:
|
|||||||
first_check: "Borg-Repo-Credentials vorhanden? Backup-Mounts erreichbar? Traefik healthy?"
|
first_check: "Borg-Repo-Credentials vorhanden? Backup-Mounts erreichbar? Traefik healthy?"
|
||||||
notes: "breite Mounts bewusst dokumentiert; /local/secrets im DR-Scope"
|
notes: "breite Mounts bewusst dokumentiert; /local/secrets im DR-Scope"
|
||||||
|
|
||||||
backrest:
|
|
||||||
description: Backup-Admin-Dienst (Legacy-Backup-Ebene)
|
|
||||||
tier: 3
|
|
||||||
category: ops
|
|
||||||
container_name: backrest
|
|
||||||
dependencies:
|
|
||||||
- traefik
|
|
||||||
url: https://backrest.kaleschke.info
|
|
||||||
dump_file: null
|
|
||||||
data_paths:
|
|
||||||
- /mnt/user/appdata/backrest
|
|
||||||
first_check: "Repo/SSH-Mounts erreichbar? Traefik healthy?"
|
|
||||||
notes: "breite Mounts bewusst dokumentiert"
|
|
||||||
|
|
||||||
hermes-gateway:
|
hermes-gateway:
|
||||||
description: Hermes Agent Gateway / AI Ops Assistant
|
description: Hermes Agent Gateway / AI Ops Assistant
|
||||||
tier: 3
|
tier: 3
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: homelab-ops-monitor
|
||||||
|
version: 1.0.0
|
||||||
|
author: Hermes Agent + Micha
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [homelab, nas, gitops, operations, monitoring, health-checks, alerts, ntfy]
|
||||||
|
description: |
|
||||||
|
Kontextueller Ops-Assistent fuer das Kallilabcore-Homelab.
|
||||||
|
Prueft Service-Health via HTTP, liest Backup-Dump-Timestamps,
|
||||||
|
analysiert Abhaengigkeiten und sendet angereicherte ntfy-Alerts.
|
||||||
|
Kein Docker CLI noetig — laeuft direkt auf der Hermes-VM.
|
||||||
|
---
|
||||||
|
|
||||||
# Skill: homelab-ops-monitor
|
# Skill: homelab-ops-monitor
|
||||||
|
|
||||||
## Zweck
|
## Zweck
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ powershell -ExecutionPolicy Bypass -File .\ops\policy-checks\check_repo.ps1 -Rep
|
|||||||
- Host-Port-Mappings
|
- Host-Port-Mappings
|
||||||
- Traefik-Router mit `Host(...)` und Middleware-Standard fuer geschuetzte Admin-/Ops-Dienste
|
- Traefik-Router mit `Host(...)` und Middleware-Standard fuer geschuetzte Admin-/Ops-Dienste
|
||||||
- sichtbare Report-Punkte fuer dokumentierte Ausnahmen wie `user: "0"`, `privileged: true` oder `network_mode: host`
|
- sichtbare Report-Punkte fuer dokumentierte Ausnahmen wie `user: "0"`, `privileged: true` oder `network_mode: host`
|
||||||
|
- digest-gepinnte mutable Tags bleiben sichtbar; nur explizit dokumentierte Ausnahmen werden als Info statt Warning gewertet
|
||||||
|
|
||||||
## Wichtige Betriebsregel
|
## Wichtige Betriebsregel
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,14 @@ function Test-ServicePolicies {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($service.Image -match ':[Ll]atest(?:[-@]|$)') {
|
||||||
|
if (($service.Image -match '@sha256:') -and (Test-IdentityMatch -Service $service -Candidates $Exceptions.allowed_mutable_tag_identities)) {
|
||||||
|
Add-Finding -Findings $Findings -Severity 'info' -Code 'IMAGE002' -Target $targetBase -Message 'Image uses a latest tag but is digest-pinned and documented as an exception.'
|
||||||
|
} else {
|
||||||
|
Add-Finding -Findings $Findings -Severity 'warning' -Code 'IMAGE001' -Target $targetBase -Message 'Image uses a latest tag. Prefer a concrete version tag, even when a digest is present.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$isDataService = $false
|
$isDataService = $false
|
||||||
$identityText = ($service.ServiceName + ' ' + $service.ContainerName + ' ' + $service.Image).ToLowerInvariant()
|
$identityText = ($service.ServiceName + ' ' + $service.ContainerName + ' ' + $service.Image).ToLowerInvariant()
|
||||||
foreach ($needle in @('postgres', 'redis', 'mongo', 'mysql', 'mariadb', 'influxdb')) {
|
foreach ($needle in @('postgres', 'redis', 'mongo', 'mysql', 'mariadb', 'influxdb')) {
|
||||||
@@ -358,6 +366,7 @@ $exceptionsRaw = Get-Content -LiteralPath $exceptionsPath -Raw | ConvertFrom-Jso
|
|||||||
$exceptions = @{
|
$exceptions = @{
|
||||||
middleware_exempt_identities = @($exceptionsRaw.middleware_exempt_identities)
|
middleware_exempt_identities = @($exceptionsRaw.middleware_exempt_identities)
|
||||||
allowed_root_identities = @($exceptionsRaw.allowed_root_identities)
|
allowed_root_identities = @($exceptionsRaw.allowed_root_identities)
|
||||||
|
allowed_mutable_tag_identities = @($exceptionsRaw.allowed_mutable_tag_identities)
|
||||||
allowed_privileged_identities = @($exceptionsRaw.allowed_privileged_identities)
|
allowed_privileged_identities = @($exceptionsRaw.allowed_privileged_identities)
|
||||||
allowed_host_network_identities = @($exceptionsRaw.allowed_host_network_identities)
|
allowed_host_network_identities = @($exceptionsRaw.allowed_host_network_identities)
|
||||||
allowed_host_port_identities = @{}
|
allowed_host_port_identities = @{}
|
||||||
|
|||||||
@@ -16,12 +16,12 @@
|
|||||||
"adguard": [
|
"adguard": [
|
||||||
"53:53/tcp",
|
"53:53/tcp",
|
||||||
"53:53/udp",
|
"53:53/udp",
|
||||||
"8082:80"
|
"100.80.98.33:8082:80"
|
||||||
],
|
],
|
||||||
"gitea": [
|
"gitea": [
|
||||||
"222:22"
|
"222:22"
|
||||||
],
|
],
|
||||||
"influxdb3-core": [
|
"monitoring-influxdb3-core": [
|
||||||
"${INFLUXDB_BIND_IP:-127.0.0.1}:8181:8181"
|
"${INFLUXDB_BIND_IP:-127.0.0.1}:8181:8181"
|
||||||
],
|
],
|
||||||
"traefik": [
|
"traefik": [
|
||||||
@@ -30,13 +30,18 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"allowed_root_identities": [
|
"allowed_root_identities": [
|
||||||
"grafana",
|
"monitoring-influxdb3-core"
|
||||||
"influxdb3-core"
|
],
|
||||||
|
"allowed_mutable_tag_identities": [
|
||||||
|
"ddns-updater",
|
||||||
|
"glances",
|
||||||
|
"scrutiny"
|
||||||
],
|
],
|
||||||
"allowed_privileged_identities": [
|
"allowed_privileged_identities": [
|
||||||
"scrutiny"
|
"scrutiny"
|
||||||
],
|
],
|
||||||
"allowed_host_network_identities": [
|
"allowed_host_network_identities": [
|
||||||
|
"plex",
|
||||||
"tailscale",
|
"tailscale",
|
||||||
"Tailscale-Docker"
|
"Tailscale-Docker"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
# Policy Check Report
|
# Policy Check Report
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
- Compose files checked: 30
|
- Compose files checked: 29
|
||||||
- Critical findings: 0
|
- Critical findings: 0
|
||||||
- Warnings: 5
|
- Warnings: 1
|
||||||
- Info findings: 9
|
- Info findings: 13
|
||||||
|
|
||||||
## Critical
|
## Critical
|
||||||
- none
|
- none
|
||||||
|
|
||||||
## Warnings
|
## Warnings
|
||||||
- [SEC001] infra\ddns-updater\docker-compose.yml :: ddns-updater: Missing security_opt no-new-privileges:true.
|
- [USER001] monitoring\docker-compose.yml :: influxdb3-core: Runs as user 0. Documented exception, keep visible for hardening.
|
||||||
- [SEC001] ops\backrest\docker-compose.yml :: backrest: Missing security_opt no-new-privileges:true.
|
|
||||||
- [USER001] ops\grafana-influxdb\docker-compose.yml :: grafana: Runs as user 0. Documented exception, keep visible for hardening.
|
|
||||||
- [USER001] ops\grafana-influxdb\docker-compose.yml :: influxdb3-core: Runs as user 0. Documented exception, keep visible for hardening.
|
|
||||||
- [SEC001] ops\scrutiny\docker-compose.yml :: scrutiny: Missing security_opt no-new-privileges:true.
|
|
||||||
|
|
||||||
## Info
|
## Info
|
||||||
- [PORT001] core\gitea\docker-compose.yml :: gitea: Allowed host port mapping: 222:22
|
- [PORT001] core\gitea\docker-compose.yml :: gitea: Allowed host port mapping: 222:22
|
||||||
- [PORT001] host-services\Adguard\docker-compose.yml :: adguard: Allowed host port mapping: 53:53/tcp
|
- [PORT001] host-services\Adguard\docker-compose.yml :: adguard: Allowed host port mapping: 53:53/tcp
|
||||||
- [PORT001] host-services\Adguard\docker-compose.yml :: adguard: Allowed host port mapping: 53:53/udp
|
- [PORT001] host-services\Adguard\docker-compose.yml :: adguard: Allowed host port mapping: 53:53/udp
|
||||||
- [PORT001] host-services\Adguard\docker-compose.yml :: adguard: Allowed host port mapping: 8082:80
|
- [PORT001] host-services\Adguard\docker-compose.yml :: adguard: Allowed host port mapping: 100.80.98.33:8082:80
|
||||||
|
- [HOSTNET001] host-services\plex\docker-compose.yml :: plex: network_mode: host is a documented exception.
|
||||||
- [HOSTNET001] host-services\tailscale\docker-compose.yml :: tailscale: network_mode: host is a documented exception.
|
- [HOSTNET001] host-services\tailscale\docker-compose.yml :: tailscale: network_mode: host is a documented exception.
|
||||||
- [PORT001] ops\grafana-influxdb\docker-compose.yml :: influxdb3-core: Allowed host port mapping: ${INFLUXDB_BIND_IP:-127.0.0.1}:8181:8181
|
- [IMAGE002] infra\ddns-updater\docker-compose.yml :: ddns-updater: Image uses a latest tag but is digest-pinned and documented as an exception.
|
||||||
|
- [PORT001] monitoring\docker-compose.yml :: influxdb3-core: Allowed host port mapping: ${INFLUXDB_BIND_IP:-127.0.0.1}:8181:8181
|
||||||
|
- [IMAGE002] ops\glances\docker-compose.yml :: glances: Image uses a latest tag but is digest-pinned and documented as an exception.
|
||||||
|
- [IMAGE002] ops\scrutiny\docker-compose.yml :: scrutiny: Image uses a latest tag but is digest-pinned and documented as an exception.
|
||||||
- [PRIV001] ops\scrutiny\docker-compose.yml :: scrutiny: Privileged mode is a documented exception.
|
- [PRIV001] ops\scrutiny\docker-compose.yml :: scrutiny: Privileged mode is a documented exception.
|
||||||
- [PORT001] traefik\docker-compose.yml :: traefik: Allowed host port mapping: 80:80
|
- [PORT001] traefik\docker-compose.yml :: traefik: Allowed host port mapping: 80:80
|
||||||
- [PORT001] traefik\docker-compose.yml :: traefik: Allowed host port mapping: 443:443
|
- [PORT001] traefik\docker-compose.yml :: traefik: Allowed host port mapping: 443:443
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Restore Tests
|
||||||
|
|
||||||
|
Kontrollierte Restore-Tests fuer `homelab-infra`.
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
|
||||||
|
- Backups durch echte Test-Restores verifizieren
|
||||||
|
- produktive Pfade nicht beschreiben
|
||||||
|
- Testlaeufe spaeter weitgehend automatisieren
|
||||||
|
|
||||||
|
## Grundregeln
|
||||||
|
|
||||||
|
- Restore-Quelle bleibt im Backup-Bereich, z. B. `/mnt/user/backups/borg`
|
||||||
|
- Test-Restores laufen nur in `/mnt/user/backups/restore-lab`
|
||||||
|
- Reports landen in `/mnt/user/backups/restore-reports`
|
||||||
|
- Test-Container nutzen das Praefix `restoretest-`
|
||||||
|
- keine produktiven Volumes schreibend mounten
|
||||||
|
- keine produktiven Domains fuer Testinstanzen uebernehmen
|
||||||
|
|
||||||
|
## Geplante Struktur
|
||||||
|
|
||||||
|
- `schedule.md`: Intervalle und Verantwortlichkeiten
|
||||||
|
- `vaultwarden-restore-test.ps1`: erster Mini-Restore-Ablauf
|
||||||
|
- `vaultwarden-restore-test.sh`: hosttauglicher Vaultwarden-Restore-Job
|
||||||
|
- `vaultwarden-plan.md`: konkreter Vaultwarden-Testplan
|
||||||
|
- `vaultwarden-compose.test.yml`: isolierte Testinstanz fuer Vaultwarden
|
||||||
|
- `gitea-restore-test.ps1`: Gitea-Mini-Restore-Ablauf
|
||||||
|
- `gitea-restore-test.sh`: hosttauglicher Gitea-Restore-Job
|
||||||
|
- `gitea-plan.md`: konkreter Gitea-Testplan
|
||||||
|
- `gitea-compose.test.yml`: isolierte Testinstanz fuer Gitea
|
||||||
|
- `paperless-restore-test.ps1`: Paperless-Mini-Restore-Ablauf
|
||||||
|
- `paperless-restore-test.sh`: hosttauglicher Paperless-Restore-Job
|
||||||
|
- `paperless-plan.md`: konkreter Paperless-Testplan
|
||||||
|
- `paperless-compose.test.yml`: isolierte Testinstanz fuer Paperless inkl. Test-Postgres und Test-Redis
|
||||||
|
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
|
||||||
|
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
||||||
|
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
|
||||||
|
- `run-restore-checks.sh`: hosttauglicher Dispatcher
|
||||||
|
- `common.sh`: gemeinsame Host-Helferfunktionen
|
||||||
|
- `automation-plan.md`: Host-Job- und Automatisierungsmodell
|
||||||
|
|
||||||
|
## Automatisierungsmodell
|
||||||
|
|
||||||
|
- Ausfuehrung: Unraid User Script / Host-Job
|
||||||
|
- Logik: Repo-Skripte in diesem Verzeichnis
|
||||||
|
- Ergebnis: Markdown-Report
|
||||||
|
- Meldung: `ntfy`
|
||||||
|
- Hermes: optional nur fuer Zusammenfassung und Auswertung
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
|
||||||
|
- die Bash-Skripte `*.sh` sind die produktive Host-Variante
|
||||||
|
- `check-restore-freshness.ps1` und die `*.ps1`-Dateien bleiben als lokale Plan-/Hilfsvariante nutzbar
|
||||||
|
- im Windows-Clone fehlen die `/mnt/user/...`-Pfade naturgemaess
|
||||||
|
|
||||||
|
## Validiertes Grundmuster
|
||||||
|
|
||||||
|
Stand nach dem ersten echten Vaultwarden-Test:
|
||||||
|
|
||||||
|
- Borg-Quelle bleibt das produktive Remote-Repo bei Hetzner
|
||||||
|
- Borg-Zugriff laeuft praktisch ueber den vorhandenen `borg-ui`-Container
|
||||||
|
- SSH-Trust wird ueber `known_hosts` im `borg-ui`-Container hergestellt
|
||||||
|
- die Borg-Passphrase kommt fuer Restore-Tests aus einer Host-Secret-Datei
|
||||||
|
- Restore-Ziel liegt immer getrennt unter `/mnt/user/backups/restore-lab`
|
||||||
|
- Reports liegen unter `/mnt/user/backups/restore-reports`
|
||||||
|
- Testinstanzen bekommen keine produktive Domain und keine Traefik-Route
|
||||||
|
|
||||||
|
Das ist das bevorzugte Muster fuer weitere dateibasierte Restore-Tests wie `gitea`.
|
||||||
|
|
||||||
|
Fuer datenbankgestuetzte Dienste wie `paperless` kommt zusaetzlich ein isolierter Dump-Restore in Test-Postgres dazu.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Aktuell ist das erste validierte Muster vorhanden.
|
||||||
|
|
||||||
|
- echter Vaultwarden-Restore am 2026-05-07 erfolgreich verifiziert
|
||||||
|
- echter Gitea-Restore am 2026-05-07 erfolgreich verifiziert
|
||||||
|
- echter Paperless-Restore am 2026-05-07 erfolgreich verifiziert
|
||||||
|
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
|
||||||
|
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
||||||
|
- V1-Ablauf weiter ohne `ntfy`, mit Bereinigung nach Erfolg
|
||||||
|
- naechster grosser Kandidat ist ein weiterer datenbankgestuetzter Dienst oder die Automatisierung
|
||||||
|
|
||||||
|
Vor dem ersten echten Testlauf muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Restore Automation Plan
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Die bereits validierten Restore-Tests fuer `vaultwarden`, `gitea` und `paperless` sollen regelmaessig mit wenig Handarbeit laufen.
|
||||||
|
|
||||||
|
## Prinzip
|
||||||
|
|
||||||
|
- Ausfuehrung bleibt hostseitig
|
||||||
|
- Logik bleibt im Repo
|
||||||
|
- Reports bleiben unter `/mnt/user/backups/restore-reports`
|
||||||
|
- Restore-Arbeitsdaten bleiben unter `/mnt/user/backups/restore-lab`
|
||||||
|
- Hermes ist Reporter, nicht Operator
|
||||||
|
|
||||||
|
## V1
|
||||||
|
|
||||||
|
### Woechentlicher Frische-Check
|
||||||
|
|
||||||
|
- Script: `check-restore-freshness.sh`
|
||||||
|
- Ziel:
|
||||||
|
- Dump-Dateien vorhanden
|
||||||
|
- Dump-Dateien nicht zu alt
|
||||||
|
- letzte Restore-Reports vorhanden
|
||||||
|
- Wirkung:
|
||||||
|
- schneller Fruehwarncheck ohne Containerstart
|
||||||
|
|
||||||
|
### Monatliche / zweimonatliche Restore-Jobs
|
||||||
|
|
||||||
|
- Script-Dispatcher: `run-restore-checks.sh`
|
||||||
|
- Modi:
|
||||||
|
- `freshness`
|
||||||
|
- `vaultwarden`
|
||||||
|
- `gitea`
|
||||||
|
- `paperless`
|
||||||
|
- diese Bash-Jobs sind jetzt hostseitig praktisch verifiziert
|
||||||
|
- die `*.ps1`-Dateien bleiben als Plan-/Hilfsvariante fuer die Windows-Arbeitskopie erhalten
|
||||||
|
|
||||||
|
## V2
|
||||||
|
|
||||||
|
- `ntfy` Erfolg/Fehler
|
||||||
|
- optional Hermes-Zusammenfassung ueber vorhandene Reports
|
||||||
|
- spaeter Job-Metadaten, Rotation und Sammel-Reports weiter ausbauen
|
||||||
|
|
||||||
|
### `ntfy`-Muster
|
||||||
|
|
||||||
|
- Script: `run-restore-job-with-ntfy.sh`
|
||||||
|
- Hilfsskript: `send-ntfy.sh`
|
||||||
|
- nur kurze Erfolg/Fehler-Meldung
|
||||||
|
- eigentlicher Detailnachweis bleibt die Markdown-Reportdatei
|
||||||
|
|
||||||
|
## Host-Integration
|
||||||
|
|
||||||
|
Empfohlen:
|
||||||
|
|
||||||
|
- Unraid User Scripts
|
||||||
|
- je ein geplanter Job pro Laufklasse
|
||||||
|
- Ausfuehrung auf dem Unraid-Host, nicht im Windows-Clone
|
||||||
|
|
||||||
|
Beispiel:
|
||||||
|
|
||||||
|
1. `restore-freshness-weekly`
|
||||||
|
2. `restore-vaultwarden-monthly`
|
||||||
|
3. `restore-gitea-monthly`
|
||||||
|
4. `restore-paperless-bimonthly`
|
||||||
|
|
||||||
|
## Erfolgskriterium
|
||||||
|
|
||||||
|
Ein automatisierter Lauf ist nur dann erfolgreich, wenn:
|
||||||
|
|
||||||
|
- Script sauber endet
|
||||||
|
- Report geschrieben wird
|
||||||
|
- bei echten Restore-Laeufen der definierte Smoke-Test erfolgreich war
|
||||||
|
|
||||||
|
## Nicht automatisieren
|
||||||
|
|
||||||
|
- neue Restore-Typen ohne bewusste Freigabe
|
||||||
|
- invasive Produktiv-Restores
|
||||||
|
- Komodo-/Auth-/Secret-Umbauten im selben Job
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
param(
|
||||||
|
[string]$DumpRoot = "/mnt/user/backups/borg/dumps/latest",
|
||||||
|
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
|
||||||
|
[int]$MaxDumpAgeHours = 26,
|
||||||
|
[int]$MaxReportAgeDays = 45
|
||||||
|
)
|
||||||
|
|
||||||
|
$checks = @(
|
||||||
|
@{ Name = "postgresql17-paperless.dump"; Path = Join-Path $DumpRoot "postgresql17-paperless.dump" },
|
||||||
|
@{ Name = "postgresql17-mailarchiver.dump"; Path = Join-Path $DumpRoot "postgresql17-mailarchiver.dump" },
|
||||||
|
@{ Name = "mealie.dump"; Path = Join-Path $DumpRoot "mealie.dump" },
|
||||||
|
@{ Name = "immich.dump"; Path = Join-Path $DumpRoot "immich.dump" },
|
||||||
|
@{ Name = "nextcloud.dump"; Path = Join-Path $DumpRoot "nextcloud.dump" },
|
||||||
|
@{ Name = "gitea.sqlite.dump"; Path = Join-Path $DumpRoot "gitea.sqlite.dump" },
|
||||||
|
@{ Name = "vaultwarden.sqlite.dump"; Path = Join-Path $DumpRoot "vaultwarden.sqlite.dump" },
|
||||||
|
@{ Name = "speedtest-tracker.sqlite.dump"; Path = Join-Path $DumpRoot "speedtest-tracker.sqlite.dump" },
|
||||||
|
@{ Name = "filebrowser.bolt.dump"; Path = Join-Path $DumpRoot "filebrowser.bolt.dump" },
|
||||||
|
@{ Name = "unraid-flash-config.tar.gz"; Path = Join-Path $DumpRoot "unraid-flash-config.tar.gz" }
|
||||||
|
)
|
||||||
|
|
||||||
|
$reportChecks = @(
|
||||||
|
@{ Name = "vaultwarden"; Path = Join-Path $ReportRoot "vaultwarden-*.md" },
|
||||||
|
@{ Name = "gitea"; Path = Join-Path $ReportRoot "gitea-*.md" },
|
||||||
|
@{ Name = "paperless"; Path = Join-Path $ReportRoot "paperless-*.md" }
|
||||||
|
)
|
||||||
|
|
||||||
|
$now = Get-Date
|
||||||
|
$critical = New-Object System.Collections.Generic.List[string]
|
||||||
|
$warnings = New-Object System.Collections.Generic.List[string]
|
||||||
|
$info = New-Object System.Collections.Generic.List[string]
|
||||||
|
|
||||||
|
foreach ($check in $checks) {
|
||||||
|
if (-not (Test-Path $check.Path)) {
|
||||||
|
$critical.Add("DUMP_MISSING $($check.Name)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = Get-Item $check.Path
|
||||||
|
if ($item.Length -le 0) {
|
||||||
|
$critical.Add("DUMP_EMPTY $($check.Name)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$ageHours = ($now - $item.LastWriteTime).TotalHours
|
||||||
|
if ($ageHours -gt $MaxDumpAgeHours) {
|
||||||
|
$critical.Add(("DUMP_STALE {0} age={1:N1}h" -f $check.Name, $ageHours))
|
||||||
|
} else {
|
||||||
|
$info.Add(("DUMP_OK {0} age={1:N1}h" -f $check.Name, $ageHours))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($check in $reportChecks) {
|
||||||
|
if (-not (Test-Path $ReportRoot)) {
|
||||||
|
$warnings.Add("REPORT_ROOT_MISSING $ReportRoot")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$latest = Get-ChildItem -Path $ReportRoot -Filter ([System.IO.Path]::GetFileName($check.Path)) -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object LastWriteTime -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $latest) {
|
||||||
|
$warnings.Add("REPORT_MISSING $($check.Name)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$ageDays = ($now - $latest.LastWriteTime).TotalDays
|
||||||
|
if ($ageDays -gt $MaxReportAgeDays) {
|
||||||
|
$warnings.Add(("REPORT_STALE {0} age={1:N1}d file={2}" -f $check.Name, $ageDays, $latest.Name))
|
||||||
|
} else {
|
||||||
|
$info.Add(("REPORT_OK {0} age={1:N1}d file={2}" -f $check.Name, $ageDays, $latest.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "# Restore Freshness Check"
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output ("Timestamp: {0}" -f $now.ToString("yyyy-MM-dd HH:mm:ss"))
|
||||||
|
Write-Output ("Critical: {0}" -f $critical.Count)
|
||||||
|
Write-Output ("Warnings: {0}" -f $warnings.Count)
|
||||||
|
Write-Output ("Info: {0}" -f $info.Count)
|
||||||
|
Write-Output ""
|
||||||
|
|
||||||
|
if ($critical.Count -gt 0) {
|
||||||
|
Write-Output "## Critical"
|
||||||
|
$critical | ForEach-Object { Write-Output ("- {0}" -f $_) }
|
||||||
|
Write-Output ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($warnings.Count -gt 0) {
|
||||||
|
Write-Output "## Warnings"
|
||||||
|
$warnings | ForEach-Object { Write-Output ("- {0}" -f $_) }
|
||||||
|
Write-Output ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($info.Count -gt 0) {
|
||||||
|
Write-Output "## Info"
|
||||||
|
$info | ForEach-Object { Write-Output ("- {0}" -f $_) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($critical.Count -gt 0) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+100
@@ -0,0 +1,100 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DUMP_ROOT="${DUMP_ROOT:-/mnt/user/backups/borg/dumps/latest}"
|
||||||
|
REPORT_ROOT="${REPORT_ROOT:-/mnt/user/backups/restore-reports}"
|
||||||
|
MAX_DUMP_AGE_HOURS="${MAX_DUMP_AGE_HOURS:-26}"
|
||||||
|
MAX_REPORT_AGE_DAYS="${MAX_REPORT_AGE_DAYS:-45}"
|
||||||
|
|
||||||
|
now_epoch="$(date +%s)"
|
||||||
|
critical=()
|
||||||
|
warnings=()
|
||||||
|
info=()
|
||||||
|
|
||||||
|
check_file_age_hours() {
|
||||||
|
local path="$1"
|
||||||
|
local mtime
|
||||||
|
mtime="$(stat -c %Y "$path")"
|
||||||
|
echo $(( (now_epoch - mtime) / 3600 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
check_file_age_days() {
|
||||||
|
local path="$1"
|
||||||
|
local mtime
|
||||||
|
mtime="$(stat -c %Y "$path")"
|
||||||
|
echo $(( (now_epoch - mtime) / 86400 ))
|
||||||
|
}
|
||||||
|
|
||||||
|
for dump in \
|
||||||
|
postgresql17-paperless.dump \
|
||||||
|
postgresql17-mailarchiver.dump \
|
||||||
|
mealie.dump \
|
||||||
|
immich.dump \
|
||||||
|
nextcloud.dump \
|
||||||
|
gitea.sqlite.dump \
|
||||||
|
vaultwarden.sqlite.dump \
|
||||||
|
speedtest-tracker.sqlite.dump \
|
||||||
|
filebrowser.bolt.dump \
|
||||||
|
unraid-flash-config.tar.gz; do
|
||||||
|
path="$DUMP_ROOT/$dump"
|
||||||
|
if [ ! -f "$path" ]; then
|
||||||
|
critical+=("DUMP_MISSING $dump")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ ! -s "$path" ]; then
|
||||||
|
critical+=("DUMP_EMPTY $dump")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
age="$(check_file_age_hours "$path")"
|
||||||
|
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
|
||||||
|
critical+=("DUMP_STALE $dump age=${age}h")
|
||||||
|
else
|
||||||
|
info+=("DUMP_OK $dump age=${age}h")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for service in vaultwarden gitea paperless; do
|
||||||
|
if [ ! -d "$REPORT_ROOT" ]; then
|
||||||
|
warnings+=("REPORT_ROOT_MISSING $REPORT_ROOT")
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
latest="$(find "$REPORT_ROOT" -maxdepth 1 -type f -name "$service-*.md" | sort | tail -n 1 || true)"
|
||||||
|
if [ -z "$latest" ]; then
|
||||||
|
warnings+=("REPORT_MISSING $service")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
age="$(check_file_age_days "$latest")"
|
||||||
|
if [ "$age" -gt "$MAX_REPORT_AGE_DAYS" ]; then
|
||||||
|
warnings+=("REPORT_STALE $service age=${age}d file=$(basename "$latest")")
|
||||||
|
else
|
||||||
|
info+=("REPORT_OK $service age=${age}d file=$(basename "$latest")")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "# Restore Freshness Check"
|
||||||
|
echo
|
||||||
|
echo "Timestamp: $(date '+%F %T')"
|
||||||
|
echo "Critical: ${#critical[@]}"
|
||||||
|
echo "Warnings: ${#warnings[@]}"
|
||||||
|
echo "Info: ${#info[@]}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [ "${#critical[@]}" -gt 0 ]; then
|
||||||
|
echo "## Critical"
|
||||||
|
printf -- '- %s\n' "${critical[@]}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#warnings[@]}" -gt 0 ]; then
|
||||||
|
echo "## Warnings"
|
||||||
|
printf -- '- %s\n' "${warnings[@]}"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#info[@]}" -gt 0 ]; then
|
||||||
|
echo "## Info"
|
||||||
|
printf -- '- %s\n' "${info[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "${#critical[@]}" -eq 0 ]
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RESTORE_TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
|
||||||
|
BORG_RESTORE_HOST_ROOT="${BORG_RESTORE_HOST_ROOT:-/mnt/user/appdata/borg-ui/restore}"
|
||||||
|
BORG_PASSPHRASE_FILE_DEFAULT="${BORG_PASSPHRASE_FILE_DEFAULT:-/mnt/user/appdata/secrets/borg_repo_passphrase.txt}"
|
||||||
|
|
||||||
|
require_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
|
echo "Missing command: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require_path() {
|
||||||
|
[ -e "$1" ] || {
|
||||||
|
echo "Missing path: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
latest_archive_name() {
|
||||||
|
docker exec "$BORG_CONTAINER" python3 - <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("select archive_name from backup_jobs where status='completed' order by created_at desc limit 1")
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise SystemExit("No completed borg archive found")
|
||||||
|
print(row[0])
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
borg_repo_url() {
|
||||||
|
docker exec "$BORG_CONTAINER" python3 - <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("select path from repositories where path is not null and path != '' order by id asc limit 1")
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise SystemExit("No borg repository configured")
|
||||||
|
print(row[0])
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
borg_extract() {
|
||||||
|
local extract_dir="$1"
|
||||||
|
shift
|
||||||
|
local paths=("$@")
|
||||||
|
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
|
||||||
|
import os, sys, subprocess
|
||||||
|
extract_dir = sys.argv[1]
|
||||||
|
paths = sys.argv[2:]
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("select path from repositories where path is not null and path != '' order by id asc limit 1")
|
||||||
|
repo = cur.fetchone()[0]
|
||||||
|
cur.execute("select archive_name from backup_jobs where status='completed' order by created_at desc limit 1")
|
||||||
|
archive = cur.fetchone()[0]
|
||||||
|
with open('/local/secrets/borg_repo_passphrase.txt', 'r', encoding='utf-8') as f:
|
||||||
|
os.environ['BORG_PASSPHRASE'] = f.read().strip()
|
||||||
|
os.makedirs(extract_dir, exist_ok=True)
|
||||||
|
os.chdir(extract_dir)
|
||||||
|
subprocess.run(['borg', 'extract', f'{repo}::{archive}', *paths], check=True)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
write_report() {
|
||||||
|
local report_file="$1"
|
||||||
|
shift
|
||||||
|
mkdir -p "$(dirname "$report_file")"
|
||||||
|
cat > "$report_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_compose() {
|
||||||
|
local compose_file="$1"
|
||||||
|
if [ -f "$compose_file" ]; then
|
||||||
|
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
restoretest-gitea:
|
||||||
|
image: docker.gitea.com/gitea:1.25.4@sha256:17d18218be2dad1f8ed402a4f906989505c90ab8b66ee9befcecfb5d470133e7
|
||||||
|
container_name: restoretest-gitea
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
|
environment:
|
||||||
|
USER_UID: "1000"
|
||||||
|
USER_GID: "1000"
|
||||||
|
GITEA__server__DOMAIN: 127.0.0.1
|
||||||
|
GITEA__server__ROOT_URL: http://127.0.0.1:13000/
|
||||||
|
GITEA__database__DB_TYPE: sqlite3
|
||||||
|
GITEA__webhook__ALLOWED_HOST_LIST: "*"
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:13000:3000"
|
||||||
|
- "127.0.0.1:12222:22"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/gitea/data:/data
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Gitea Restore Test Plan
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Nachweisen, dass ein Gitea-Backup in einer isolierten Testumgebung wieder startbar ist und sowohl Web-UI als auch SSH-Port wieder verfuegbar sind.
|
||||||
|
|
||||||
|
## Quelle
|
||||||
|
|
||||||
|
- Backup-Quelle: Borg / Share-Backup
|
||||||
|
- fachlich relevanter Datenpfad: `/mnt/user/services/gitea/data`
|
||||||
|
- keine separaten Secret-Dateien dokumentiert
|
||||||
|
|
||||||
|
## Test-Ziel
|
||||||
|
|
||||||
|
- Restore-Lab: `/mnt/user/backups/restore-lab/gitea`
|
||||||
|
- Testdatenpfad: `/mnt/user/backups/restore-lab/gitea/data`
|
||||||
|
- Testcontainer: `restoretest-gitea`
|
||||||
|
- Testports:
|
||||||
|
- Web: `127.0.0.1:13000:3000`
|
||||||
|
- SSH: `127.0.0.1:12222:22`
|
||||||
|
- Report-Ziel: `/mnt/user/backups/restore-reports/gitea-YYYY-MM-DD.md`
|
||||||
|
|
||||||
|
## Schutzregeln
|
||||||
|
|
||||||
|
- produktiven Pfad `/mnt/user/services/gitea/data` nie beschreiben
|
||||||
|
- produktive Domain `git.kaleschke.info` nicht fuer die Testinstanz uebernehmen
|
||||||
|
- produktiven SSH-Port `222` nicht fuer die Testinstanz uebernehmen
|
||||||
|
- keine Traefik-Labels fuer die Testinstanz
|
||||||
|
- Testcontainer nur gegen Restore-Lab-Daten starten
|
||||||
|
|
||||||
|
## Geplanter Ablauf
|
||||||
|
|
||||||
|
1. Restore-Ziel unter `/mnt/user/backups/restore-lab/gitea` vorbereiten
|
||||||
|
2. Gitea-Daten aus Backup in `restore-lab/gitea/data` wiederherstellen
|
||||||
|
3. Testinstanz mit `ops/restore-tests/gitea-compose.test.yml` starten
|
||||||
|
4. lokalen Smoke-Test gegen `http://127.0.0.1:13000` und `127.0.0.1:12222` ausfuehren
|
||||||
|
5. Report unter `/mnt/user/backups/restore-reports/` schreiben
|
||||||
|
6. Testcontainer stoppen und Testumgebung bereinigen oder bewusst stehen lassen
|
||||||
|
|
||||||
|
## Smoke-Test
|
||||||
|
|
||||||
|
Minimal erfolgreich:
|
||||||
|
|
||||||
|
- Container startet
|
||||||
|
- Web-UI antwortet
|
||||||
|
- mindestens ein bestehendes Repository-Verzeichnis ist im Restore-Lab sichtbar
|
||||||
|
- SSH-Port reagiert auf Verbindungsaufbau
|
||||||
|
|
||||||
|
Optional spaeter:
|
||||||
|
|
||||||
|
- Login-Seite gezielt pruefen
|
||||||
|
- SQLite-Datei `gitea.db` oder Nachfolger explizit bestaetigen
|
||||||
|
- `gitea doctor` oder interner Healthcheck als Zusatz
|
||||||
|
|
||||||
|
## Noch offen vor dem ersten echten Lauf
|
||||||
|
|
||||||
|
- exakter Borg-Restore-Befehl bzw. Restore-Quelle auf dem Host
|
||||||
|
- Bereinigungsstrategie fuer alte Restore-Lab-Daten
|
||||||
|
- ob Reports spaeter zusaetzlich per `ntfy` referenziert werden
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
param(
|
||||||
|
[string]$BackupSource = "/mnt/user/backups/borg",
|
||||||
|
[string]$RestoreRoot = "/mnt/user/backups/restore-lab/gitea",
|
||||||
|
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
|
||||||
|
[string]$BorgPassphraseFile = "/mnt/user/appdata/secrets/borg_repo_passphrase.txt",
|
||||||
|
[switch]$WhatIf
|
||||||
|
)
|
||||||
|
|
||||||
|
$Mode = if ($WhatIf) { "WhatIf" } else { "PlanOnly" }
|
||||||
|
|
||||||
|
Write-Output "Gitea restore test scaffold"
|
||||||
|
Write-Output "BackupSource: $BackupSource"
|
||||||
|
Write-Output "RestoreRoot: $RestoreRoot"
|
||||||
|
Write-Output "ReportRoot: $ReportRoot"
|
||||||
|
Write-Output "BorgPassphraseFile: $BorgPassphraseFile"
|
||||||
|
Write-Output "Expected Borg source path inside archive: local/gitea/data"
|
||||||
|
|
||||||
|
if ($WhatIf) {
|
||||||
|
Write-Output "Mode: WhatIf"
|
||||||
|
} else {
|
||||||
|
Write-Output "Mode: PlanOnly"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "Planned steps:"
|
||||||
|
Write-Output "1. Prepare restore-lab target under /mnt/user/backups/restore-lab/gitea"
|
||||||
|
Write-Output "2. Restore Gitea data into an isolated test path"
|
||||||
|
Write-Output ' Template: borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/gitea/data'
|
||||||
|
Write-Output ' Passphrase source: $(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)'
|
||||||
|
Write-Output "3. Start container restoretest-gitea against test data only"
|
||||||
|
Write-Output "4. Run smoke checks against the local web and SSH endpoints"
|
||||||
|
Write-Output "5. Write markdown report under /mnt/user/backups/restore-reports"
|
||||||
|
Write-Output "6. Stop test container and clean restore data after success"
|
||||||
|
Write-Output ""
|
||||||
|
Write-Output "This script is intentionally a scaffold only."
|
||||||
|
Write-Output "No restore, no container start, no file write is executed yet."
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
. "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
|
WHATIF=0
|
||||||
|
KEEP_DATA=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--what-if) WHATIF=1 ;;
|
||||||
|
--keep-data) KEEP_DATA=1 ;;
|
||||||
|
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
RESTORE_ROOT="/mnt/user/backups/restore-lab/gitea"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
DATA_DIR="$RESTORE_ROOT/data"
|
||||||
|
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/gitea-extract"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/gitea-compose.test.yml"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/gitea-$(date +%F).md"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Gitea restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
ReportRoot: $REPORT_ROOT
|
||||||
|
Expected Borg source path: local/gitea/data
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_cmd curl
|
||||||
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$DATA_DIR"
|
||||||
|
fi
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORE_ROOT"
|
||||||
|
|
||||||
|
archive="$(latest_archive_name)"
|
||||||
|
repo="$(borg_repo_url)"
|
||||||
|
borg_extract "/restore/gitea-extract" "local/gitea/data"
|
||||||
|
mv "$EXTRACT_DIR/local/gitea/data" "$DATA_DIR"
|
||||||
|
|
||||||
|
repo_sample="$(find "$DATA_DIR/git/repositories" -maxdepth 3 -type d | sed -n '2p')"
|
||||||
|
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d >/dev/null
|
||||||
|
sleep 8
|
||||||
|
status="$(curl -s -o /tmp/gitea-body.html -w '%{http_code}' http://127.0.0.1:13000)"
|
||||||
|
grep -qi "Gitea" /tmp/gitea-body.html
|
||||||
|
if timeout 5 bash -lc '</dev/tcp/127.0.0.1/12222' >/dev/null 2>&1; then
|
||||||
|
ssh_state="open"
|
||||||
|
else
|
||||||
|
echo "Gitea SSH port not reachable" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Gitea Restore Test Report - $(date +%F)
|
||||||
|
|
||||||
|
- Service: \`gitea\`
|
||||||
|
- Source repo: \`$repo\`
|
||||||
|
- Archive: \`$archive\`
|
||||||
|
- Restore target: \`$DATA_DIR\`
|
||||||
|
- Test container: \`restoretest-gitea\`
|
||||||
|
- Test endpoints:
|
||||||
|
- Web: \`http://127.0.0.1:13000\`
|
||||||
|
- SSH: \`127.0.0.1:12222\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Borg extract into isolated restore-lab: \`ok\`
|
||||||
|
- HTTP status: \`$status\`
|
||||||
|
- HTML content: \`Gitea\`
|
||||||
|
- SSH port: \`$ssh_state\`
|
||||||
|
- Repository sample: \`$repo_sample\`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Test ran without Traefik and without the productive domain.
|
||||||
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Gitea restore test ok -> $REPORT_FILE"
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Gitea Restore Runbook
|
||||||
|
|
||||||
|
## Vorbedingungen
|
||||||
|
|
||||||
|
- Borg-Quelle ist verfuegbar
|
||||||
|
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||||
|
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
|
||||||
|
|
||||||
|
## Bestaetigter Host-Stand
|
||||||
|
|
||||||
|
- produktive Gitea-Daten liegen unter `/mnt/user/services/gitea/data`
|
||||||
|
- Borg-Scope fuer Gitea ist `/local/gitea/data`
|
||||||
|
- `restore-lab` und `restore-reports` sind auf dem Host vorhanden
|
||||||
|
|
||||||
|
## Bestaetigter Teststand
|
||||||
|
|
||||||
|
- echter Mini-Restore am `2026-05-07` erfolgreich gelaufen
|
||||||
|
- Restore-Ziel war `/mnt/user/backups/restore-lab/gitea/data`
|
||||||
|
- Testcontainer `restoretest-gitea` lief lokal auf `127.0.0.1:13000` und `127.0.0.1:12222`
|
||||||
|
- HTTP `200 OK`, HTML-Titel und SSH-Port wurden erfolgreich verifiziert
|
||||||
|
- Report liegt unter `/mnt/user/backups/restore-reports/gitea-2026-05-07.md`
|
||||||
|
|
||||||
|
## Platzhalter
|
||||||
|
|
||||||
|
- `ARCHIVE_NAME`: Borg-Archiv fuer den Restore-Test
|
||||||
|
- `REPORT_DATE`: z. B. `2026-05-07`
|
||||||
|
- `BORG_REPO`: Host-Borg-Repo
|
||||||
|
- `BORG_PASSPHRASE_FILE`: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
|
||||||
|
1. Testpfade vorbereiten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /mnt/user/backups/restore-lab/gitea/data
|
||||||
|
mkdir -p /mnt/user/backups/restore-reports
|
||||||
|
rm -rf /mnt/user/backups/restore-lab/gitea/data/*
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Gitea-Daten aus Borg in das Restore-Lab extrahieren
|
||||||
|
|
||||||
|
Archiv zuerst pruefen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export BORG_REPO='...'
|
||||||
|
export BORG_PASSPHRASE="$(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)"
|
||||||
|
borg list "$BORG_REPO"
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore in das Testziel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/user/backups/restore-lab/gitea
|
||||||
|
borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/gitea/data
|
||||||
|
mv /mnt/user/backups/restore-lab/gitea/local/gitea/data /mnt/user/backups/restore-lab/gitea/data
|
||||||
|
rmdir /mnt/user/backups/restore-lab/gitea/local/gitea
|
||||||
|
rmdir /mnt/user/backups/restore-lab/gitea/local
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn das Archiv den Pfad anders ablegt, zuerst mit `borg list "$BORG_REPO" "::ARCHIVE_NAME"` den exakten Eintrag pruefen.
|
||||||
|
|
||||||
|
3. Testcontainer starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Smoke-Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I http://127.0.0.1:13000
|
||||||
|
ssh -p 12222 -o BatchMode=yes -o StrictHostKeyChecking=no git@127.0.0.1
|
||||||
|
docker logs restoretest-gitea --tail 50
|
||||||
|
```
|
||||||
|
|
||||||
|
Minimal erfolgreich:
|
||||||
|
|
||||||
|
- HTTP-Antwort kommt
|
||||||
|
- Login-Seite ist erreichbar
|
||||||
|
- SSH-Port antwortet
|
||||||
|
- Restore-Lab enthaelt Gitea-Daten
|
||||||
|
|
||||||
|
5. Testcontainer wieder stoppen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Report schreiben
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mnt/user/backups/restore-reports/gitea-REPORT_DATE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Testdaten nach erfolgreichem Lauf bereinigen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf /mnt/user/backups/restore-lab/gitea/data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Festgelegte Entscheidungen
|
||||||
|
|
||||||
|
- Testdaten werden nach erfolgreichem Lauf geloescht.
|
||||||
|
- `ntfy` wird nicht im ersten echten Lauf eingebunden.
|
||||||
|
- Die Borg-Passphrase wird fuer Restore-Tests aus einer Host-Secret-Datei gelesen, nicht aus Borg-UI-Interna.
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TOPIC="${TOPIC:-homelab-info}"
|
||||||
|
TESTS="${TESTS:-vaultwarden gitea paperless}"
|
||||||
|
|
||||||
|
pick_random() {
|
||||||
|
printf '%s\n' $TESTS | awk 'BEGIN { srand() } { items[++count] = $0 } END { print items[int(rand() * count) + 1] }'
|
||||||
|
}
|
||||||
|
|
||||||
|
selected="$(pick_random)"
|
||||||
|
echo "Selected monthly restore test: $selected"
|
||||||
|
|
||||||
|
exec "$SCRIPT_DIR/run-restore-job-with-ntfy.sh" "$selected" "$TOPIC"
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
services:
|
||||||
|
restoretest-paperless-postgres:
|
||||||
|
image: postgres:17.9@sha256:5b96f1a16bd9768b060dd2ffe55cb6225c4d9ef4d214a8b21eb08134869a97e4
|
||||||
|
container_name: restoretest-paperless-postgres
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
POSTGRES_USER: paperless
|
||||||
|
POSTGRES_DB: paperless
|
||||||
|
POSTGRES_PASSWORD: restoretest-paperless-db
|
||||||
|
PGDATA: /var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/paperless/postgres:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U paperless -d paperless"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
restoretest-paperless-redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: restoretest-paperless-redis
|
||||||
|
restart: "no"
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- exec redis-server --appendonly yes --requirepass "restoretest-paperless-redis"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
restoretest-paperless:
|
||||||
|
image: ghcr.io/paperless-ngx/paperless-ngx:2.20.10@sha256:07a0b4ba01ce377c82a0636e16c0c3d931fde5b7e9304de6601986cc42d9b6e6
|
||||||
|
container_name: restoretest-paperless
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
restoretest-paperless-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
restoretest-paperless-redis:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
PAPERLESS_TIKA_ENABLED: "0"
|
||||||
|
PAPERLESS_DBENGINE: postgresql
|
||||||
|
PAPERLESS_DBHOST: restoretest-paperless-postgres
|
||||||
|
PAPERLESS_DBNAME: paperless
|
||||||
|
PAPERLESS_DBUSER: paperless
|
||||||
|
PAPERLESS_DBPASS: restoretest-paperless-db
|
||||||
|
PAPERLESS_REDIS: redis://:restoretest-paperless-redis@restoretest-paperless-redis:6379
|
||||||
|
PAPERLESS_TIME_ZONE: Europe/Berlin
|
||||||
|
PAPERLESS_OCR_LANGUAGE: deu+eng
|
||||||
|
PAPERLESS_URL: http://127.0.0.1:18120
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:18120:8000"
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/paperless/consume:/usr/src/paperless/consume
|
||||||
|
- /mnt/user/backups/restore-lab/paperless/data:/usr/src/paperless/data
|
||||||
|
- /mnt/user/backups/restore-lab/paperless/export:/usr/src/paperless/export
|
||||||
|
- /mnt/user/backups/restore-lab/paperless/media:/usr/src/paperless/media
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Paperless Restore Test Plan
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Nachweisen, dass ein Paperless-Backup in einer isolierten Testumgebung wieder startbar ist und sowohl Dokumentenpfade als auch PostgreSQL-Dump sauber zusammenlaufen.
|
||||||
|
|
||||||
|
## Quelle
|
||||||
|
|
||||||
|
- Backup-Quelle: Borg / Share-Backup
|
||||||
|
- fachlich relevante Dateipfade:
|
||||||
|
- `/mnt/user/appdata/paperless-ngx/data`
|
||||||
|
- `/mnt/user/documents/paperless`
|
||||||
|
- `/mnt/user/documents/paperless/export`
|
||||||
|
- `/mnt/user/documents/scans_inbox`
|
||||||
|
- fachlich relevanter Dump:
|
||||||
|
- `/mnt/user/backups/borg/dumps/latest/postgresql17-paperless.dump`
|
||||||
|
|
||||||
|
## Test-Ziel
|
||||||
|
|
||||||
|
- Restore-Lab: `/mnt/user/backups/restore-lab/paperless`
|
||||||
|
- Testdatenpfade:
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless/data`
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless/media`
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless/export`
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless/consume`
|
||||||
|
- `/mnt/user/backups/restore-lab/paperless/postgres`
|
||||||
|
- Testcontainer:
|
||||||
|
- `restoretest-paperless`
|
||||||
|
- `restoretest-paperless-postgres`
|
||||||
|
- `restoretest-paperless-redis`
|
||||||
|
- Testport Web: `127.0.0.1:18120:8000`
|
||||||
|
- Report-Ziel: `/mnt/user/backups/restore-reports/paperless-YYYY-MM-DD.md`
|
||||||
|
|
||||||
|
## Schutzregeln
|
||||||
|
|
||||||
|
- produktive Pfade nie beschreiben
|
||||||
|
- produktive Domain `paperless.kaleschke.info` nicht fuer die Testinstanz uebernehmen
|
||||||
|
- keine Traefik-Labels fuer die Testinstanz
|
||||||
|
- keine produktive PostgreSQL- oder Redis-Instanz fuer den Test verwenden
|
||||||
|
- Testcontainer nur gegen Restore-Lab-Daten und isolierte Test-Backends starten
|
||||||
|
|
||||||
|
## Geplanter Ablauf
|
||||||
|
|
||||||
|
1. Restore-Ziel unter `/mnt/user/backups/restore-lab/paperless` vorbereiten
|
||||||
|
2. Paperless-Dateipfade aus Borg in das Restore-Lab wiederherstellen
|
||||||
|
3. Test-Postgres und Test-Redis mit `ops/restore-tests/paperless-compose.test.yml` starten
|
||||||
|
4. `postgresql17-paperless.dump` in Test-Postgres importieren
|
||||||
|
5. Testinstanz `restoretest-paperless` starten
|
||||||
|
6. lokalen Smoke-Test gegen `http://127.0.0.1:18120` ausfuehren
|
||||||
|
7. Report unter `/mnt/user/backups/restore-reports/` schreiben
|
||||||
|
8. Testcontainer stoppen und Testumgebung bereinigen
|
||||||
|
|
||||||
|
## Smoke-Test
|
||||||
|
|
||||||
|
Minimal erfolgreich:
|
||||||
|
|
||||||
|
- Test-Postgres startet
|
||||||
|
- Dump-Import gelingt
|
||||||
|
- Paperless-Web-UI antwortet
|
||||||
|
- mindestens ein Dokument liegt im Restore-Lab-Medienpfad
|
||||||
|
|
||||||
|
Optional spaeter:
|
||||||
|
|
||||||
|
- Login-Seite gezielt pruefen
|
||||||
|
- Dokumentanzahl aus UI oder DB querpruefen
|
||||||
|
- OCR-/Task-Worker-Status verifizieren
|
||||||
|
|
||||||
|
## Noch offen vor dem ersten echten Lauf
|
||||||
|
|
||||||
|
- exakter Borg-Restore-Befehl fuer alle vier Dateipfade
|
||||||
|
- exakter `pg_restore`-Befehl im Test-Postgres
|
||||||
|
- wie stark wir `consume` im ersten Lauf ueberhaupt brauchen
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user