95 Commits

Author SHA1 Message Date
Micha f77a69a0b2 Record audit baseline tag 2026-05-26 19:44:50 +02:00
Micha f73cf48e41 Document external recovery dependencies 2026-05-26 19:44:14 +02:00
Micha eea2697ca1 Triage policy check warnings 2026-05-26 19:42:01 +02:00
Micha a3d77d7529 Document hardware capacity baseline 2026-05-26 19:39:42 +02:00
Micha 02a50e1a58 Record Komodo metadata fix 2026-05-26 19:32:26 +02:00
Micha 267e76059a Clean up Komodo webhook drift 2026-05-26 19:29:51 +02:00
Micha 9d4fee02ca Record next audit handoff 2026-05-26 15:39:41 +02:00
Micha 24ebcaa3c7 Document Nextcloud webhook refresh 2026-05-26 15:34:43 +02:00
Micha 45bae13aa0 Remove legacy monitoring stacks 2026-05-26 15:27:37 +02:00
Micha ff5991cec8 Document AdGuard webhook diagnosis 2026-05-26 15:15:34 +02:00
Micha 5b6e7b8b66 Record AdGuard Tailscale validation 2026-05-26 15:00:35 +02:00
Micha 5cb401797d Bind AdGuard admin to Tailscale 2026-05-26 14:55:49 +02:00
Micha 1d0cba92bd Record Unraid flash backup live evidence 2026-05-25 19:49:38 +02:00
Micha 9353a9fc44 Fix Borg preflight freshness dump path 2026-05-25 19:44:22 +02:00
Micha d50b11784d Add Unraid flash config to Borg preflight 2026-05-25 19:36:16 +02:00
Micha 09eeac51e1 Record legacy grafana hook disablement 2026-05-25 16:51:14 +02:00
Micha 565940b9ef Record monitoring migration completion 2026-05-25 16:47:03 +02:00
Micha b6bbca43ad Replace Uptime Kuma with monitoring checks 2026-05-25 16:37:46 +02:00
Micha 388e57e385 Document AdGuard LAN admin decision 2026-05-25 16:27:03 +02:00
Micha 0c2bb8484a Record Homepage live removal evidence 2026-05-25 14:52:18 +02:00
Micha a7797fd02e Consolidate dashboard on Glance 2026-05-25 14:44:46 +02:00
Micha bac927bbcc Record Jellyfin live removal evidence 2026-05-25 12:15:08 +02:00
Micha add8b71ea9 Remove Jellyfin from homelab target state 2026-05-25 11:57:00 +02:00
Micha e21e89e51b Document Borg passphrase host secret 2026-05-25 11:38:03 +02:00
Micha 4e4684b616 Document external GitHub mirror 2026-05-25 11:27:28 +02:00
Micha 84030956ac Fix Gitea external DNS for GitHub mirror 2026-05-25 11:17:31 +02:00
Micha 17fe8073bb Allow GitHub mirror target for Gitea 2026-05-25 10:56:04 +02:00
Micha 9f32ba72c1 Make audit final runtime wording stable 2026-05-25 07:37:28 +02:00
Micha e9a7f79025 Clarify audit doc-only deployment state 2026-05-25 07:36:03 +02:00
Micha 43727151df Refresh final audit live status 2026-05-25 07:34:08 +02:00
Micha 66ee10cb55 Clarify Disk1 parity follow-up 2026-05-25 06:17:13 +02:00
Micha ab68900216 Complete Disk1 phase 2 migration 2026-05-25 06:13:50 +02:00
Micha 8f56c6edcd Document Disk1 phase 2 backup readiness 2026-05-24 13:07:45 +02:00
Micha 8e400fb3c3 Finalize homelab audit end state 2026-05-23 11:29:08 +02:00
Micha cd650b19ac Close Gitea signup, dedup posture-check alerts, extend Borg scope
Operational hardening across several services after live incident
analysis between 2026-05-18 and 2026-05-20:

- Gitea: disable public registration and OpenID signup/signin to
  stop the external POST / 5xx bursts that triggered availability
  alerts. New repo-wide policy requires every productive
  Micha/homelab-infra Komodo stack to ship with an active
  Gitea->Komodo webhook on the current stack ID (documented in
  CLAUDE.md, AI_CONTEXT.md, WORKFLOW.md).
- posture-check: extract the Disk1 fstype check into its own
  function so the documented Disk1 NTFS exception no longer raises
  ntfy warnings, skip POSIX inode checks on NTFS, and dedup ntfy
  alerts via a fingerprint state file with ALERT_REPEAT_SECONDS
  (default 24h). Repeat-spam on the same cause now suppressed.
- docker-critical-events: parse the event JSON for container name,
  action, exit code and signal; drop `die exit=0` events (clean
  stops); ship a structured ntfy message instead of the raw event
  line.
- Borg UI: mount /mnt/user/services into the backup container as
  /local/services:ro and include homelab-infra, stacks and
  posture-check in all-important-sources.txt. RESTORE_MATRIX and
  DISASTER_RECOVERY updated accordingly.
- Unraid user scripts: document the new
  homelab-operations-report-daily cron job and the SMTP password
  file it expects on the host.
- MIGRATION_LOG: capture the four live events from this window -
  Gitea 5xx burst + signup closure, Komodo webhook reconciliation,
  posture-check host-version verification, Borg scope extension,
  and Traefik 5xx alert detuning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 11:05:35 +02:00
Codex af231dd4e8 Fix zero-count noise pattern handling 2026-05-23 11:03:02 +02:00
Micha 428223d2e4 Mark posture report scripts executable 2026-05-23 11:00:40 +02:00
Micha b6d3ed4832 Tune homelab availability alerts 2026-05-23 10:58:12 +02:00
Micha 9e7bebbd3c Add daily operations report with hardened log-noise filtering
Brings the previously untracked daily-status-report.sh and
send-operations-report-mail.sh into the repo, plus a refactor of the
log-noise pipeline:

- New helper services/posture-check/lib/normalize-noise-patterns.sh
  strips comments, empty lines and trailing whitespace from
  log-noise.patterns before grep -f sees it. A stray empty line in
  the pattern file would otherwise have made grep -Eaif match every
  hit and silently wipe the log highlights.
- log-noise.patterns is now documented per-pattern (Why / Re-check).
  The Vaultwarden pattern is split: token/session noise stays as
  noise; DNS/Connect/Resolve/reqwest/hyper errors are removed from
  the noise set so real network signals stay visible.
- collect_log_highlights now reports a per-container and per-pattern
  noise breakdown (Top N) and an escalation flag when any pattern
  exceeds NOISE_ESCALATION_THRESHOLD (default 500). The flag is fed
  into derive_report_status and the management summary.
- New shell tests under services/posture-check/tests/ verify the
  normalize helper handles comments, empty lines, whitespace-only
  lines, and that unknown error lines remain in the attention set.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 10:41:33 +02:00
Micha b7cbbe51de Fix Jellyfin external DNS 2026-05-18 20:29:18 +02:00
Micha 71ac18b21c Fix Jellyfin native auth routing 2026-05-18 13:43:41 +02:00
Micha 90f270be96 Fix Jellyfin config permissions 2026-05-18 13:21:30 +02:00
Micha e28f8dabec Add Jellyfin media server stack 2026-05-18 13:09:32 +02:00
Micha edfec5b66d Add Plex media server stack 2026-05-18 13:09:27 +02:00
Micha 59bec9ac77 Fix Glance live widget data sources 2026-05-18 09:35:53 +02:00
Micha 9f86da708a Add Glance live network widgets 2026-05-18 08:31:57 +02:00
Micha d6170211c4 Refine Glance network widgets 2026-05-18 08:13:13 +02:00
Micha fb681086f3 Restyle Glance dashboard layout 2026-05-18 08:03:59 +02:00
Micha 5b101f3b3d Keep only verified Glance community widget 2026-05-17 18:20:56 +02:00
Micha 669efbd57e Fix Glance Speedtest subrequest headers 2026-05-17 18:18:14 +02:00
Micha 2dd5590a2a Polish Glance community widgets 2026-05-17 18:16:05 +02:00
Micha 175cd6951f Add Glance community homelab widgets 2026-05-17 18:07:57 +02:00
Micha aeb7573b03 Remove noisy Glance dashboard widgets 2026-05-17 17:08:10 +02:00
Micha 215f44b962 Fix Glance monitor health checks 2026-05-17 17:05:23 +02:00
Micha 6ce625f77a Fix Glance socket proxy image tag 2026-05-17 16:59:29 +02:00
Micha c3c8060ddf Add Glance homelab dashboard stack 2026-05-17 16:51:43 +02:00
Micha 29eaf8001f Normalize ntfy alert routing 2026-05-17 14:57:45 +02:00
Micha db7dc3f2af Add ntfy alert delivery for monitoring 2026-05-17 11:34:19 +02:00
Micha c748236886 Prune monitoring dashboard imports 2026-05-17 11:30:00 +02:00
Micha 8aa850df40 Set Grafana DNS resolvers 2026-05-17 11:26:27 +02:00
Micha 2c4854f628 Accept protected HTTP checks in blackbox 2026-05-17 11:25:35 +02:00
Micha b7050812d4 Fix blackbox DNS resolution 2026-05-17 11:24:20 +02:00
Micha c95fa601f0 Add monitoring replacement baseline 2026-05-17 11:22:38 +02:00
Micha 0c308ff352 Preserve InfluxDB data in monitoring stack 2026-05-17 10:47:57 +02:00
Micha 53216e50c1 Fix monitoring InfluxDB volume permissions 2026-05-17 10:45:32 +02:00
Micha b7dfdad621 Consolidate monitoring target stack 2026-05-17 10:41:29 +02:00
Micha 61625a7a1c ops: keep monitoring importer running for komodo 2026-05-16 22:39:09 +02:00
Micha 6e28ea94d2 ops: wire monitoring stack to traefik metrics 2026-05-16 22:10:43 +02:00
Micha 58eb53a6a8 ops: add monitoring compose stack 2026-05-16 21:59:20 +02:00
Micha d345d770c2 docs: add homelab audit report 2026-05-16 21:51:48 +02:00
Micha 2e136d9060 Update current homelab rest list 2026-05-16 20:30:31 +02:00
Micha 6ca829ec45 Document Unraid automation schedules 2026-05-16 20:11:19 +02:00
Micha ef3b546d30 Align documentation consistency fixes 2026-05-16 20:04:46 +02:00
Micha 6f684fb4e3 Document Unraid native log rotation 2026-05-16 19:31:54 +02:00
Micha 0adddb6533 Add Unraid automation script templates 2026-05-16 14:34:35 +02:00
Micha 162421e537 Harden Gitea webhooks and Docker log limits 2026-05-16 13:34:45 +02:00
Micha bf30240217 Remove Loki image-internal healthcheck 2026-05-16 13:32:02 +02:00
Micha 5f7940aa01 Tune Loki host bootstrap settings 2026-05-16 13:31:08 +02:00
Micha a5add937f8 Add Loki Alloy logging baseline 2026-05-16 13:26:49 +02:00
Micha 5ada1ad153 Treat Filebrowser state as file-backed dump 2026-05-16 13:16:01 +02:00
Micha ead7e1e17d Fallback SQLite dumps to host paths 2026-05-16 13:14:10 +02:00
Micha 14e9c0963d Allow posture warnings before Borg 2026-05-16 13:12:47 +02:00
Micha 23262cd7b9 Allow Disk1 NTFS posture transition 2026-05-16 13:12:19 +02:00
Micha 878ad2d5f1 Harden backup and posture checks 2026-05-16 13:04:22 +02:00
Micha 12a87ad342 Clean up SQLite dump temp files on failure 2026-05-16 12:03:10 +02:00
Micha 0e7e639df4 Correct Filebrowser backup state 2026-05-16 11:59:57 +02:00
Micha 18df2d155d Add consistent Borg database dumps 2026-05-16 11:49:36 +02:00
Micha fa177155e6 Document final restore service secrets 2026-05-16 10:55:42 +02:00
Micha 11c863c8aa Set explicit DNS for Grafana 2026-05-16 10:41:06 +02:00
Micha dd9f677779 Use LSIO file secret for code-server password 2026-05-16 10:37:25 +02:00
Micha a9e62ee8e5 Document restore exceptions and layout fixes 2026-05-16 07:43:46 +02:00
Micha 4e7d313a20 Pin restored auth stack image tags 2026-05-15 18:18:32 +02:00
Micha 57ea7507a7 Remove Backrest and WD backup references 2026-05-15 16:57:42 +02:00
Micha 1e3e019f28 Update STORAGE_LAYOUT.draft.md
docs: storage-layout v1.3 als bindendes Architektur-Dokument

Erstfassung des verbindlichen Storage-Layouts nach NTFS-Cache-Vorfall
2026-05-11. Definiert Pool-/Share-Konfiguration, Appdata-Struktur,
Backup-Architektur (Borg only, kein Backrest, kein WD-MyBookLive),
Posture-Check-Pflichten, Hard Rules und Migrationspfade.

Operator-Entscheidungen v1.0 -> v1.3 eingearbeitet. Sechs der elf
Open Items entschieden, eines deferred (Disk-Groessen via Posture-Check),
drei als Folge-Aufgaben (Retention, Schwellen-Kalibrierung,
RESTORE_MATRIX-Klassifikation).

Refs: Incident 2026-05-11 NTFS-Cache-Korruption
2026-05-15 16:10:55 +02:00
Micha 54fd0c3347 diverse Änderungen 2026-05-15 16:05:34 +02:00
116 changed files with 10205 additions and 681 deletions
+1
View File
@@ -27,3 +27,4 @@
Thumbs.db
*.tmp
*.log
.serena/
+2
View File
@@ -69,6 +69,8 @@ Standard-Workflow:
7. Komodo-Deploy/Runtime pruefen
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.
## Sicherheitsregeln
+54 -40
View File
@@ -3,7 +3,7 @@
> **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.
**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
### 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
- `frontend_net` = Proxy-/Web-Netz
- `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`
Postgres, Redis und ähnliche Dienste laufen ausschließlich in `backend_net` oder einem eigenen internen Compose-Netz.
### 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
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 |
| `backend_net` | bridge, `internal: true` | interne App-/DB-/Cache-Kommunikation | Standard |
| `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 |
| `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 |
| `grafana_influx_lan` | Compose-intern, bridge | nicht-oeffentliches Zusatznetz nur fuer Docker Host-Port-Publishing von InfluxDB 8181 | ✅ umgesetzt |
| `monitoring_net` | Compose-intern, bridge | zentraler Observability-Stack fuer Prometheus, Loki, Grafana, Promtail, Exporter und InfluxDB | Zielzustand |
| `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 |
### 3.2 Finales Diagramm (vereinfacht)
@@ -103,7 +104,7 @@ traefik (80/443)
└── frontend_net
├── ö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)
└── Dienste mit Internetbedarf ohne öffentliche UI (ddns-updater)
@@ -118,11 +119,11 @@ dns_net
└── unbound
App-interne Netze
├── mealie_mealie_internal (internal: true) ✅
├── mealie_internal (internal: true) ✅
├── immich_default (internal: true) ✅
├── nextcloud_internal (internal: true) ✅
├── grafana_influx_internal (internal: true)
└── grafana_influx_lan (Bridge fuer LAN-Port-Publishing, keine Traefik-Route)
├── monitoring_net (zentraler Observability-Stack)
└── monitoring_influx_lan (Bridge fuer LAN-Port-Publishing, keine Traefik-Route)
Host-Sonderfälle
├── tailscale
@@ -149,22 +150,20 @@ Diese Dienste sind über echte `*.kaleschke.info`-Domains erreichbar:
Diese Dienste sind **keine Public Apps**:
- `Komodo` — komodo.kaleschke.info (Traefik, aber bewusst ohne zentrale Middleware; native Auth bleibt aktiv)
- `UptimeKuma` — uptime.kaleschke.info (Middleware)
- `filebrowser` — files.kaleschke.info (Middleware)
- `scrutiny` — scrutiny.kaleschke.info (Middleware)
- `code-server` — Traefik + Middleware
- `backrest` — Traefik + Middleware
- `borg-ui` — borg.kaleschke.info (Middleware)
- `homepage`home.kaleschke.info (Middleware)
- `glance`glance.kaleschke.info (Middleware)
- `paperless-gpt` — paperless-gpt.kaleschke.info (Middleware)
- `mail-archiver` — mail.kaleschke.info (Middleware + App-Auth)
- `glances` — glances.kaleschke.info (Middleware)
- `speedtest-tracker` — speedtest.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)
- `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
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 |
|---|---|---|---|---|---|
| `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 | — |
| `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 |
| `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
@@ -256,7 +253,7 @@ Legende Status:
|---|---|---|---|---|---|
| `postgresql17` | ✅ | `backend_net` | intern | kein Host-Port, `POSTGRES_PASSWORD_FILE` | — |
| `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_redis` | ⏳ | `immich_default` | intern | intern-only | anonymes Volume → named volume |
| `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` | — |
| `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 | — |
| `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_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 |
| `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
@@ -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 |
| `code-server` | ✅ | `frontend_net` | Traefik + Middleware | `PASSWORD_FILE` aktiv | — |
| `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 |
| `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 |
@@ -292,18 +290,21 @@ Legende Status:
| 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` | — |
| `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` | — |
| `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 |
| `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-grafana` | ✅ | `frontend_net`, `monitoring_net` | Traefik + Middleware | zentrale UI via `monitoring.kaleschke.info`; Datasources fuer Prometheus, Loki und InfluxDB | — |
| `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
| 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
@@ -319,11 +320,15 @@ Legende Status:
| `dashdot` | 2026-03-28 | nicht mehr aktiv |
| `netdata` | 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 |
| `PortainerCE` | 2026-03-29 | abgeschaltet; Komodo ist alleiniger Stack-Manager |
| `beszel` | 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 |
| `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 |
| `scrutiny` | `privileged: true` | SMART-Datenzugriff auf Laufwerke |
| `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 |
| `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` |
| `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 |
| `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` |
| `grafana`, `influxdb3-core` | `user: "0"` | aktueller Live-Stand fuer Host-Appdata-/Plugin-Permissions; als Ausnahme dokumentiert, spaeter separat mit UID/GID-Test haerten |
| `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 |
| `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`)
- Netzwerke: `dns_net` (feste IP 172.23.0.3) + `frontend_net`
- 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`
### 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)
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:...`.
- 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.
### 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.
- `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 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.
- `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 `influxdb3-core` zusaetzlich zum internen Grafana-Netz `grafana_influx_lan`. Das ist keine Public-App-Freigabe und ersetzt nicht die Token-Authentifizierung.
- `monitoring/` ist der zentrale Zielstack fuer Prometheus, Loki, Promtail, Grafana, node-exporter, cAdvisor und InfluxDB 3 Core.
- `monitoring-grafana` wird als geschuetztes Monitoring-UI unter `monitoring.kaleschke.info` betrieben.
- `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.
- `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 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
- 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`.
---
+14 -3
View File
@@ -13,6 +13,14 @@ Bei Restore-, Host-Ausfall- oder Wiederanlauf-Fragen zusaetzlich:
3. `docs/DISASTER_RECOVERY.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
@@ -38,7 +46,8 @@ Bei Restore-, Host-Ausfall- oder Wiederanlauf-Fragen zusaetzlich:
- `security/` -> sicherheitskritische Dienste
- `infra/` -> Datenbanken und technische Services
- `apps/` -> Anwendungen
- `ops/` -> Monitoring und Tools
- `ops/` -> operative Tools
- `monitoring/` -> zentraler Observability-Stack
- `host-services/` -> Dienste mit Host-Netz
- `traefik/` -> Reverse Proxy Konfiguration
- `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 bleibt bewusst bei nativer Authentifizierung; zentrale Traefik-Auth wird dort nicht pauschal vorgeschaltet.
- 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.
- 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.
- 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`.
- `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.
-42
View File
@@ -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
+2 -2
View File
@@ -12,10 +12,10 @@ services:
DB_PASSWORD: ${IMMICH_DB_PASSWORD}
DB_DATABASE_NAME: immich
REDIS_HOSTNAME: redis
TZ: Europe/Berlin
volumes:
- /mnt/user/photos/immich:/usr/src/app/upload
- /mnt/user/photos/family_archive:/usr/src/app/external
- /etc/localtime:/etc/localtime:ro
networks:
- immich_default
- frontend_net
@@ -43,7 +43,7 @@ services:
redis:
container_name: immich_redis
image: redis:7
image: redis:7.4-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
restart: unless-stopped
networks:
- immich_default
+1 -2
View File
@@ -14,13 +14,12 @@ services:
POSTGRES_SERVER: mealie-postgres
POSTGRES_DB: mealie
POSTGRES_USER: mealie
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
POSTGRES_PASSWORD: ${MEALIE_POSTGRES_PASSWORD}
BASE_URL: https://mealie.kaleschke.info
volumes:
- /mnt/user/appdata/mealie/data:/app/data
- /mnt/user/appdata/secrets/mealie_postgres_password.txt:/run/secrets/postgres_password:ro
networks:
- frontend_net
+2 -2
View File
@@ -1,6 +1,6 @@
services:
nextcloud:
image: nextcloud:33.0.2-apache
image: nextcloud:33.0.2-apache@sha256:39b2ba219271a22851f8409a7b1295d5892aba1696d9193500311c02e60591a4
container_name: nextcloud
restart: unless-stopped
depends_on:
@@ -64,7 +64,7 @@ services:
- no-new-privileges:true
nextcloud-redis:
image: redis:7.4-alpine
image: redis:7.4-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
container_name: nextcloud-redis
restart: unless-stopped
command: redis-server --save 60 1 --loglevel warning
+1 -1
View File
@@ -1,6 +1,6 @@
services:
unbound:
image: shaanmajid/unbound:latest@sha256:0a163e92e55698ddc45c0265884a86c7363da245ab4a909a8e5cb0f541aeeb4d
image: shaanmajid/unbound:1.24.2@sha256:d278b71c592b2555cc802911bb0757a6a24f4a8ad7f5848720296c04876eeb63
container_name: unbound
restart: unless-stopped
volumes:
+9 -1
View File
@@ -11,9 +11,17 @@ services:
- GITEA__server__DOMAIN=git.kaleschke.info
- GITEA__server__ROOT_URL=https://git.kaleschke.info/
- 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:
- /mnt/user/services/gitea/data:/data
dns:
- 1.1.1.1
- 8.8.8.8
ports:
- "222:22"
networks:
+17 -10
View File
@@ -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`.
- Komodo ist Stack-Manager und Deploy-Consumer.
- 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
@@ -50,7 +52,7 @@ Traefik ist der zentrale Web-Einstieg fuer HTTP(S). Admin-/Ops-UIs liegen entwed
### 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
@@ -78,11 +80,14 @@ Nach Aenderungen an `services.json` oder `check_health.py`: `git pull` auf der V
### Monitoring / Metriken
- Grafana laeuft unter `grafana.kaleschke.info` hinter Traefik + Authelia.
- InfluxDB 3 Core ist nicht public und nicht im `frontend_net`.
- Zielzustand ist ein zentraler Stack `monitoring/` unter `https://monitoring.kaleschke.info`.
- `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`.
- 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
@@ -99,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.
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
| Netzwerk | Bedeutung |
@@ -106,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 |
| `backend_net` | internes Netz fuer shared PostgreSQL, Redis und Backends |
| `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 |
Regeln:
@@ -114,7 +121,7 @@ Regeln:
- Datenbanken nie ins `frontend_net`.
- Admin-UIs nur mit Traefik + Middleware oder dokumentierter Ausnahme.
- 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
@@ -129,12 +136,12 @@ Bekannte Ausnahmen:
- Traefik: 80/443
- 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`
- Scrutiny: privileged
- Komodo: Docker-Socket, native Auth
- 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
## Backup- und Restore-Modell
@@ -183,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-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`.
- `homepage`, `glances` und `komodo-periphery` nutzen Docker-Socket-Mounts; Zugriff bewusst behandeln.
- `backrest`, `borg-ui` und `filebrowser` haben breite Mounts; bei Hardening nicht ad hoc, sondern gezielt vorgehen.
- `glance-docker-socket-proxy`, `glances` und `komodo-periphery` nutzen Docker-/Socket-Zugriff; Zugriff bewusst behandeln.
- `borg-ui` und `filebrowser` haben breite Mounts; bei Hardening nicht ad hoc, sondern gezielt vorgehen.
- `scrutiny` ist privilegiert und hat Device-Mounts.
- `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.
+31
View File
@@ -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.
+369
View File
@@ -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. 97139 (`dump_sqlite_container`), Z. 253258 (Nextcloud `pg_dump`), Z. 261264 (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. 2325). |
| **(2) Filebrowser entschaerfen** — `/mnt/user/appdata:/srv/appdata` weg, gezielte RW-Subpfade | offen | ✅ erledigt | `ops/filebrowser/docker-compose.yml` Z. 1116. 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. 1725. 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 | 12 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 | 12 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.
+22
View File
@@ -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.
+88
View File
@@ -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.
+895
View File
@@ -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 515 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 (12 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 (23 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 (34 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 510 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 05 und bist nach < 8 h wieder im Vollbetrieb. Repo-Bootstrap aus GitHub-Mirror, Stacks in Stufen 15, 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 13 in 46 Wochen sitzen, bist du auf einer 1-Note. Wenn dann Sprint 45 in weiteren 68 Wochen kommen, hat die Familie ein echtes Self-Hosting-System, kein "Container-Sammlung im Keller". Das ist der Unterschied, den der Audit-Auftrag adressiert.
+95
View File
@@ -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
```
+80
View File
@@ -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`.
+65
View File
@@ -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 |
+32
View File
@@ -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.
+34 -26
View File
@@ -9,6 +9,8 @@ Verwandte Dokumente:
- `docs/ROLLBACK.md` - Rueckweg bei Fehlern im laufenden GitOps-Betrieb
- `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
---
@@ -60,12 +62,14 @@ Diese Punkte sollten **vor** einem echten Ausfall geklaert sein:
| Thema | Sollzustand |
|---|---|
| Repo-Zugang ausserhalb von Gitea | externer Mirror oder lokaler aktueller Clone vorhanden |
| Unraid USB-/Flash-Backup | eingerichtet und wiederherstellbar |
| Repo-Zugang ausserhalb von Gitea | privater GitHub-Push-Mirror `michaelkaleschke-spec/homelab-infra` und lokaler aktueller Clone vorhanden |
| 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-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 |
| 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 |
**Wichtig:** Dieses Dokument ist nur so gut wie die Vorbereitung ausserhalb des Repos.
@@ -81,13 +85,13 @@ Deshalb gilt:
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.
Empfohlene Wege:
Verfuegbare Wege:
- externer Push-Mirror
- externer Push-Mirror: `https://github.com/michaelkaleschke-spec/homelab-infra`
- lokaler Bare-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.
---
@@ -100,6 +104,8 @@ Wenn **kein externer Repo-Zugang** besteht, ist `services/gitea/data` selbst ein
3. Array wieder zuweisen und starten
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
Mindestens diese Pfade muessen wieder verfuegbar sein:
@@ -165,7 +171,6 @@ Diese Werte sind vor dem Start der betroffenen Dienste zu pruefen bzw. wieder in
- `KOMODO_JWT_SECRET`
- `KOMODO_MONGO_PASSWORD`
- `KOMODO_PERIPHERY_PASSKEY`
- relevante `HOMEPAGE_VAR_*`
- `APP_KEY` und `ADMIN_PASSWORD` fuer `speedtest-tracker`
### 6.3 Rechte
@@ -200,11 +205,16 @@ Besonders kritisch:
- `/mnt/user/appdata/secrets`
- `/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/appdata/authelia/config`
- `/mnt/user/appdata/komodo/core`
- `/mnt/user/appdata/komodo/periphery`
- `/mnt/user/backups/borg/dumps/latest`
- `/mnt/user/backups/borg/dumps/latest/unraid-flash-config.tar.gz`
- dienstspezifische App- und Nutzdatenpfade
**Nicht blind alles extrahieren**, wenn nur einzelne Pfade oder Dienste betroffen sind.
@@ -263,20 +273,18 @@ Ziel:
### Stufe 5 - Restliche Apps und Ops
15. `apps/homepage/`
16. `apps/ntfy/`
17. `apps/paperless-gpt/`
18. `apps/bentopdf/`
19. `ops/uptime-kuma/`
20. `ops/borg-ui/`
21. `ops/backrest/`
22. `ops/filebrowser/`
23. `ops/glances/`
24. `ops/scrutiny/`
25. `ops/speedtest/`
26. `ops/grafana-influxdb/`
27. `ops/hermes-agent/`
28. `infra/ddns-updater/`
15. `apps/ntfy/`
16. `apps/paperless-gpt/`
17. `apps/bentopdf/`
18. `ops/glance/`
19. `ops/borg-ui/`
20. `ops/filebrowser/`
21. `ops/glances/`
22. `ops/scrutiny/`
23. `ops/speedtest/`
24. `monitoring/`
25. `ops/hermes-agent/`
26. `infra/ddns-updater/`
**Regel:** Nach jeder Stufe kurz pruefen, bevor die naechste beginnt.
@@ -311,7 +319,7 @@ Ziel:
- Borg UI startet und kennt sein Repo noch
- 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
---
@@ -368,6 +376,7 @@ Relevant:
- Dump-Ziel: `/mnt/user/backups/borg/dumps/latest`
- 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
@@ -384,15 +393,14 @@ Smoke-Test: `hermes-gateway` healthcheck ist gruen, `hermes.kaleschke.info` leit
### 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
- externer Repo-Zugang fuer den Ernstfall absichern
- Unraid-USB-/Flash-Backup pruefen
- Borg-Passphrase extern sicher hinterlegen
- Unraid-USB-/Flash-Backup regelmaessig ueber `unraid-flash-config.tar.gz` und optional Unraid Connect pruefen
- Borg-Passphrase aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` extern analog sicher hinterlegen
- Komodo Stack-ENV-Werte zentral ausserhalb von Komodo dokumentieren
- regelmaessige automatisierte Restore-Smoke-Tests fuer Vaultwarden, Gitea und Paperless etablieren
- `komodo-mongo`-Dump nach Major-Upgrades gezielt kontrollieren
+77
View File
@@ -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.
+68
View File
@@ -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 |
+38
View File
@@ -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 |
+7 -7
View File
@@ -90,18 +90,18 @@ Sollzustand:
Soll fuer Home Assistant:
```bash
cd /mnt/user/services/stacks/grafana
cd /mnt/user/services/stacks/monitoring
git fetch --all --prune
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:
```bash
docker network ls | grep -E "grafana|influx"
docker inspect influxdb3-core --format '{{json .NetworkSettings.Networks}}'
docker inspect influxdb3-core --format '{{json .NetworkSettings.Ports}}'
docker network ls | grep -E "monitoring|influx"
docker inspect monitoring-influxdb3-core --format '{{json .NetworkSettings.Networks}}'
docker inspect monitoring-influxdb3-core --format '{{json .NetworkSettings.Ports}}'
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep influx
ss -ltnp | grep 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:
- 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`.
- `ss` zeigt `docker-proxy` auf `192.168.178.58:8181`.
- `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
+187
View File
@@ -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
```
+3 -3
View File
@@ -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.
In Komodo/Stack-Environment fuer `ops/grafana-influxdb` setzen:
Im Zielzustand in Komodo/Stack-Environment fuer `monitoring` setzen:
```env
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:
@@ -97,7 +97,7 @@ ha core restart
## 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
SHOW TABLES
+168 -2
View File
@@ -8,6 +8,7 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab
- Komodo ist der einzige produktive Stack-Manager.
- Portainer CE ist 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.
- GitHub Desktop ist der bevorzugte lokale Workflow fuer `Fetch`, `Pull`, `Commit` und `Push`.
- Mutable Image-Tags sind auf die aktuell laufenden Digests eingefroren.
@@ -16,6 +17,162 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab
## 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.
@@ -72,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.
- 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
- 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.
@@ -90,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`).
- 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.
- 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
@@ -137,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.
- Dump-Zielpfad auf `/mnt/user/backups/borg/dumps` umgestellt.
- 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
+106
View File
@@ -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 |
+41
View File
@@ -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.
+89
View File
@@ -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
View File
@@ -1,6 +1,6 @@
# 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.
@@ -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 |
| `infra/` | technische Infrastruktur wie PostgreSQL, Redis, DDNS |
| `ops/` | Operations-, Backup-, Monitoring- und Admin-Tools |
| `services/` | Host-seitige Betriebsskripte und Recovery-kritische Service-Hilfen |
| `security/` | Identity/Security-Dienste wie Authelia und Vaultwarden |
| `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/GITOPS_DRIFT_RUNBOOK.md` | Pflichtmatrix fuer Git/Gitea/Komodo/Docker/Host-Drift |
| `docs/DISASTER_RECOVERY.md` | Wiederanlauf nach Host-/Systemausfall |
| `docs/RESTORE_MATRIX.md` | Restore-Quellen und Smoke-Tests je Dienst |
| `docs/ROLLBACK.md` | Rueckweg im GitOps-Betrieb |
| `docs/RESTORE_MATRIX.md` | Restore-Quellen, Dump-Artefakte und Smoke-Tests je Dienst |
| `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/HOME_ASSISTANT_INFLUXDB_ECOWITT.md` | Home Assistant -> InfluxDB 3 -> Grafana Ablauf |
| `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/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 |
| `ops/grafana-influxdb/provisioning/datasources/influxdb.yml` | Grafana Datasource Provisioning fuer InfluxDB 3 Core |
| `ops/borg-ui/scripts/pre-backup-dumps.sh` | Host-seitiges Dump-Skript fuer PostgreSQL, Mealie, Immich und Komodo Mongo |
| `monitoring/prometheus/prometheus.yml` | Prometheus Scrape-Konfiguration fuer dedizierten Monitoring-Stack |
| `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/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/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 |
## 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 |
|---|---|---|---|---|---|---|
| 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` |
| 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 |
@@ -73,35 +88,35 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
| 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 |
| 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 |
| 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
| 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 |
### Operations
| 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 |
| 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 |
| Filebrowser | `ops/filebrowser/docker-compose.yml` | `filebrowser` -> `filebrowser/filebrowser:latest@sha256:...` | `files.kaleschke.info` | `frontend_net` | keine | Appdata-Mount, Admin-UI hinter Authelia |
| 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: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 |
| 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 |
| 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 |
| 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 |
| Uptime Kuma | `ops/uptime-kuma/docker-compose.yml` | `UptimeKuma` -> `louislam/uptime-kuma:1@sha256:...` | `uptime.kaleschke.info` | `frontend_net` | keine | Monitor-State in Appdata |
| 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 |
### Traefik
@@ -114,20 +129,19 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
| Host | Service | Zugriff |
|---|---|---|
| `auth.kaleschke.info` | Authelia | Auth provider / bypass fuer eigene Domain |
| `backrest.kaleschke.info` | Backrest | Traefik + Authelia |
| `borg.kaleschke.info` | Borg UI | Traefik + Authelia |
| `cloud.kaleschke.info` | Nextcloud | Traefik, native App-Auth |
| `code.kaleschke.info` | code-server | Traefik + Authelia |
| `files.kaleschke.info` | Filebrowser | Traefik + Authelia |
| `git.kaleschke.info` | Gitea Web | Traefik |
| `glance.kaleschke.info` | Glance | Traefik + Authelia |
| `glances.kaleschke.info` | Glances | Traefik + Authelia |
| `grafana.kaleschke.info` | Grafana | 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 |
| `komodo.kaleschke.info` | Komodo | Traefik, native Komodo-Auth; keine pauschale ForwardAuth |
| `mail.kaleschke.info` | Mail Archiver | Traefik + Authelia + App-Auth |
| `mealie.kaleschke.info` | Mealie | Traefik |
| `monitoring.kaleschke.info` | Monitoring Grafana | Traefik + Authelia |
| `ntfy.kaleschke.info` | ntfy | Traefik |
| `paperless.kaleschke.info` | Paperless-ngx | Traefik |
| `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 |
| `speedtest.kaleschke.info` | Speedtest Tracker | Traefik + Authelia |
| `traefik.kaleschke.info` | Traefik Dashboard | Traefik + Authelia |
| `uptime.kaleschke.info` | Uptime Kuma | Traefik + Authelia |
| `vault.kaleschke.info` | Vaultwarden | Traefik |
## 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 |
| `dns_net` | App-/Host-Netz | AdGuard Home und Unbound |
| `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 |
| `grafana_influx_internal` | Compose-intern, `internal: true` | Grafana und InfluxDB |
| `grafana_influx_lan` | Compose-intern bridge | InfluxDB Host-Port-Publishing fuer LAN Writer |
| `monitoring_net` | Compose-/Stack-Netz bridge | Prometheus, Loki, Promtail, Monitoring-Grafana, node-exporter, cAdvisor; Traefik fuer Metrics-Scrape |
| `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 |
| `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
@@ -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` |
| 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` |
| 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` |
| 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` |
| 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 |
| code-server | `/mnt/user/appdata/code-server`, `/mnt/user/services/dev`, Homepage production mount |
| Filebrowser | `/mnt/user/appdata`, Filebrowser database/config paths |
| code-server | `/mnt/user/appdata/code-server`, `/mnt/user/services/dev` |
| 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` |
| Scrutiny | `/mnt/user/appdata/scrutiny/*`, `/run/udev`, selected `/dev/...` disks |
| Speedtest | `/mnt/user/appdata/speedtest-tracker/config` |
| Uptime Kuma | `/mnt/user/appdata/uptime-kuma` |
| Grafana/InfluxDB | `/mnt/user/appdata/grafana`, Grafana provisioning, `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` |
| 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 |
| 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` |
@@ -198,33 +211,36 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
| Paperless | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS` als Komodo Stack ENV |
| Immich | `IMMICH_DB_PASSWORD` Stack ENV; `immich_postgres_password.txt` fuer Postgres |
| 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 |
| 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 |
| 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 |
| 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
| 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` |
| `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.
## Unsicherheiten / TODOs aus Repo-Sicht
- 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.
- `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.
- 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.
- `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.
- `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`.
+1 -1
View File
@@ -193,7 +193,7 @@ bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh paperles
### Optional mit `ntfy`
```bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-restore
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-info
```
---
+24 -13
View File
@@ -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 |
|---|---|---|---|---|---|---|
| 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 |
| 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 |
| 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 |
| 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` | `borg_repo_passphrase.txt` fuer Restore-Tests | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert; 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 |
| Vaultwarden | Borg / Share | `/mnt/user/appdata/vaultwarden` | dateibasiert | `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 |
| 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`, `/mnt/user/services/stacks` | `komodo-mongo.archive.gz` falls verifiziert | `komodo_mongo_password.txt`, `KOMODO_*` Stack ENV | Traefik, Mongo, Gitea | UI erreichbar, Periphery verbunden |
| 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 |
---
@@ -46,8 +48,8 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
| 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 |
| 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 |
| Homepage | Borg / Share | `/mnt/user/appdata/homepage` | keine | `HOMEPAGE_VAR_*` | Traefik, Authelia | Dashboard startet, Widgets laden |
| 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 |
| 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 |
| 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 |
|---|---|---|---|---|---|---|
| Borg UI | Borg / Share | `/mnt/user/appdata/borg-ui/data` | keine eigene DB | 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 |
| 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 |
| Borg UI | Borg + Dump | `/mnt/user/appdata/borg-ui/data` | `borg-ui.sqlite` | Borg-Repo-Creds in `/data` | Traefik | UI startet, Repo-Verbindung bekannt |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 | 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 |
| 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 |
---
@@ -82,6 +84,15 @@ Aktuell relevante Dump-Artefakte unter `/mnt/user/backups/borg/dumps/latest`:
- `postgresql17-authelia.dump` (optional / wenn vorhanden)
- `mealie.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)
Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.sh` vorgesehen.
+33 -1
View File
@@ -89,11 +89,43 @@ Vor dem ersten produktiven Einsatz reicht es, den vorbereiteten Stack nicht zu d
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
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
## 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
+22 -10
View File
@@ -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 |
| 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 |
| 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 |
| Paperless-ngx | DB Password | Stack ENV `${PAPERLESS_DBPASS}` | 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-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 | 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 | 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 | 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 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 |
| 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-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 |
| Hermes Agent | Provider-Keys, Bot-Tokens, API-Server-Key | `/mnt/user/appdata/hermes-agent/data/.env` | neu |
| Hermes Agent | SSH-Runner Private Key | `/mnt/user/appdata/secrets/hermes_runner_id_ed25519` -> `/root/.ssh/id_ed25519` | neu |
| Grafana | Admin Password | `/mnt/user/appdata/secrets/grafana_admin_password.txt` -> `GF_SECURITY_ADMIN_PASSWORD__FILE` | vorbereitet |
| InfluxDB 3 Core | Admin Token JSON | `/mnt/user/appdata/secrets/influxdb3_admin_token.json` -> Docker Secret `/run/secrets/influxdb3_admin_token` | vorbereitet |
| Grafana -> InfluxDB | Datasource Token | `/mnt/user/appdata/secrets/grafana_influxdb_token.txt` -> Docker Secret `/run/secrets/grafana_influxdb_token` | vorbereitet |
| Borg Repo | Borg-Passphrase fuer Restore-Tests und Notfallzugriff | `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` -> Host-Secret-Datei, nicht im Repo | aktiv |
| Unraid Flash Backup | Boot-/Array-/Share-/Plugin-Konfiguration, ggf. Hashes/Keys/Templates | `/mnt/user/backups/borg/dumps/latest/unraid-flash-config.tar.gz`, via Borg/Hetzner gesichert | aktiv; wie Secret-Material behandeln |
| Hermes Agent | Provider-Keys, Bot-Tokens, API-Server-Key | `/mnt/user/appdata/hermes-agent/data/.env` | VM-seitig offen |
| Hermes Agent | SSH-Runner Private Key | `/mnt/user/appdata/secrets/hermes_runner_id_ed25519` -> `/root/.ssh/id_ed25519` | VM-seitig offen |
| 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 |
---
@@ -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 |
| 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_postgres_password.txt
|-- authelia_session_secret.txt
|-- authelia_smtp_password.txt
|-- authelia_storage_encryption_key.txt
|-- immich_postgres_password.txt
|-- komodo_mongo_password.txt
|-- mealie_postgres_password.txt
|-- monitoring_grafana_admin_password.txt
|-- monitoring_grafana_influxdb_token.txt
|-- nextcloud_admin_password.txt
|-- nextcloud_admin_user.txt
|-- nextcloud_postgres_password.txt
|-- postgres_password.txt
|-- redis_password.txt
|-- grafana_admin_password.txt
|-- grafana_influxdb_token.txt
|-- borg_repo_passphrase.txt
|-- influxdb3_admin_token.json
|-- filebrowser_admin_password.txt
`-- vaultwarden_admin_token.txt
```
@@ -87,6 +97,8 @@ Weitere dokumentierte Secret-Pfade:
- `/mnt/user/appdata/secrets/hermes_runner_id_ed25519`
- `/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.
- 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.
---
+100
View File
@@ -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
View File
@@ -1,6 +1,6 @@
# 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.
@@ -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 |
|---|---|---|---|---|---|---|---|---|
| `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 |
| `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
| 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 |
| `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
@@ -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-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 |
| `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-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` | 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` | `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 |
| `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 |
## Operations / Monitoring / Admin
| 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-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 |
| `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 |
| `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 |
| `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 |
| `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 |
| `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 |
| `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 |
| `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 |
| `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 |
| `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 |
| `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 |
| `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 | kein Docker-Socket; terminal backend `ssh` auf dedizierte VM (192.168.178.143, User `hermes`, Repo unter `/srv/hermes-workspace/homelab-infra/`); Ops-Monitor-Skill `homelab-ops-monitor` aktiv; echte `.env` auf Host-Appdata |
| `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 |
| `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 |
| `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/...` |
| `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/...` |
| `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 |
| `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 |
| `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 |
| `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 |
| `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 |
| `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` |
| `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 |
| `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 |
| `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
@@ -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.
- 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
- 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.
- 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.
+415
View File
@@ -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.
+20 -18
View File
@@ -115,6 +115,23 @@ Komodo ist in diesem Setup:
- Pushes koennen automatisch einen Komodo-Deploy ausloesen
- 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
Komodo bleibt **bewusst** ohne zentrale Traefik-ForwardAuth-Middleware.
@@ -301,29 +318,14 @@ Nach jeder erfolgreichen Migration oder relevanten Aenderung muessen diese Datei
- `docs/MIGRATION_LOG.md`
- `docs/SECRETS_MAP.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
- `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
Jede Aenderung muss rueckrollbar sein. Vor jedem Deploy muss klar sein:
+103
View File
@@ -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.
+550
View File
@@ -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.
+1 -1
View File
@@ -1,4 +1,4 @@
BASE_DOMAIN=kaleschke.info
TRAEFIK_DOMAIN=traefik.kaleschke.info
AUTH_DOMAIN=auth.kaleschke.info
HOME_DOMAIN=home.kaleschke.info
GLANCE_DOMAIN=glance.kaleschke.info
+1 -1
View File
@@ -13,7 +13,7 @@ services:
ports:
- "53:53/tcp"
- "53:53/udp"
- "8082:80"
- "100.80.98.33:8082:80"
security_opt:
- no-new-privileges:true
+19
View File
@@ -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
+2
View File
@@ -3,6 +3,8 @@ services:
image: ghcr.io/qdm12/ddns-updater:latest@sha256:ee16ab4f6203bf9e5b0925d38a0b4ebf2d9f23771f933cfb2f5a2dbd5f9a2f88
container_name: ddns-updater
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- frontend_net
environment:
+1 -1
View File
@@ -1,6 +1,6 @@
services:
redis:
image: redis:7-alpine
image: redis:7.4-alpine@sha256:6ab0b6e7381779332f97b8ca76193e45b0756f38d4c0dcda72dbb3c32061ab99
container_name: Redis
restart: unless-stopped
command:
+91
View File
@@ -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()
+25
View File
@@ -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
+24
View File
@@ -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
+364
View File
@@ -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
+47
View File
@@ -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
+58
View File
@@ -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."
+73
View File
@@ -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
+34
View File
@@ -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
-48
View File
@@ -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
+1 -3
View File
@@ -57,7 +57,6 @@ Der technische Scope für `critical_infra` ist in `all-important-sources.txt` fe
- `/local/secrets`
- `/local/appdata/authelia/config`
- `/local/appdata/traefik`
- `/local/appdata/homepage`
- `/local/appdata/ntfy`
- `/local/appdata/paperless-gpt`
- `/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 |
| Authelia | Config + Secrets | Shared PostgreSQL Dump vorgesehen / erzeugt | 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 |
| Paperless-GPT | lokale Daten / Prompts | 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:
- `postgresql17-globals.sql` 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.
## Was aktuell bewusst nicht als Problem gewertet wird
+23 -24
View File
@@ -1,6 +1,6 @@
# 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.
@@ -11,66 +11,62 @@ Use Borg as the single backup system for:
- critical file-backed application data
- secrets, keys, and reverse-proxy state
- 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.
## 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.
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 Unraid flash configuration archive is intentional as well and must be treated as secret backup material.
## Service Inventory
| 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` |
| 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` |
| 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` |
| Traefik | file data | `/local/appdata/traefik` |
| Homepage | file data | `/local/appdata/homepage` |
| ntfy | file data | `/local/appdata/ntfy` |
| Paperless-GPT | file data | `/local/appdata/paperless-gpt` |
| Tailscale | file data | `/local/appdata/tailscale` |
| 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` |
| Nextcloud | raw DB path + file data | `/local/appdata/nextcloud/html`, `/local/appdata/nextcloud/postgres`, `/local/appdata/nextcloud/redis`; user data path see gap below |
| Grafana | file data | `/local/appdata/grafana` |
| GitOps host automation | repo clone + Komodo workspaces + host-check state | `/local/services/homelab-infra`, `/local/services/stacks`, `/local/services/posture-check` |
| 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` |
| 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 |
## Open Decisions and Coverage Gaps
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.
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.
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.
### 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.
### 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
### Shared PostgreSQL (`postgresql17`)
@@ -83,16 +79,21 @@ Until decided, the raw path is what Borg sees today and is the only Nextcloud DB
- `mealie`
- `immich`
- `nextcloud`
### Other Databases
- 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
- `/mnt/user/appdata/postgresql17`
- `/mnt/user/appdata/mealie/postgres`
- `/mnt/user/appdata/immich_postgres`
- `/mnt/user/appdata/nextcloud/postgres`
- `/mnt/user/appdata/komodo/mongo`
- `/mnt/user/appdata/redis`
- `/mnt/user/appdata/scrutiny/influxdb`
@@ -104,10 +105,8 @@ These are not part of the first-class Borg scope:
- Plex metadata and cache
- AdGuard query log
- code-server extensions cache
- uptime-kuma
- scrutiny metrics history
- dozzle, glances, speedtest
- filebrowser app state
## Suggested Retention
+3 -1
View File
@@ -12,7 +12,6 @@
/local/secrets
/local/appdata/authelia/config
/local/appdata/traefik
/local/appdata/homepage
/local/appdata/ntfy
/local/appdata/paperless-gpt
/local/appdata/tailscale
@@ -20,3 +19,6 @@
/local/appdata/borg-ui/data
/local/appdata/komodo/periphery
/local/appdata/komodo/core
/local/services/homelab-infra
/local/services/stacks
/local/services/posture-check
+2
View File
@@ -20,8 +20,10 @@ services:
- /mnt/user/documents/scans_inbox:/local/paperless/consume:ro
- /mnt/user/documents/paperless:/local/paperless/media: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/family_archive:/local/immich/external:ro
- /mnt/user/services:/local/services:ro
- /mnt/user/services/gitea/data:/local/gitea/data:ro
- /mnt/user/appdata/borg-ui/restore:/restore
dns:
+4
View File
@@ -14,6 +14,10 @@ Fresh dump artifacts are written to:
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
- The script is written for host execution where `docker` is available.
+33 -15
View File
@@ -1,10 +1,10 @@
# 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
The pre-backup dump refresh should run:
The Borg pre-flight should run:
- on the Unraid host
- 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
`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 host paths under `/mnt/user/...`
@@ -24,24 +30,36 @@ That makes host execution simpler, more transparent, and lower-risk than giving
## Recommended rollout
1. Store the script on the host, for example at:
- `/mnt/user/appdata/borg-ui/scripts/pre-backup-dumps.sh`
2. Make it executable:
- `chmod +x /mnt/user/appdata/borg-ui/scripts/pre-backup-dumps.sh`
3. Create a User Scripts entry such as:
1. Use the repo clone on the host:
- `/mnt/user/services/homelab-infra`
2. Make the scripts executable:
- `chmod +x /mnt/user/services/homelab-infra/ops/borg-ui/scripts/pre-borg.sh`
- `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`
4. Let that entry run:
- on a fixed schedule before the expected Borg backup window
- or manually before ad hoc Borg runs
5. Keep Borg UI focused on backing up `/local/borg-dumps`, not on generating the dumps itself.
4. Script body:
```bash
#!/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
The intended sequence is:
1. Host script refreshes `latest` dump artifacts.
2. Borg UI backs up `/local/borg-dumps` together with the rest of `critical_infra`.
3. Borg history preserves dump history, so the host only needs to keep the most recent dump set.
1. Host wrapper checks posture.
2. Host script refreshes `latest` dump artifacts.
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
+163
View File
@@ -68,6 +68,143 @@ dump_pg_globals() {
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() {
container="$1"
password="$2"
@@ -131,6 +268,9 @@ dump_mongo_container() {
main() {
need_cmd docker
need_cmd sqlite3
need_cmd tar
need_cmd sha256sum
ensure_dirs
# Shared PostgreSQL 17
@@ -162,9 +302,32 @@ main() {
warn "Skipping missing container: immich_postgres"
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
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"
}
+62
View File
@@ -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"
+2 -3
View File
@@ -1,6 +1,6 @@
services:
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
restart: unless-stopped
security_opt:
@@ -10,7 +10,7 @@ services:
- PUID=1000
- PGID=1000
- TZ=Europe/Berlin
- PASSWORD_FILE=/run/secrets/password
- FILE__PASSWORD=/run/secrets/password
- DEFAULT_WORKSPACE=/workspace
- PWA_APPNAME=KalliLab Code
@@ -18,7 +18,6 @@ services:
volumes:
- /mnt/user/appdata/code-server:/config
- /mnt/user/services/dev:/workspace
- /mnt/user/appdata/homepage:/prod/homepage
- /mnt/user/appdata/code-server/secrets/password:/run/secrets/password:ro
networks:
+4 -2
View File
@@ -1,6 +1,6 @@
services:
filebrowser:
image: filebrowser/filebrowser:latest@sha256:4dce87308b9f9cfbcf8d0a284fc9565d2b515530a6bae2d920b388161e093f26
image: filebrowser/filebrowser:v2.63.2@sha256:4dce87308b9f9cfbcf8d0a284fc9565d2b515530a6bae2d920b388161e093f26
container_name: filebrowser
restart: unless-stopped
security_opt:
@@ -9,7 +9,9 @@ services:
- PUID=99
- PGID=100
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/config:/config
command: ["--database", "/database/filebrowser.db"]
+879
View File
@@ -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
+56
View File
@@ -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
-81
View File
@@ -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.
-85
View File
@@ -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
+24 -43
View File
@@ -315,48 +315,41 @@
"first_check": "HTTPS erreichbar? NTFY_BEHIND_PROXY=true gesetzt? Traefik healthy?",
"notes": "KRITISCH: Ausfall bedeutet keine anderen Alerts ankommen"
},
"homepage": {
"description": "Start-Dashboard",
"glance": {
"description": "Homelab-Dashboard",
"tier": 3,
"category": "ops",
"container_name": "homepage",
"container_name": "glance",
"dependencies": ["traefik"],
"url": "https://home.kaleschke.info",
"url": "https://glance.kaleschke.info",
"dump_file": null,
"data_paths": ["/mnt/user/appdata/homepage"],
"first_check": "Traefik erreichbar? Docker-Socket read-only lesbar? API-Tokens gueltig?",
"notes": "Docker socket read-only; viele API Tokens in Config"
"data_paths": [],
"first_check": "Traefik erreichbar? Docker-Socket-Proxy intern erreichbar? API-Tokens fuer Widgets gueltig?",
"notes": "aktives Homelab-Dashboard; Homepage wurde entfernt"
},
"uptime-kuma": {
"description": "Monitoring / Uptime Checks",
"monitoring-grafana": {
"description": "Zentrale Observability-UI",
"tier": 3,
"category": "ops",
"container_name": "UptimeKuma",
"dependencies": ["traefik"],
"url": "https://uptime.kaleschke.info",
"container_name": "monitoring-grafana",
"dependencies": [
"monitoring-prometheus",
"monitoring-loki",
"monitoring-influxdb3-core",
"traefik"
],
"url": "https://monitoring.kaleschke.info",
"dump_file": null,
"data_paths": ["/mnt/user/appdata/uptime-kuma"],
"first_check": "Datenbank-Volume intakt? Traefik erreichbar?",
"notes": "Monitore nach Restore manuell pruefen"
"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"
},
"grafana": {
"description": "Metrik-Dashboard",
"monitoring-influxdb3-core": {
"description": "Zeitreihen- / Metrikdaten fuer Monitoring und Home Assistant",
"tier": 3,
"category": "ops",
"container_name": "grafana",
"dependencies": ["influxdb3-core", "traefik"],
"url": "https://grafana.kaleschke.info",
"dump_file": null,
"data_paths": ["/mnt/user/appdata/grafana"],
"first_check": "influxdb3-core healthy? Datasource-Token gesetzt? Provisioning-Konfig vorhanden?",
"notes": "laeuft als user 0; Datasource wird provisioniert"
},
"influxdb3-core": {
"description": "Zeitreihen- / Metrikdaten fuer Grafana und Home Assistant",
"tier": 3,
"category": "ops",
"container_name": "influxdb3-core",
"dependencies": [],
"container_name": "monitoring-influxdb3-core",
"dependencies": ["monitoring-grafana"],
"url": null,
"dump_file": null,
"data_paths": [
@@ -408,18 +401,6 @@
"first_check": "Borg-Repo-Credentials vorhanden? Backup-Mounts erreichbar? Traefik healthy?",
"notes": "breite Mounts bewusst dokumentiert; /local/secrets im DR-Scope"
},
"backrest": {
"description": "Backup-Admin-Dienst",
"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": {
"description": "Hermes Agent Gateway / AI Ops Assistant",
"tier": 3,
+21 -47
View File
@@ -395,55 +395,43 @@ services:
# TIER 3 — Ops / Tools (Ausfall schmerzt, blockiert nichts Kritisches)
# ---------------------------------------------------------------------------
homepage:
description: Start-Dashboard
glance:
description: Homelab-Dashboard
tier: 3
category: ops
container_name: homepage
container_name: glance
dependencies:
- traefik
url: https://home.kaleschke.info
url: https://glance.kaleschke.info
dump_file: null
data_paths:
- /mnt/user/appdata/homepage
first_check: "Traefik erreichbar? Docker-Socket read-only lesbar? API-Tokens gueltig?"
notes: "Docker socket read-only; viele API Tokens in Config"
data_paths: []
first_check: "Traefik erreichbar? Docker-Socket-Proxy intern erreichbar? API-Tokens fuer Widgets gueltig?"
notes: "aktives Homelab-Dashboard; Homepage wurde entfernt"
uptime-kuma:
description: Monitoring / Uptime Checks
monitoring-grafana:
description: Zentrale Observability-UI
tier: 3
category: ops
container_name: UptimeKuma
container_name: monitoring-grafana
dependencies:
- monitoring-prometheus
- monitoring-loki
- monitoring-influxdb3-core
- traefik
url: https://uptime.kaleschke.info
url: https://monitoring.kaleschke.info
dump_file: null
data_paths:
- /mnt/user/appdata/uptime-kuma
first_check: "Datenbank-Volume intakt? Traefik erreichbar?"
notes: "Monitore nach Restore manuell pruefen"
- grafana_data
first_check: "Authelia-Redirect? Datasources Prometheus, Loki und InfluxDB 3 Core gruen?"
notes: "ersetzt alten Grafana-Altstand und Uptime-Kuma-Views"
grafana:
description: Metrik-Dashboard
monitoring-influxdb3-core:
description: Zeitreihen- / Metrikdaten fuer Monitoring und Home Assistant
tier: 3
category: ops
container_name: grafana
container_name: monitoring-influxdb3-core
dependencies:
- influxdb3-core
- 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: []
- monitoring-grafana
url: null
dump_file: null
data_paths:
@@ -495,20 +483,6 @@ services:
first_check: "Borg-Repo-Credentials vorhanden? Backup-Mounts erreichbar? Traefik healthy?"
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:
description: Hermes Agent Gateway / AI Ops Assistant
tier: 3
+1
View File
@@ -32,6 +32,7 @@ powershell -ExecutionPolicy Bypass -File .\ops\policy-checks\check_repo.ps1 -Rep
- Host-Port-Mappings
- 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`
- digest-gepinnte mutable Tags bleiben sichtbar; nur explizit dokumentierte Ausnahmen werden als Info statt Warning gewertet
## Wichtige Betriebsregel
+9
View File
@@ -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
$identityText = ($service.ServiceName + ' ' + $service.ContainerName + ' ' + $service.Image).ToLowerInvariant()
foreach ($needle in @('postgres', 'redis', 'mongo', 'mysql', 'mariadb', 'influxdb')) {
@@ -358,6 +366,7 @@ $exceptionsRaw = Get-Content -LiteralPath $exceptionsPath -Raw | ConvertFrom-Jso
$exceptions = @{
middleware_exempt_identities = @($exceptionsRaw.middleware_exempt_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_host_network_identities = @($exceptionsRaw.allowed_host_network_identities)
allowed_host_port_identities = @{}
+9 -4
View File
@@ -16,12 +16,12 @@
"adguard": [
"53:53/tcp",
"53:53/udp",
"8082:80"
"100.80.98.33:8082:80"
],
"gitea": [
"222:22"
],
"influxdb3-core": [
"monitoring-influxdb3-core": [
"${INFLUXDB_BIND_IP:-127.0.0.1}:8181:8181"
],
"traefik": [
@@ -30,13 +30,18 @@
]
},
"allowed_root_identities": [
"grafana",
"influxdb3-core"
"monitoring-influxdb3-core"
],
"allowed_mutable_tag_identities": [
"ddns-updater",
"glances",
"scrutiny"
],
"allowed_privileged_identities": [
"scrutiny"
],
"allowed_host_network_identities": [
"plex",
"tailscale",
"Tailscale-Docker"
]
+10 -10
View File
@@ -1,28 +1,28 @@
# Policy Check Report
## Summary
- Compose files checked: 30
- Compose files checked: 29
- Critical findings: 0
- Warnings: 5
- Info findings: 9
- Warnings: 1
- Info findings: 13
## Critical
- none
## Warnings
- [SEC001] infra\ddns-updater\docker-compose.yml :: ddns-updater: Missing security_opt no-new-privileges:true.
- [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.
- [USER001] monitoring\docker-compose.yml :: influxdb3-core: Runs as user 0. Documented exception, keep visible for hardening.
## Info
- [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/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.
- [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.
- [PORT001] traefik\docker-compose.yml :: traefik: Allowed host port mapping: 80:80
- [PORT001] traefik\docker-compose.yml :: traefik: Allowed host port mapping: 443:443
+19 -3
View File
@@ -1,7 +1,7 @@
param(
[string]$DumpRoot = "/mnt/user/backups/borg/dumps/latest",
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
[int]$MaxDumpAgeHours = 36,
[int]$MaxDumpAgeHours = 26,
[int]$MaxReportAgeDays = 45
)
@@ -9,7 +9,13 @@ $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 = "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 = @(
@@ -30,15 +36,25 @@ foreach ($check in $checks) {
}
$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) {
$warnings.Add(("DUMP_STALE {0} age={1:N1}h" -f $check.Name, $ageHours))
$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
+22 -3
View File
@@ -3,7 +3,7 @@ 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:-36}"
MAX_DUMP_AGE_HOURS="${MAX_DUMP_AGE_HOURS:-26}"
MAX_REPORT_AGE_DAYS="${MAX_REPORT_AGE_DAYS:-45}"
now_epoch="$(date +%s)"
@@ -25,21 +25,40 @@ check_file_age_days() {
echo $(( (now_epoch - mtime) / 86400 ))
}
for dump in postgresql17-paperless.dump postgresql17-mailarchiver.dump mealie.dump immich.dump; do
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
warnings+=("DUMP_STALE $dump age=${age}h")
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")
+15
View File
@@ -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"
@@ -3,10 +3,11 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
MODE="${1:-}"
TOPIC="${2:-homelab-restore}"
SUCCESS_TOPIC="${2:-${RESTORE_SUCCESS_TOPIC:-homelab-info}}"
FAILURE_TOPIC="${RESTORE_FAILURE_TOPIC:-homelab-alerts}"
if [ -z "$MODE" ]; then
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless> [topic]" >&2
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless> [success_topic]" >&2
exit 1
fi
@@ -20,10 +21,10 @@ echo "Report target: $REPORT_FILE"
if "$SCRIPT_DIR/run-restore-checks.sh" "$MODE" > "$REPORT_FILE"; then
echo "Restore job succeeded, sending ntfy..."
"$SCRIPT_DIR/send-ntfy.sh" "$TOPIC" "Restore job ok: $MODE" "Restore job succeeded. Report: $REPORT_FILE" default || true
"$SCRIPT_DIR/send-ntfy.sh" "$SUCCESS_TOPIC" "Restore job ok: $MODE" "Restore job succeeded. Report: $REPORT_FILE" default || true
echo "Done"
else
echo "Restore job failed, sending ntfy..."
"$SCRIPT_DIR/send-ntfy.sh" "$TOPIC" "Restore job failed: $MODE" "Restore job failed. Report: $REPORT_FILE" high || true
"$SCRIPT_DIR/send-ntfy.sh" "$FAILURE_TOPIC" "Restore job failed: $MODE" "Restore job failed. Report: $REPORT_FILE" high || true
exit 1
fi
+13 -1
View File
@@ -45,11 +45,23 @@ Spaeter:
- `vaultwarden`
- Jeden 3. Samstag im Monat, 07:00:
- `gitea`
- Jeden 2. Monat am 2. Samstag, 08:00:
- Jeden 2. Samstag in ungeraden Monaten, 08:00:
- `paperless`
- Jeden 1. des Monats, 09:00:
- `monthly-random-restore.sh`
- Quartalsweise am 1. Werktag des Quartals:
- DR-/Restore-Sanity-Check
## Unraid User Scripts Cron
| Script | Cron | Bedeutung |
|---|---|---|
| `restore-freshness-weekly` | `30 6 * * 1` | jeden Montag 06:30 |
| `restore-vaultwarden-monthly` | `0 7 1-7 * 6` | erster Samstag im Monat 07:00 |
| `restore-gitea-monthly` | `15 7 15-21 * 6` | dritter Samstag im Monat 07:15 |
| `restore-paperless-bimonthly` | `0 8 8-14 1,3,5,7,9,11 *` | zweiter Samstag in ungeraden Monaten 08:00 |
| `monthly-random-restore` | `0 9 1 * *` | erster Kalendertag im Monat 09:00 |
## Betriebsmodus
- V1:
+8 -6
View File
@@ -7,7 +7,7 @@ Diese Vorlagen binden die validierten Restore-Checks in Unraid User Scripts ein.
Host-Repo-Pfad:
```text
/mnt/user/services/homelab
/mnt/user/services/homelab-infra
```
## Script 1 - `restore-freshness-weekly`
@@ -20,7 +20,7 @@ Inhalt:
```bash
#!/bin/bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh freshness \
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness \
> /mnt/user/backups/restore-reports/freshness-$(date +%F).md
```
@@ -40,7 +40,7 @@ V1-Inhalt:
```bash
#!/bin/bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh vaultwarden \
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh vaultwarden \
> /mnt/user/backups/restore-reports/vaultwarden-$(date +%F).md
```
@@ -54,7 +54,7 @@ V1-Inhalt:
```bash
#!/bin/bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh gitea \
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh gitea \
> /mnt/user/backups/restore-reports/gitea-$(date +%F).md
```
@@ -68,7 +68,7 @@ V1-Inhalt:
```bash
#!/bin/bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh paperless \
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh paperless \
> /mnt/user/backups/restore-reports/paperless-$(date +%F).md
```
@@ -97,9 +97,11 @@ Beispiel:
```bash
#!/bin/bash
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-restore
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-info
```
Fehler gehen unabhaengig vom Erfolgstopic nach `homelab-alerts`, damit Restore-Probleme auf dem gleichen Handy-Topic landen wie Prometheus-, Docker-, Borg- und Posture-Alarme.
Verwendete Hilfsskripte:
- `ops/restore-tests/send-ntfy.sh`
+2
View File
@@ -18,6 +18,8 @@ services:
- /dev/sdb:/dev/sdb
- /dev/sdc:/dev/sdc
- /dev/nvme0n1:/dev/nvme0n1
security_opt:
- no-new-privileges:true
networks:
- frontend_net
labels:
+1 -1
View File
@@ -1,6 +1,6 @@
services:
speedtest-tracker:
image: lscr.io/linuxserver/speedtest-tracker:latest@sha256:eb3d249f16177964daa4fff7f6a90bbf6645f4e23158d92f5cddb133728d0804
image: lscr.io/linuxserver/speedtest-tracker:1.13.12@sha256:eb3d249f16177964daa4fff7f6a90bbf6645f4e23158d92f5cddb133728d0804
container_name: speedtest-tracker
restart: unless-stopped
security_opt:
-27
View File
@@ -1,27 +0,0 @@
services:
uptime-kuma:
image: louislam/uptime-kuma:1@sha256:3d632903e6af34139a37f18055c4f1bfd9b7205ae1138f1e5e8940ddc1d176f9
container_name: UptimeKuma
restart: unless-stopped
dns:
- 8.8.8.8
- 1.1.1.1
security_opt:
- no-new-privileges:true
volumes:
- /mnt/user/appdata/uptime-kuma:/app/data
networks:
- frontend_net
labels:
- "traefik.enable=true"
- "traefik.docker.network=frontend_net"
- "traefik.http.routers.uptime-kuma.rule=Host(`uptime.kaleschke.info`)"
- "traefik.http.routers.uptime-kuma.entrypoints=websecure"
- "traefik.http.routers.uptime-kuma.tls=true"
- "traefik.http.routers.uptime-kuma.tls.certresolver=le"
- "traefik.http.routers.uptime-kuma.middlewares=authelia@file,secure-headers@file"
- "traefik.http.services.uptime-kuma.loadbalancer.server.port=3001"
networks:
frontend_net:
external: true
+9
View File
@@ -0,0 +1,9 @@
# Windows Reinstall Helpers
Diese Skripte sind bewusst versionierte Operator-Hilfen fuer den Windows-Neuaufsetzen-/Dual-Boot-Kontext vom Mai 2026.
- `backup-delta-after-2026-05-07.ps1` kopiert nach einem definierten Cutoff lokale Nutzdaten in ein Backup-Ziel.
- `repair-disk0-boot-to-new-windows.ps1` repariert EFI/Bootdateien fuer das neue Windows auf der Intel-SSD und verlangt Adminrechte.
- `cleanup-dualboot-bcd.ps1` bereinigt BCD-Bootmenueeintraege und verlangt eine explizite Textbestaetigung.
Die Skripte enthalten keine Secrets, arbeiten aber mit lokalen Windows-Datentraegern und duerfen nur interaktiv und mit vorheriger Sichtpruefung ausgefuehrt werden.
@@ -0,0 +1,118 @@
param(
[datetime]$Cutoff = "2026-05-07T15:30:00",
[string]$BackupRoot = "H:\Windows-Neuaufsetzen-Backup",
[string]$UserProfilePath = "C:\Users\michi"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Invoke-RobocopyDelta {
param(
[string]$Source,
[string]$Destination,
[string]$LogName
)
if (-not (Test-Path $Source)) {
Write-Host "SKIP missing source: $Source"
return
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
$LogPath = Join-Path $DeltaLogDir $LogName
Write-Host "DELTA COPY $Source"
Write-Host " -> $Destination"
robocopy $Source $Destination /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /MAXAGE:$CutoffString /TEE /LOG:$LogPath
$ExitCode = $LASTEXITCODE
if ($ExitCode -gt 7) {
throw "Robocopy failed with exit code $ExitCode for source: $Source"
}
}
if (-not (Test-Path $BackupRoot)) {
throw "BackupRoot does not exist: $BackupRoot"
}
if (-not (Test-Path $UserProfilePath)) {
throw "UserProfilePath does not exist: $UserProfilePath"
}
$DeltaRoot = Join-Path $BackupRoot "_Delta_2026-05-19"
$DeltaLogDir = Join-Path $DeltaRoot "00_Logs"
New-Item -ItemType Directory -Force -Path $DeltaLogDir | Out-Null
$CutoffString = $Cutoff.ToString("yyyyMMdd")
$Manifest = [ordered]@{
CreatedAt = (Get-Date).ToString("s")
Cutoff = $Cutoff.ToString("s")
CutoffNote = "Robocopy /MAXAGE uses date granularity, so files changed on or after the cutoff day are included."
RunningSystemDrive = $env:SystemDrive
RunningWinDir = $env:windir
BackupRoot = $BackupRoot
DeltaRoot = $DeltaRoot
}
$Manifest.GetEnumerator() |
ForEach-Object { "$($_.Key)=$($_.Value)" } |
Out-File (Join-Path $DeltaRoot "delta_manifest.txt") -Encoding UTF8
$Jobs = @(
@{ Source = Join-Path $UserProfilePath "Desktop"; Destination = Join-Path $DeltaRoot "01_Desktop"; Log = "delta_current_desktop.log" },
@{ Source = Join-Path $UserProfilePath "Documents"; Destination = Join-Path $DeltaRoot "02_Dokumente"; Log = "delta_current_documents.log" },
@{ Source = Join-Path $UserProfilePath "Pictures"; Destination = Join-Path $DeltaRoot "03_Bilder"; Log = "delta_current_pictures.log" },
@{ Source = Join-Path $UserProfilePath "Videos"; Destination = Join-Path $DeltaRoot "04_Videos"; Log = "delta_current_videos.log" },
@{ Source = Join-Path $UserProfilePath "Downloads"; Destination = Join-Path $DeltaRoot "05_Downloads"; Log = "delta_current_downloads.log" },
@{ Source = Join-Path $UserProfilePath ".ssh"; Destination = Join-Path $DeltaRoot "09_Programme_Settings_Lizenzen\ssh"; Log = "delta_current_ssh.log" },
@{ Source = Join-Path $UserProfilePath "AppData\Local\Subsembly"; Destination = Join-Path $DeltaRoot "09_Programme_Settings_Lizenzen\Subsembly_Local"; Log = "delta_subsembly_local.log" },
@{ Source = Join-Path $UserProfilePath "AppData\Local\Buhl"; Destination = Join-Path $DeltaRoot "09_Programme_Settings_Lizenzen\Buhl_Local"; Log = "delta_buhl_local.log" },
@{ Source = Join-Path $UserProfilePath "AppData\Local\Buhl Data Service GmbH"; Destination = Join-Path $DeltaRoot "09_Programme_Settings_Lizenzen\Buhl_Data_Service_Local"; Log = "delta_buhl_data_service_local.log" },
@{ Source = "C:\ProgramData\Buhl Data Service GmbH"; Destination = Join-Path $DeltaRoot "09_Programme_Settings_Lizenzen\Buhl_Data_Service_ProgramData"; Log = "delta_buhl_data_service_programdata.log" },
@{ Source = "G:\Gitea_Clone"; Destination = Join-Path $DeltaRoot "06_Projekte\Gitea_Clone"; Log = "delta_g_gitea_clone.log" },
@{ Source = "G:\open-webui"; Destination = Join-Path $DeltaRoot "09_Programme_Settings_Lizenzen\open-webui"; Log = "delta_g_open_webui.log" },
@{ Source = "G:\Treiber"; Destination = Join-Path $DeltaRoot "13_Treiber_Windows\G_Treiber"; Log = "delta_g_treiber.log" },
@{ Source = "F:\BMW Leasing"; Destination = Join-Path $DeltaRoot "07_Banking_Finanzen\BMW Leasing"; Log = "delta_f_bmw_leasing.log" },
@{ Source = "F:\Marina Handy 2025"; Destination = Join-Path $DeltaRoot "03_Bilder\Marina Handy 2025"; Log = "delta_f_marina_handy_2025.log" }
)
foreach ($Job in $Jobs) {
Invoke-RobocopyDelta -Source $Job.Source -Destination $Job.Destination -LogName $Job.Log
}
$ExplicitBankingDir = Join-Path $DeltaRoot "07_Banking_Finanzen\Banking4_Datentresor_explizit"
New-Item -ItemType Directory -Force -Path $ExplicitBankingDir | Out-Null
$BankingFiles = @(
(Join-Path $UserProfilePath "Documents\Mein Datentresor.sub"),
(Join-Path $UserProfilePath "Documents\.Mein Datentresor.sub.att")
)
foreach ($File in $BankingFiles) {
if (Test-Path $File) {
Copy-Item -LiteralPath $File -Destination (Join-Path $ExplicitBankingDir (Split-Path $File -Leaf)) -Force
}
}
$SummaryRoots = Get-ChildItem $DeltaRoot -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "00_Logs" }
$Summary = foreach ($Root in $SummaryRoots) {
$Files = @(Get-ChildItem $Root.FullName -Recurse -Force -File -ErrorAction SilentlyContinue)
$Measure = $Files | Measure-Object Length -Sum
[pscustomobject]@{
Path = $Root.FullName
Files = $Files.Count
SizeGB = [math]::Round(($Measure.Sum / 1GB), 4)
Newest = ($Files | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty LastWriteTime)
}
}
$Summary | Export-Csv (Join-Path $DeltaLogDir "delta_summary.csv") -NoTypeInformation -Encoding UTF8
Write-Host ""
Write-Host "Delta backup finished:"
Write-Host " $DeltaRoot"
Write-Host ""
Write-Host "Summary:"
$Summary | Format-Table -AutoSize
+105
View File
@@ -0,0 +1,105 @@
param(
[string]$BackupRoot = "H:\Windows-Neuaufsetzen-Backup",
[string]$UserProfilePath = $env:USERPROFILE,
[switch]$IncludeOllama
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Invoke-RobocopyBackup {
param(
[string]$Source,
[string]$Destination,
[string]$LogName
)
if (-not (Test-Path $Source)) {
Write-Host "SKIP missing source: $Source"
return
}
New-Item -ItemType Directory -Force -Path $Destination | Out-Null
$LogDir = Join-Path $BackupRoot "12_Exportierte_Listen"
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
$LogPath = Join-Path $LogDir $LogName
Write-Host "COPY $Source"
Write-Host " -> $Destination"
robocopy $Source $Destination /E /COPY:DAT /DCOPY:DAT /R:2 /W:2 /XJ /TEE /LOG:$LogPath
$ExitCode = $LASTEXITCODE
if ($ExitCode -gt 7) {
throw "Robocopy failed with exit code $ExitCode for source: $Source"
}
}
if (-not (Test-Path $BackupRoot)) {
throw "BackupRoot does not exist: $BackupRoot"
}
if (-not (Test-Path $UserProfilePath)) {
throw "UserProfilePath does not exist: $UserProfilePath"
}
$CurrentUserJobs = @(
@{ Source = Join-Path $UserProfilePath "Desktop"; Destination = Join-Path $BackupRoot "01_Desktop"; Log = "backup_current_desktop.log" },
@{ Source = Join-Path $UserProfilePath "Documents"; Destination = Join-Path $BackupRoot "02_Dokumente"; Log = "backup_current_documents.log" },
@{ Source = Join-Path $UserProfilePath "Pictures"; Destination = Join-Path $BackupRoot "03_Bilder"; Log = "backup_current_pictures.log" },
@{ Source = Join-Path $UserProfilePath "Videos"; Destination = Join-Path $BackupRoot "04_Videos"; Log = "backup_current_videos.log" },
@{ Source = Join-Path $UserProfilePath "Downloads"; Destination = Join-Path $BackupRoot "05_Downloads_wichtig\_current_downloads_all"; Log = "backup_current_downloads.log" },
@{ Source = Join-Path $UserProfilePath "Music"; Destination = Join-Path $BackupRoot "15_Musik"; Log = "backup_current_music.log" },
@{ Source = Join-Path $UserProfilePath ".ssh"; Destination = Join-Path $BackupRoot "09_Programme_Settings_Lizenzen\ssh"; Log = "backup_current_ssh.log" }
)
foreach ($Job in $CurrentUserJobs) {
Invoke-RobocopyBackup -Source $Job.Source -Destination $Job.Destination -LogName $Job.Log
}
$GitConfig = Join-Path $UserProfilePath ".gitconfig"
if (Test-Path $GitConfig) {
$GitDest = Join-Path $BackupRoot "09_Programme_Settings_Lizenzen\git"
New-Item -ItemType Directory -Force -Path $GitDest | Out-Null
Copy-Item -LiteralPath $GitConfig -Destination (Join-Path $GitDest ".gitconfig") -Force
}
$OldUserRoot = "D:\Users\Baerchen"
$OldUserJobs = @(
@{ Source = Join-Path $OldUserRoot "Desktop"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Desktop"; Log = "backup_old_baerchen_desktop.log" },
@{ Source = Join-Path $OldUserRoot "Documents"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Documents"; Log = "backup_old_baerchen_documents.log" },
@{ Source = Join-Path $OldUserRoot "Pictures"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Pictures"; Log = "backup_old_baerchen_pictures.log" },
@{ Source = Join-Path $OldUserRoot "Videos"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Videos"; Log = "backup_old_baerchen_videos.log" },
@{ Source = Join-Path $OldUserRoot "Downloads"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Downloads"; Log = "backup_old_baerchen_downloads.log" },
@{ Source = Join-Path $OldUserRoot "Music"; Destination = Join-Path $BackupRoot "99_Unsortiert_von_D_F_G\D_Users_Baerchen\Music"; Log = "backup_old_baerchen_music.log" }
)
foreach ($Job in $OldUserJobs) {
Invoke-RobocopyBackup -Source $Job.Source -Destination $Job.Destination -LogName $Job.Log
}
$ExtraJobs = @(
@{ Source = "F:\BMW Leasing"; Destination = Join-Path $BackupRoot "07_Banking_Finanzen\BMW Leasing"; Log = "backup_f_bmw_leasing.log" },
@{ Source = "F:\Marina Handy 2025"; Destination = Join-Path $BackupRoot "03_Bilder\Marina Handy 2025"; Log = "backup_f_marina_handy_2025.log" },
@{ Source = "F:\Marina Handy Backup"; Destination = Join-Path $BackupRoot "03_Bilder\Marina Handy Backup"; Log = "backup_f_marina_handy_backup.log" },
@{ Source = "G:\Gitea_Clone"; Destination = Join-Path $BackupRoot "06_Projekte\Gitea_Clone"; Log = "backup_g_gitea_clone.log" },
@{ Source = "G:\open-webui"; Destination = Join-Path $BackupRoot "09_Programme_Settings_Lizenzen\open-webui"; Log = "backup_g_open_webui.log" },
@{ Source = "G:\Treiber"; Destination = Join-Path $BackupRoot "13_Treiber_Windows\G_Treiber"; Log = "backup_g_treiber.log" }
)
if ($IncludeOllama) {
$ExtraJobs += @{ Source = "G:\Ollama"; Destination = Join-Path $BackupRoot "09_Programme_Settings_Lizenzen\Ollama"; Log = "backup_g_ollama.log" }
}
foreach ($Job in $ExtraJobs) {
Invoke-RobocopyBackup -Source $Job.Source -Destination $Job.Destination -LogName $Job.Log
}
Write-Host ""
Write-Host "Known-data backup finished."
Write-Host "Backup root: $BackupRoot"
Write-Host "Logs: $BackupRoot\12_Exportierte_Listen"
if (-not $IncludeOllama) {
Write-Host "Note: G:\Ollama was not copied. Re-run with -IncludeOllama if you want to keep local models too."
}
@@ -0,0 +1,141 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$BackupDir = "H:\Windows-Neuaufsetzen-Backup\12_Exportierte_Listen"
$BeforeFile = Join-Path $BackupDir "bcdedit_enum_before_dualboot_cleanup.txt"
$AfterFile = Join-Path $BackupDir "bcdedit_enum_after_dualboot_cleanup.txt"
function Assert-Admin {
$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$Principal = [Security.Principal.WindowsPrincipal]::new($Identity)
if (-not $Principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw "Dieses Skript muss in PowerShell als Administrator ausgefuehrt werden."
}
}
function Get-BcdOsLoaderEntries {
$Text = & bcdedit /enum osloader /v
if ($LASTEXITCODE -ne 0) {
throw "bcdedit /enum osloader /v ist fehlgeschlagen."
}
$Entries = @()
$Current = $null
foreach ($Line in $Text) {
if ($Line -match "^-{5,}$") {
if ($Current -and $Current.Identifier) {
$Entries += [pscustomobject]$Current
}
$Current = @{
Identifier = $null
Device = $null
OsDevice = $null
Path = $null
Description = $null
}
continue
}
if (-not $Current) {
continue
}
if ($Line -match "^\s*(identifier|Bezeichner)\s+(.+)$") { $Current.Identifier = $Matches[2].Trim(); continue }
if ($Line -match "^\s*device\s+(.+)$") { $Current.Device = $Matches[1].Trim(); continue }
if ($Line -match "^\s*osdevice\s+(.+)$") { $Current.OsDevice = $Matches[1].Trim(); continue }
if ($Line -match "^\s*path\s+(.+)$") { $Current.Path = $Matches[1].Trim(); continue }
if ($Line -match "^\s*description\s+(.+)$") { $Current.Description = $Matches[1].Trim(); continue }
}
if ($Current -and $Current.Identifier) {
$Entries += [pscustomobject]$Current
}
return $Entries
}
Assert-Admin
New-Item -ItemType Directory -Force -Path $BackupDir | Out-Null
& bcdedit /enum all /v | Out-File $BeforeFile -Encoding UTF8
$Entries = Get-BcdOsLoaderEntries
Write-Host "Gefundene Windows-Boot-Eintraege:"
$Entries | Select-Object Identifier, Description, Device, OsDevice, Path | Format-Table -AutoSize
$NewWindows = $Entries | Where-Object {
$_.OsDevice -eq "partition=D:" -and
$_.Path -match "winload\.efi$" -and
(Test-Path "D:\Windows\System32\winload.efi")
} | Select-Object -First 1
$OldWindows = $Entries | Where-Object {
$_.OsDevice -eq "partition=C:" -and
$_.Path -match "winload\.efi$" -and
(Test-Path "C:\Windows\System32\winload.efi")
} | Select-Object -First 1
if (-not $NewWindows) {
throw "Abbruch: Neuer Windows-Eintrag auf D:\Windows wurde nicht eindeutig gefunden."
}
if (-not $OldWindows) {
throw "Abbruch: Alter Windows-Eintrag auf C:\Windows wurde nicht eindeutig gefunden."
}
$KeepIds = @($NewWindows.Identifier, $OldWindows.Identifier)
$DeleteEntries = $Entries | Where-Object { $KeepIds -notcontains $_.Identifier }
Write-Host ""
Write-Host "Bleibt erhalten:"
Write-Host " NEU: $($NewWindows.Identifier) -> $($NewWindows.OsDevice) -> D:\Windows"
Write-Host " ALT: $($OldWindows.Identifier) -> $($OldWindows.OsDevice) -> C:\Windows"
Write-Host ""
Write-Host "Wird aus dem Bootmenue entfernt:"
if ($DeleteEntries) {
$DeleteEntries | Select-Object Identifier, Description, Device, OsDevice, Path | Format-Table -AutoSize
} else {
Write-Host " Keine ueberfluessigen Eintraege gefunden."
}
Write-Host ""
Write-Host "Es werden nur Bootmenue-Eintraege geloescht, keine Partitionen und keine Windows-Ordner."
$Confirmation = Read-Host "Tippe exakt BOOTCLEAN um fortzufahren"
if ($Confirmation -ne "BOOTCLEAN") {
throw "Abbruch: Bestaetigung wurde nicht eingegeben."
}
& bcdedit /set $NewWindows.Identifier description "Windows 11 Neu"
if ($LASTEXITCODE -ne 0) { throw "Konnte neuen Windows-Eintrag nicht umbenennen." }
& bcdedit /set $OldWindows.Identifier description "Windows 11 Alt"
if ($LASTEXITCODE -ne 0) { throw "Konnte alten Windows-Eintrag nicht umbenennen." }
& bcdedit /default $NewWindows.Identifier
if ($LASTEXITCODE -ne 0) { throw "Konnte Standard-Boot-Eintrag nicht setzen." }
& bcdedit /timeout 5
if ($LASTEXITCODE -ne 0) { throw "Konnte Timeout nicht setzen." }
foreach ($Entry in $DeleteEntries) {
Write-Host "Loesche Boot-Eintrag $($Entry.Identifier) ($($Entry.Description))"
& bcdedit /delete $Entry.Identifier /f
if ($LASTEXITCODE -ne 0) {
throw "Konnte Boot-Eintrag $($Entry.Identifier) nicht loeschen."
}
}
& bcdedit /enum all /v | Out-File $AfterFile -Encoding UTF8
Write-Host ""
Write-Host "Bootmenue bereinigt."
Write-Host "Vorher gesichert: $BeforeFile"
Write-Host "Nachher gesichert: $AfterFile"
Write-Host ""
Write-Host "Bitte neu starten. Es sollten nur noch zwei Eintraege erscheinen:"
Write-Host " Windows 11 Neu"
Write-Host " Windows 11 Alt"
@@ -0,0 +1,69 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ExpectedDiskNumber = 0
$ExpectedFriendlyName = "INTEL SSDSC2BW180A3L"
$ExpectedSerialNumber = "CVCV3105053K180EGN"
$MinSizeGB = 160
$MaxSizeGB = 180
function Assert-Admin {
$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$Principal = [Security.Principal.WindowsPrincipal]::new($Identity)
if (-not $Principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw "Dieses Skript muss in PowerShell als Administrator ausgefuehrt werden."
}
}
Assert-Admin
$Disk = Get-Disk -Number $ExpectedDiskNumber
$SizeGB = [math]::Round($Disk.Size / 1GB, 2)
Write-Host "Zieldatentraeger:"
$Disk | Select-Object Number, FriendlyName, SerialNumber, HealthStatus, OperationalStatus, @{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,2)}}, PartitionStyle | Format-Table -AutoSize
Write-Host "Aktuelle Partitionen auf Datentraeger ${ExpectedDiskNumber}:"
Get-Partition -DiskNumber $ExpectedDiskNumber | Sort-Object PartitionNumber | Select-Object DiskNumber, PartitionNumber, DriveLetter, Type, @{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,3)}} | Format-Table -AutoSize
if ($Disk.FriendlyName -ne $ExpectedFriendlyName) {
throw "Abbruch: FriendlyName passt nicht. Erwartet '$ExpectedFriendlyName', gefunden '$($Disk.FriendlyName)'."
}
if ($Disk.SerialNumber -ne $ExpectedSerialNumber) {
throw "Abbruch: SerialNumber passt nicht. Erwartet '$ExpectedSerialNumber', gefunden '$($Disk.SerialNumber)'."
}
if ($SizeGB -lt $MinSizeGB -or $SizeGB -gt $MaxSizeGB) {
throw "Abbruch: Groesse passt nicht. Erwartet ca. 167 GB, gefunden $SizeGB GB."
}
$SystemPartitions = Get-Partition -DiskNumber $ExpectedDiskNumber | Where-Object {
$_.IsBoot -or $_.IsSystem -or $_.IsActive
}
if ($SystemPartitions) {
throw "Abbruch: Datentraeger $ExpectedDiskNumber enthaelt Boot/System/Aktiv-Partitionen."
}
Write-Host ""
Write-Host "WARNUNG: Datentraeger 0 wird jetzt komplett geleert."
Write-Host "Das betrifft die alte Windows-SSD mit aktuell D: und Recovery-Partitionen."
Write-Host "Andere Datentraeger werden nicht angefasst."
Write-Host ""
$Confirmation = Read-Host "Tippe exakt LEEREN um fortzufahren"
if ($Confirmation -ne "LEEREN") {
throw "Abbruch: Bestaetigung wurde nicht eingegeben."
}
Clear-Disk -Number $ExpectedDiskNumber -RemoveData -RemoveOEM -Confirm:$false
Write-Host ""
Write-Host "Datentraeger $ExpectedDiskNumber wurde geleert."
Write-Host "Ergebnis:"
Get-Disk -Number $ExpectedDiskNumber | Select-Object Number, FriendlyName, HealthStatus, OperationalStatus, @{Name="SizeGB";Expression={[math]::Round($_.Size/1GB,2)}}, PartitionStyle | Format-Table -AutoSize
Get-Partition -DiskNumber $ExpectedDiskNumber -ErrorAction SilentlyContinue | Format-Table -AutoSize
Write-Host ""
Write-Host "Naechster Schritt: Vom Windows-USB-Stick booten und Windows auf den nicht zugeordneten Speicher von Datentraeger 0 installieren."
@@ -0,0 +1,34 @@
param(
[string]$BackupRoot = "H:\Windows-Neuaufsetzen-Backup",
[switch]$ImportWingetList
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
throw "winget is not available. Install or update App Installer from Microsoft Store first."
}
Write-Host "Installing UniGetUI..."
winget install --exact --id Devolutions.UniGetUI --source winget --accept-package-agreements --accept-source-agreements
$WingetExport = Join-Path $BackupRoot "12_Exportierte_Listen\winget-export.json"
if ($ImportWingetList) {
if (-not (Test-Path $WingetExport)) {
throw "WinGet export file not found: $WingetExport"
}
Write-Host "Importing WinGet package list..."
Write-Host "Source: $WingetExport"
winget import --import-file $WingetExport --accept-package-agreements --accept-source-agreements
} else {
Write-Host ""
Write-Host "UniGetUI installed."
Write-Host "WinGet export is available here:"
Write-Host " $WingetExport"
Write-Host ""
Write-Host "Review the list before importing. To import later, run:"
Write-Host " .\postinstall-unigetui.ps1 -ImportWingetList"
}
@@ -0,0 +1,139 @@
param(
[string]$BackupRoot = "H:\Windows-Neuaufsetzen-Backup",
[string]$UserProfilePath = $env:USERPROFILE
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function New-BackupFolder {
param([string]$Name)
$Path = Join-Path $BackupRoot $Name
New-Item -ItemType Directory -Force -Path $Path | Out-Null
return $Path
}
function Export-CsvSafe {
param(
[Parameter(ValueFromPipeline = $true)]$InputObject,
[string]$Path
)
begin {
$Items = @()
}
process {
$Items += $InputObject
}
end {
$Items | Export-Csv -Path $Path -NoTypeInformation -Encoding UTF8
}
}
if (-not (Test-Path $UserProfilePath)) {
throw "UserProfilePath does not exist: $UserProfilePath"
}
New-Item -ItemType Directory -Force -Path $BackupRoot | Out-Null
$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"
)
$Folders | ForEach-Object { New-BackupFolder $_ | Out-Null }
$ExportDir = Join-Path $BackupRoot "12_Exportierte_Listen"
Get-ItemProperty `
HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*, `
HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* |
Where-Object {
$_.PSObject.Properties.Name -contains "DisplayName" -and
-not [string]::IsNullOrWhiteSpace($_.DisplayName)
} |
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, InstallLocation |
Sort-Object DisplayName |
Export-CsvSafe -Path (Join-Path $ExportDir "installierte_programme.csv")
if (Get-Command winget -ErrorAction SilentlyContinue) {
winget list | Out-File -FilePath (Join-Path $ExportDir "winget-list.txt") -Encoding UTF8
}
Get-Volume |
Sort-Object DriveLetter |
Select-Object DriveLetter, FileSystemLabel, FileSystem, Size, SizeRemaining, HealthStatus |
Export-CsvSafe -Path (Join-Path $ExportDir "laufwerke.csv")
Get-Disk |
Select-Object Number, FriendlyName, SerialNumber, HealthStatus, OperationalStatus, Size, PartitionStyle |
Export-CsvSafe -Path (Join-Path $ExportDir "datentraeger.csv")
Get-Partition |
Select-Object DiskNumber, PartitionNumber, DriveLetter, Type, Size, GptType |
Sort-Object DiskNumber, PartitionNumber |
Export-CsvSafe -Path (Join-Path $ExportDir "partitionen.csv")
try {
wmic path softwarelicensingservice get OA3xOriginalProductKey |
Out-File -FilePath (Join-Path $ExportDir "windows_oem_key.txt") -Encoding UTF8
} catch {
"Could not read OEM key: $($_.Exception.Message)" |
Out-File -FilePath (Join-Path $ExportDir "windows_oem_key.txt") -Encoding UTF8
}
if (Get-Command manage-bde -ErrorAction SilentlyContinue) {
manage-bde -status | Out-File -FilePath (Join-Path $ExportDir "bitlocker_status.txt") -Encoding UTF8
}
if (Get-Command wsl -ErrorAction SilentlyContinue) {
wsl --list --verbose | Out-File -FilePath (Join-Path $ExportDir "wsl-distros.txt") -Encoding UTF8
}
$KnownUserPaths = @(
"Desktop",
"Documents",
"Pictures",
"Videos",
"Downloads",
"Music",
".ssh",
".gitconfig"
)
$KnownUserPaths |
ForEach-Object {
$Path = Join-Path $UserProfilePath $_
[pscustomobject]@{
Name = $_
Path = $Path
Exists = Test-Path $Path
LastWriteTime = if (Test-Path $Path) { (Get-Item $Path -Force).LastWriteTime } else { $null }
}
} |
Export-CsvSafe -Path (Join-Path $ExportDir "benutzerpfade.csv")
Write-Host ""
Write-Host "Backup preparation inventory created:"
Write-Host " $BackupRoot"
Write-Host ""
Write-Host "Next manual steps:"
Write-Host " 1. Save screenshots into: $BackupRoot\14_Screenshots"
Write-Host " 2. Review CSV files in: $ExportDir"
Write-Host " 3. Start data backup only after confirming H: is the correct external drive."

Some files were not shown because too many files have changed in this diff Show More