HOMELAB_ARCHITECTURE_MASTER_V2.md aktualisiert

This commit is contained in:
2026-03-26 16:07:11 +00:00
parent 66eb077512
commit 2ba78acc78
+265 -216
View File
@@ -3,6 +3,8 @@
> **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-03-26 | **Aktueller Sprint:** 4 (Frontend-Stack) — Sprints 13 abgeschlossen
---
## Inhaltsverzeichnis
@@ -18,6 +20,7 @@
10. [Bekannte Ausnahmen und Begründungen](#10-bekannte-ausnahmen-und-begründungen)
11. [Projektorganisation und Arbeitsmodus](#11-projektorganisation-und-arbeitsmodus)
12. [Nutzung mit KI / Kontext-Regel](#12-nutzung-mit-ki--kontext-regel)
13. [Betriebserfahrungen und Entscheidungs-Log](#13-betriebserfahrungen-und-entscheidungs-log)
---
@@ -33,7 +36,7 @@
| Basis-Domain | `kaleschke.info` |
| TLS | Let's Encrypt via Cloudflare DNS Challenge |
| Certresolver | `le` |
| Compose-Standard | Unraid Compose Manager |
| Compose-Standard | Portainer Stacks (Web-Editor oder Git) |
| Zusatz-Tool | Portainer als Verwaltungs-UI |
| Homelab-Compose-Pfad | `/mnt/user/services/homelab/` |
| Secrets-Pfad | `/mnt/user/appdata/secrets/` |
@@ -44,14 +47,14 @@
## 2. Architektur-Prinzipien
### P1 — Traefik ist der einzige öffentliche HTTP(S)-Einstiegspunkt
Kein Webdienst veröffentlicht final direkte Host-Ports außer `traefik` selbst.
Begründete Ausnahmen: `gitea`-SSH, `Plex-Media-Server`, `binhex-official-pihole`, `Tailscale-Docker`.
Kein Webdienst veröffentlicht finale direkte Host-Ports außer `traefik` selbst.
Begründete Ausnahmen: `gitea`-SSH (Port 222), `Plex-Media-Server`, `binhex-official-pihole`, `Tailscale-Docker`.
### P2 — Das Setup bleibt bewusst einfach: `frontend_net` + `backend_net` + app-interne Netze
Die Quellenlage für Homelabs mit Traefik spricht für ein simples 2-Netz-Modell:
- `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_internal`, `immich_default`, `scanopy_scanopy`, `dns_net`)
- zusätzliche Netze nur app-intern, wenn technisch nötig (`mealie_mealie_internal`, `immich_default`, `dns_net`)
Es gibt **keine künstlichen globalen Zusatznetze** wie `admin_net`, `monitoring_net` oder `media_net`.
@@ -66,7 +69,7 @@ Alle produktiven Container werden als Compose verwaltet.
Bestehende Dockerman-/Ad-hoc-Container werden schrittweise migriert.
### P6 — Secrets nie im Klartext
Passwörter, Tokens und API-Keys gehören in Secret-Dateien unter `/mnt/user/appdata/secrets/` oder in `_FILE`-Variablen.
Passwörter, Tokens und API-Keys gehören in Secret-Dateien unter `/mnt/user/appdata/secrets/` oder als Portainer Environment Variables mit `${VARIABLE}` in der Compose.
Ziel: kein sensibles Secret mehr sichtbar in `docker inspect`.
### P7 — `restart: unless-stopped` ist Pflichtstandard
@@ -89,10 +92,8 @@ 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 für `unbound` | bleibt |
| `mealie_internal` | bridge, `internal: true` | internes Netz nur für `mealie` + `mealie-postgres` | Ziel |
| `immich_default` | Compose-intern | internes Immich-Netz | bleibt |
| `scanopy_scanopy` | Compose-intern | internes Scanopy-Netz | bleibt |
| `diun_default` | Compose-intern | Compose-Netz für diun | bleibt, zusätzlich Join zu `frontend_net` |
| `mealie_mealie_internal` | bridge, `internal: true` | internes Netz nur für `mealie` + `mealie-postgres` | ✅ umgesetzt |
| `immich_default` | Compose-intern | internes Immich-Netz | `internal: true` noch setzen |
| `host` | host | nur für echte Sonderfälle | begründet |
### 3.2 Finales Diagramm (vereinfacht)
@@ -104,20 +105,19 @@ Internet
traefik (80/443)
└── frontend_net
├── öffentliche Apps
├── Admin-UIs mit Middleware
└── interne Web-UIs für Tailscale-only
├── öffentliche Apps (vaultwarden, mealie, paperless, immich, gitea, gotify)
├── Admin-UIs mit Middleware (portainer, dozzle, uptime-kuma, dashdot, filebrowser, scrutiny, code-server)
└── Hybrid-Dienste mit Internetbedarf (mail-archiver, ddns-updater)
backend_net (internal: true)
├── postgresql17
├── Redis
├── mail-archiver
└── paperless / andere Backends
└── paperless-ngx
App-interne Netze
├── mealie_internal
── immich_default
└── scanopy_scanopy
├── mealie_mealie_internal (internal: true) ✅
── immich_default (internal: true ausstehend) ⏳
Host-Sonderfälle
├── Tailscale-Docker
@@ -153,9 +153,8 @@ Diese Dienste dürfen final über echte `*.kaleschke.info`-Domains erreichbar se
- `gotify`
- `gitea` (Web)
- `immich_server`
- optional `mail-archiver`
- optional `backrest`
- optional `Stash` (nur wenn bewusst gewünscht)
- `mail-archiver`
- `backrest`
### 4.2 Nicht öffentlich / nur Tailscale oder Traefik + Middleware
Diese Dienste sind **keine Public Apps**:
@@ -168,7 +167,6 @@ Diese Dienste sind **keine Public Apps**:
- `scrutiny`
- `luckyBackup`
- `code-server`
- `scanopy-server`
- `Traefik-Dashboard`
- `netdata`
- `Glances`
@@ -189,7 +187,7 @@ Admin-Dienste dürfen im `frontend_net` liegen, wenn:
1. Keine produktiven Dienste im Docker-Default-`bridge`
2. Keine direkten Host-Ports für Web-UIs außer dokumentierte Ausnahmen
3. `restart: unless-stopped` als Standard
4. Secrets als Datei / `_FILE`
4. Secrets als Datei / `_FILE` oder Portainer Environment Variables mit `${VAR}`
5. `no-new-privileges:true` ergänzen, wo praktikabel
6. `traefik.docker.network=frontend_net` immer explizit setzen
7. Admin-Dienste immer mit `dashboard-auth@file,secure-headers@file`
@@ -207,29 +205,34 @@ Admin-Dienste dürfen im `frontend_net` liegen, wenn:
- **Ja** → `frontend_net`
- **Nein** → weiter zu Schritt 2
### Schritt 2 — Braucht der Dienst eine DB / Redis / interne Backends?
### Schritt 2 — Braucht der Dienst externe Internetverbindungen?
- **Ja** → `frontend_net` (auch ohne Web-UI — z.B. `ddns-updater`, `mail-archiver`)
- **Nein** → weiter zu Schritt 3
### Schritt 3 — Braucht der Dienst eine DB / Redis / interne Backends?
- **Ja** → zusätzlich `backend_net` oder eigenes app-internes Netz
- **Nein** → nur das funktional nötige Netz
### Schritt 3 — Ist es eine Datenbank oder ein Cache?
### Schritt 4 — Ist es eine Datenbank oder ein Cache?
- **Ja** → niemals `frontend_net`, nur `backend_net` oder internes Compose-Netz
### Schritt 4 — Ist es ein Admin-/Monitoring-Dienst?
### Schritt 5 — Ist es ein Admin-/Monitoring-Dienst?
- **Ja** → wenn Web-UI vorhanden trotzdem `frontend_net`, aber nur mit:
- Middleware
- keiner direkten Portfreigabe
- Tailscale-only-Charakter
### Schritt 5 — Braucht der Dienst Host-/Discovery-/L2-Sicht?
### Schritt 6 — Braucht der Dienst Host-/Discovery-/L2-Sicht?
- **Ja** → `host` nur mit dokumentierter Begründung
- **Nein** → kein `host`
### Schritt 6 — Braucht die App ein eigenes internes App-Netz?
### Schritt 7 — Braucht die App ein eigenes internes App-Netz?
- **Ja** → Compose-internes Netz mit `internal: true`
- **Nein** → kein weiteres Netz anlegen
### Kurzregel
- **UI** → `frontend_net`
- **Externer Internetzugriff nötig** → `frontend_net` (auch ohne UI)
- **DB/Cache** → `backend_net`
- **Spezialfall** → app-internes Netz
- **Host-Zugriff** → nur dokumentierte Ausnahme
@@ -239,7 +242,7 @@ Admin-Dienste dürfen im `frontend_net` liegen, wenn:
## 7. Container-Zielbild (vollständig)
Legende Status:
- `✅` = bereits weitgehend passend
- `✅` = umgesetzt
- `⏳` = noch zu migrieren / zu korrigieren
### 7.1 Infrastruktur / Core
@@ -248,66 +251,70 @@ Legende Status:
|---|---|---|---|---|---|
| `traefik` | ✅ | `frontend_net`, `backend_net` | öffentlich | zentraler Ingress, 80/443 direkt | Middleware-File-Provider sauber pflegen |
| `homepage` | ✅ | `frontend_net` | Traefik | öffentliche Startseite | — |
| `ddns-updater` | | `backend_net` | intern | kein Grund für `frontend_net` | aus `frontend_net` raus |
| `Tailscale-Docker` | | `host` | VPN-Zugang | bleibt Host-Sonderfall | `restart` fixen; `TS_USERSPACE`/`privileged` später prüfen, nicht als Quick Win |
| `binhex-official-pihole` | | `host` | LAN DNS / intern | bleibt Host-Sonderfall | `restart` fixen |
| `ddns-updater` | | `frontend_net` | intern | bleibt in `frontend_net` — braucht Internetzugang für Cloudflare DNS | Dokumentierte Ausnahme, kein `backend_net` wegen `internal: true` |
| `Tailscale-Docker` | | `host` | VPN-Zugang | Host-Sonderfall, `restart: unless-stopped` gesetzt | `TS_USERSPACE`/`privileged` später prüfen |
| `binhex-official-pihole` | | `host` | LAN DNS / intern | Host-Sonderfall, `restart: unless-stopped` gesetzt | — |
| `unbound` | ✅ | `dns_net` | intern | Resolver bleibt isoliert | — |
| `backrest` | | `frontend_net`, `backend_net` | Traefik oder intern | UI via Traefik, Repo intern | `traefik.docker.network` auf `frontend_net`; DNS-Hardcoding entfernen; Mounts straffen |
| `diun` | | `diun_default`, `frontend_net` | intern | braucht `frontend_net`, um `gotify` zu erreichen | Gotify-Endpoint per Container-Name |
| `theme-park` | ⏳ | `frontend_net` oder rein intern | intern oder Traefik | nur veröffentlichen, wenn wirklich genutzt | Placeholder-Labels bereinigen |
| `scanopy-daemon` | ⏳ | `host`, `scanopy_scanopy` falls nötig | intern | Host-naher Sonderfall | Privileged nur dokumentiert belassen oder später testen |
| `backrest` | | `frontend_net`, `backend_net` | Traefik | `traefik.docker.network=frontend_net` korrigiert | Breite Mounts straffen (Block F) |
| `diun` | | `frontend_net` | intern | in `frontend_net`, gotify via Container-Name erreichbar | — |
### 7.2 Sicherheit / Identity
| Container | Status | Soll-Netz(e) | Finaler Zugang | Finaler Sollzustand | Offene Punkte |
|---|---|---|---|---|---|
| `vaultwarden` | ✅ | `frontend_net` | Traefik | keine Host-Ports, kein `bridge`, Secret-Datei | `ADMIN_TOKEN`-Bug fixen; Port 4743 entfernen; Compose-Migration |
| `vaultwarden` | ✅ | `frontend_net` | Traefik | kein Host-Port, Secret-Datei (`ADMIN_TOKEN_FILE`) | — |
### 7.3 Datenbanken / Caches
| Container | Status | Soll-Netz(e) | Finaler Zugang | Finaler Sollzustand | Offene Punkte |
|---|---|---|---|---|---|
| `postgresql17` | ✅ | `backend_net` | intern | keine Host-Ports, kein `bridge` | Port 5432 entfernen; Secret-Datei; Compose-Migration |
| `postgresql17` | ✅ | `backend_net` | intern | kein Host-Port, `POSTGRES_PASSWORD_FILE` | — |
| `Redis` | ✅ | `backend_net` | intern | intern-only Cache | optional named volume statt anonym |
| `mealie-postgres` | ✅ | `mealie_internal` | intern | nur intern, nie `frontend_net` | aus `frontend_net` raus; Secret-Datei |
| `immich_postgres` | | `immich_default` | intern | intern-only | Passwort rotieren; named volume prüfen |
| `immich_redis` | ⏳ | `immich_default` | intern | intern-only | anonymes Volume perspektivisch bereinigen |
| `scanopy-postgres` | ⏳ | `scanopy_scanopy` | intern | intern-only | Passwort rotieren (aktuell Wiederverwendung) |
| `mealie-postgres` | ✅ | `mealie_mealie_internal` | intern | isoliert, nie `frontend_net` | |
| `immich_postgres` | | `immich_default` | intern | intern-only | `immich_default``internal: true` ausstehend |
| `immich_redis` | ⏳ | `immich_default` | intern | intern-only | anonymes Volume → named volume |
### 7.4 Öffentliche Apps
| Container | Status | Soll-Netz(e) | Finaler Zugang | Finaler Sollzustand | Offene Punkte |
|---|---|---|---|---|---|
| `paperless-ngx` | | `frontend_net`, `backend_net` | Traefik | vorbereitete Labels final aktivieren | `traefik.enable=true`; echte Domain; Port entfernen; Secret-Datei |
| `Paperless-AI` | | `frontend_net` | Traefik oder intern | Port nicht mehr direkt offen | echte Domain; Labels aktivieren |
| `mealie` | ✅ | `frontend_net`, `mealie_internal` | Traefik | sauber getrennte App/DB-Struktur | Port entfernen; Secret-Datei |
| `gotify` | | `frontend_net` | Traefik oder intern | intern per Container-Name für `diun` erreichbar | Passwort rotieren; Port entfernen |
| `gitea` | | `frontend_net` | Traefik + SSH | Web via Traefik, SSH-Port bleibt | HTTP-Labels sauberziehen |
| `immich_server` | | `immich_default`, `frontend_net` | Traefik | internes Immich-Netz bleibt; Web via Traefik | Port 2283 entfernen; Secret-Datei; `frontend_net` ergänzen |
| `paperless-ngx` | | `frontend_net`, `backend_net` | Traefik | aktiv via `paperless.kaleschke.info` | — |
| `Paperless-AI` | | `frontend_net` | Traefik | aktiv | — |
| `mealie` | ✅ | `frontend_net`, `mealie_mealie_internal` | Traefik | sauber getrennte App/DB-Struktur | |
| `gotify` | | `frontend_net` | Traefik | aktiv via `gotify.kaleschke.info`, kein Host-Port | — |
| `gitea` | | `frontend_net` | Traefik + SSH | Web via Traefik, SSH-Port 222 bleibt | |
| `immich_server` | | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | `immich_default``internal: true` ausstehend |
| `immich_machine_learning` | ✅ | `immich_default` | intern | bleibt intern | — |
| `Stash` | ⏳ | `frontend_net` | intern oder Traefik | aus `bridge` raus; nur veröffentlichen wenn bewusst gewünscht | Port entfernen; ggf. Traefik-Labels |
### 7.5 Admin / Operations
| Container | Status | Soll-Netz(e) | Finaler Zugang | Finaler Sollzustand | Offene Punkte |
|---|---|---|---|---|---|
| `code-server` | | `frontend_net` | Traefik + Middleware / Tailscale-only | nicht öffentlich offen | `PASSWORD``HASHED_PASSWORD`; Secret-Datei |
| `PortainerCE` | | `frontend_net` | Traefik + Middleware / Tailscale-only | keine Host-Ports | echte Domain; Labels aktivieren; direkte Ports entfernen; **Docker-Socket nicht blind auf `:ro`** |
| `Dozzle` | | `frontend_net` | Traefik + Middleware / Tailscale-only | keine Host-Ports | Labels aktivieren; direkte Ports entfernen |
| `filebrowser` | | `frontend_net` | Traefik + Middleware / Tailscale-only | aus `bridge`; breite FS-Mounts prüfen | Port entfernen; Mounts einschränken |
| `scanopy-server` | ⏳ | `scanopy_scanopy`, `frontend_net` | Traefik + Middleware / Tailscale-only | kein direkter Host-Port | `frontend_net` ergänzen; Port entfernen |
| `luckyBackup` | | `frontend_net` | Traefik + Middleware / Tailscale-only | aus `bridge`; nur intern | Port entfernen; breite Mounts prüfen |
| `code-server` | | `frontend_net` | Traefik + Middleware | `PASSWORD_FILE` aktiv | — |
| `PortainerCE` | | `frontend_net` | Traefik + Middleware | keine Host-Ports | |
| `Dozzle` | | `frontend_net` | Traefik + Middleware | keine Host-Ports | |
| `filebrowser` | | `frontend_net` | Traefik + Middleware | aktiv via `files.kaleschke.info` | Breite Mounts einschränken (Block F) |
| `luckyBackup` | ⏳ | `frontend_net` | Traefik + Middleware | noch in `bridge` | aus `bridge`; Port entfernen |
| `mail-archiver` | | `frontend_net`, `backend_net` | Traefik | aktiv via `mail.kaleschke.info`, kein Host-Port | — |
### 7.6 Monitoring / Status
| Container | Status | Soll-Netz(e) | Finaler Zugang | Finaler Sollzustand | Offene Punkte |
|---|---|---|---|---|---|
| `UptimeKuma` | | `frontend_net` | Traefik + Middleware / Tailscale-only | keine direkten Ports | Labels sauberziehen; Port entfernen |
| `dashdot` | | `frontend_net` | Traefik + Middleware / Tailscale-only | keine direkten Ports | Placeholder-Domain raus; Labels aktivieren |
| `Glances` | | `host` | Tailscale-only | Host-Metriken | optional später hinter Traefik, aber nicht nötig |
| `netdata` | | `host` | Tailscale-only | Host-Metriken | `restart` fixen; leere CLAIM-Vars aufräumen |
| `scrutiny` | | `frontend_net` | Traefik + Middleware / Tailscale-only | aus `bridge`; echtes Image beibehalten | Ports entfernen; später prüfen, ob `privileged` reduziert werden kann |
| `netalertx` | ✅ | `host` | Tailscale-only | Host-/Netzsicht bleibt | optional später hinter Traefik, aber kein Muss |
| `UptimeKuma` | | `frontend_net` | Traefik + Middleware | aktiv via `uptime.kaleschke.info`, kein Host-Port | — |
| `dashdot` | | `frontend_net` | Traefik + Middleware | aktiv, kein Host-Port | |
| `Glances` | | `host` | Tailscale-only | Host-Metriken | |
| `netdata` | | `host` | Tailscale-only | Host-Metriken | leere CLAIM-Vars entfernen |
| `scrutiny` | | `frontend_net` | Traefik + Middleware | aktiv via `scrutiny.kaleschke.info`, Git-Stack | `privileged` später prüfen ob reduzierbar |
| `netalertx` | ✅ | `host` | Tailscale-only | Host-/Netzsicht | — |
### 7.7 Entfernte Container
| Container | Entfernt am | Begründung |
|---|---|---|
| `scanopy-server` | 2026-03-26 | nicht aktiv genutzt, durch paperless-ngx ersetzt |
| `scanopy-postgres` | 2026-03-26 | zusammen mit scanopy entfernt |
| `scanopy-daemon` | 2026-03-26 | zusammen mit scanopy entfernt |
---
@@ -332,9 +339,10 @@ labels:
```
### Regeln
- `traefik.docker.network` immer explizit auf `frontend_net`
- `traefik.docker.network` immer explizit auf `frontend_net` — nie auf `backend_net` oder anderen Netzen!
- keine `yourdomain.tld`-Platzhalter
- certresolver immer `le`
- `tls=true` immer explizit setzen — ohne diese Zeile kein TLS
- wenn Traefik aktiv ist, werden direkte Host-Ports entfernt
- Admin-Dienste niemals ohne Middleware veröffentlichen
@@ -342,96 +350,125 @@ labels:
## 9. Migrationsstrategie (Blöcke AF)
**Letzte Aktualisierung:** 2026-03-25
**Letzte Aktualisierung:** 2026-03-26
### Block A — Quick Wins (geringes Risiko, sofort)
### Block A — Quick Wins ✅ ABGESCHLOSSEN
```text
[x] restart: unless-stopped für:
- Tailscale-Docker
- binhex-official-pihole
- postgresql17
- vaultwarden
- mail-archiver
- scrutiny
- netdata
- Stash
- Plex-Media-Server
[x] vaultwarden ADMIN_TOKEN-Doppelpräfix korrigieren
[x] backrest DNS-Hardcoding entfernen
[x] diun ↔ gotify Erreichbarkeit prüfen / herstellen
[x] leere Netzwerke prüfen und entfernen: br0, immich_net, kopia_default, netbox_default
[x] ungenutzte anonyme Volumes prüfen
[x] restart: unless-stopped für alle Container gesetzt
[x] vaultwarden ADMIN_TOKEN-Doppelpräfix korrigiert
[x] backrest DNS-Hardcoding entfernt
[x] diun ↔ gotify Erreichbarkeit hergestellt
[x] leere Netzwerke entfernt: br0, immich_net, kopia_default, netbox_default, diun_default
[x] anonyme/verwaiste Volumes bereinigt (9 anonyme + immich_model-cache alt)
[x] scanopy komplett entfernt (3 Container + 2 Volumes + Netz)
```
### Block B — Kritische Kernmigrationen (höchste Priorität)
### Block B — Kritische Kernmigrationen ✅ ABGESCHLOSSEN
```text
[x] vaultwarden
- von bridge weg
- Host-Port weg
- Secret-Datei
- Traefik sauber aktiv
- aus bridge, in frontend_net
- Host-Port entfernt
- ADMIN_TOKEN_FILE aktiv
- Traefik aktiv
[x] postgresql17
- Port 5432 entfernen
- Port 5432 entfernt
- nur backend_net
- Secret-Datei
- aus bridge raus
- POSTGRES_PASSWORD_FILE aktiv
[x] diun
- Join zu frontend_net
- gotify via Container-Name
- frontend_net beigetreten
- gotify via Container-Name (http://gotify:80) erreichbar
- Token via Portainer ENV
[x] mealie-postgres
- aus frontend_net raus
- mealie_internal
- nur mealie_mealie_internal
```
### Block C — Frontend-Stack finalisieren
```text
[ ] paperless-ngx
[x] gotify
- traefik.enable=true
- gotify.kaleschke.info
- Port entfernt
[x] paperless-ngx
- traefik.enable=true
- paperless.kaleschke.info
- Port entfernt
- tls=true ergänzt
[x] Paperless-AI
- traefik.enable=true
- aktiv
[x] PortainerCE
- traefik.enable=true
- Middleware aktiv
- direkte Ports entfernt
[x] Dozzle
- traefik.enable=true
- Middleware aktiv
- direkte Ports entfernt
[x] dashdot
- traefik.enable=true
- Middleware aktiv
- direkte Ports entfernt
[x] UptimeKuma
- traefik.enable=true
- uptime.kaleschke.info
- Port entfernt
- Middleware aktiv
[x] filebrowser
- aus bridge → frontend_net
- traefik.enable=true
- files.kaleschke.info
- Port entfernt
- Middleware aktiv
[x] scrutiny
- aus bridge → frontend_net
- traefik.enable=true
- scrutiny.kaleschke.info
- Ports entfernt
- Middleware aktiv
- als Git-Stack migriert
[x] gitea
- traefik.enable=true
- git.kaleschke.info
- SSH-Port 222 bleibt (Ausnahme dokumentiert)
[x] backrest
- traefik.docker.network=frontend_net korrigiert (war backend_net — Routing-Bug)
[ ] luckyBackup
- aus bridge → frontend_net
- traefik.enable=true
- Middleware
- Port entfernen
[ ] Paperless-AI
- traefik.enable=true
- paperless-ai.kaleschke.info
- Port entfernen
[ ] PortainerCE
- traefik.enable=true
- portainer.kaleschke.info
- Middleware
- direkte Ports entfernen
[ ] Dozzle
- traefik.enable=true
- dozzle.kaleschke.info
- Middleware
- direkte Ports entfernen
[ ] dashdot
- traefik.enable=true
- dash.kaleschke.info
- Middleware
- direkte Ports entfernen
[ ] theme-park
- nur wenn wirklich benötigt veröffentlichen
- direkte Ports 80/443 entfernen
```
### Block D — Bridge-/Dockerman-Container in Compose
```text
[ ] vaultwarden
[ ] postgresql17
[ ] mail-archiver
[ ] scrutiny
[ ] filebrowser
[x] vaultwarden
[x] postgresql17
[x] mail-archiver
[x] scrutiny ✅ (Git-Stack)
[x] filebrowser
[ ] luckyBackup
[ ] Stash
[ ] Tailscale-Docker
@@ -443,31 +480,42 @@ labels:
### Block E — Secrets-Migration
```text
[ ] vaultwarden → ADMIN_TOKEN_FILE
[ ] postgresql17 → POSTGRES_PASSWORD_FILE
[ ] mail-archiver → Authentication__Password_FILE
[ ] mealie → POSTGRES_PASSWORD_FILE
[ ] mealie-postgres → POSTGRES_PASSWORD_FILE
[ ] gotify → Passwort rotieren + Secret-Datei
[ ] diun → GOTIFY token als Datei
[ ] paperless-ngx → PAPERLESS_DBPASS_FILE
[ ] code-server → HASHED_PASSWORD / Secret
[ ] immich_server → DB_PASSWORD rotieren + Datei
[ ] immich_postgres → POSTGRES_PASSWORD_FILE
[ ] scanopy-postgres → Passwort rotieren
[x] vaultwarden → ADMIN_TOKEN_FILE
[x] postgresql17 → POSTGRES_PASSWORD_FILE
[x] mail-archiver → Portainer ENV (${MAILARCHIVER_AUTH_PASSWORD}) ✅
[x] mealie → Portainer ENV (kein _FILE-Support) ✅
[x] mealie-postgres → Portainer ENV (kein _FILE-Support) ✅
[x] gotify → GOTIFY_DEFAULTUSER_PASS_FILE ✅
[x] diun → GOTIFY_TOKEN via Portainer ENV ✅
[x] paperless-ngx → Portainer ENV (${PAPERLESS_DBPASS}) ✅
[x] code-server → PASSWORD_FILE ✅
[x] immich_server → Portainer ENV (${IMMICH_DB_PASSWORD}) ✅
[x] immich_postgres → POSTGRES_PASSWORD_FILE
[ ] immich_redis → anonymes Volume → named volume
[ ] luckyBackup → Secrets prüfen
```
### Block F — Feinschliff / Hardening
```text
[ ] backrest
- traefik.docker.network → frontend_net
- Mounts straffen
[ ] Redis
- optional named volume
[ ] immich_default
- internal: true setzen (kurzer Downtime nötig)
[ ] immich_redis
- anonymes Volume → named volume in Compose
[ ] immich_server
- anonymes Volume prüfen und benennen
[ ] backrest
- /mnt/user doppelt gemountet (einmal ro, einmal rw)
- rw-Mount auf konkrete Pfade einschränken
[ ] filebrowser
- /mnt/user:/srv ist sehr breit
- auf /mnt/user/documents:/srv einschränken wenn möglich
[ ] Redis
- optional named volume
[ ] netdata
@@ -490,14 +538,17 @@ labels:
| Container | Ausnahme | Begründung |
|---|---|---|
| `traefik` | Host-Ports 80/443 | zentraler Reverse Proxy |
| `Tailscale-Docker` | `host`, aktuell `privileged`, `TS_USERSPACE=true` | bestehender VPN-Zugang; Umstellung nur kontrolliert |
| `Tailscale-Docker` | `host`, aktuell `privileged` | bestehender VPN-Zugang; Umstellung nur kontrolliert |
| `binhex-official-pihole` | `host` | DNS-/DHCP-naher Sonderfall |
| `Plex-Media-Server` | `host` | Discovery / mDNS / Plex GDM |
| `netdata` | `host` + zusätzliche Rechte | Host-Metriken |
| `Glances` | `host` | Host-Metriken |
| `netalertx` | `host` + Netzwerksicht | ARP / Netzwerkscan |
| `scanopy-daemon` | `host`, aktuell privilegiert | hardware-/systemnaher Sonderfall |
| `scrutiny` | `privileged: true` | SMART-Datenzugriff auf Laufwerke |
| `PortainerCE` | Docker-Socket nicht dogmatisch `:ro` | Management-UI; Schreiboperationen können nötig sein |
| `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 Internetzugang für Cloudflare-API; `backend_net` ist `internal: true` |
| `mail-archiver` | `frontend_net` + `backend_net` | braucht Internetzugang für IMAP-Abruf (GMX, Gmail) und DB-Zugang |
---
@@ -507,12 +558,15 @@ labels:
Dieses Projekt wird **blockweise** umgesetzt, nicht wild containerweise.
### 11.2 Reihenfolge der Umsetzung
1. **Sprint 1:** Quick Wins + `vaultwarden`
2. **Sprint 2:** `postgresql17` + `diun/gotify`
3. **Sprint 3:** `mealie` / `mealie-postgres` + `mail-archiver`
4. **Sprint 4:** Frontend-Stack (`paperless`, `Portainer`, `Dozzle`, `dashdot`, etc.)
5. **Sprint 5:** Compose-Migration der Dockerman-Container
6. **Sprint 6:** Hardening / Secrets / Volumes / Sonderfälle
| Sprint | Inhalt | Status |
|---|---|---|
| Sprint 1 | Quick Wins + `vaultwarden` | ✅ Abgeschlossen |
| Sprint 2 | `postgresql17` + `diun/gotify` | ✅ Abgeschlossen |
| Sprint 3 | `mealie` / `mealie-postgres` + `mail-archiver` | ✅ Abgeschlossen |
| Sprint 4 | Frontend-Stack (`paperless`, `Portainer`, `Dozzle`, `dashdot`, `scrutiny`, `filebrowser`, `gitea`, `UptimeKuma`) | 🔄 In Bearbeitung |
| Sprint 5 | Compose-Migration der Dockerman-Container (`luckyBackup`, `Stash`, `Tailscale`, `netdata`, `Plex`, `Pi-hole`) | ⏳ Offen |
| Sprint 6 | Hardening / Secrets / Volumes / Sonderfälle (`immich_default`, Volumes, Mounts) | ⏳ Offen |
### 11.3 Regel für jede Änderung
Jeder Sprint folgt demselben Schema:
@@ -528,7 +582,7 @@ Jeder Sprint folgt demselben Schema:
### 11.4 Source-of-Truth-Hierarchie
1. **Dieses Dokument**
2. Compose-Dateien
3. operative Checklisten / Excel
3. operative Checklisten
4. ad-hoc Notizen / Chat
---
@@ -537,7 +591,7 @@ Jeder Sprint folgt demselben Schema:
Wenn mit einer KI gearbeitet wird, gilt immer:
> **„Lies zuerst `HOMELAB_ARCHITECTURE_MASTER_V2.md`, dann beantworte meine Frage.**
> **„Lies zuerst `HOMELAB_ARCHITECTURE_MASTER_V2.md`, dann beantworte meine Frage."**
Damit ist sofort klar:
- welche Netze Standard sind
@@ -548,66 +602,84 @@ Damit ist sofort klar:
- welche Ausnahmen bewusst dokumentiert sind
---
## ⚠️ Erweiterte Learnings (Portainer + GitOps)
### Secrets in Portainer Git-Stacks
## 13. Betriebserfahrungen und Entscheidungs-Log
Bei Deployments über Portainer mit Git-Repositories gilt:
### Secrets in Portainer Stacks
* Host-Pfade in `env_file` (z. B. `/mnt/...`) sind **nicht verfügbar**
* Portainer-Container hat keinen Zugriff auf diese Pfade
Bei Deployments über Portainer gilt:
#### Konsequenz
- Host-Pfade in `env_file` (z.B. `/mnt/...`) sind nicht verfügbar — Portainer-Container hat keinen Zugriff
- `env_file` ist für Secrets ungeeignet in diesem Setup
* `env_file` ist für Secrets ungeeignet in diesem Setup
**Standardlösung:** Portainer Environment Variables + `${VARIABLE_NAME}` in der Compose
#### Standardlösung
```yaml
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
```
* Verwendung von **Portainer Environment Variables**
* Compose nutzt Variablen:
* `POSTGRES_PASSWORD: ${VARIABLE_NAME}`
👉 Ergebnis:
* keine Secrets im Git
* funktionierender Git-Deployment-Flow
→ kein Secret im Git, funktionierender Deployment-Flow
---
### Umgang mit `_FILE` Variablen
Nicht alle Container unterstützen `_FILE`-basierte Secrets.
Nicht alle Container unterstützen `_FILE`-basierte Secrets:
#### Übersicht
| Container | `_FILE` Support |
|---|---|
| Vaultwarden | ✅ ja |
| PostgreSQL | ✅ ja |
| Gotify | ✅ ja (`GOTIFY_DEFAULTUSER_PASS_FILE`) |
| code-server | ✅ ja (`PASSWORD_FILE`) |
| Mealie | ❌ nein → Portainer ENV |
| diun | ❌ nein für Token → Portainer ENV |
| paperless-ngx | ❌ nein für DB-Pass → Portainer ENV |
* Vaultwarden → unterstützt `_FILE`
* PostgreSQL → unterstützt `_FILE`
* Mealie → unterstützt `_FILE` **nicht**
**Regel:** Wenn `_FILE` nicht unterstützt wird → Portainer Environment Variable
#### Regel
---
* Wenn `_FILE` nicht unterstützt wird:
→ Nutzung von Environment Variables über Portainer
### diun — Korrekte Konfiguration (v4.x)
Wichtige Learnings aus der Migration:
- `DIUN_NOTIFY_GOTIFY=true` existiert **nicht** — diese Variable gibt es in v4 nicht, führt zu `field not found` Fehler
- `DIUN_NOTIF_GOTIFY_TOKEN_FILE` wird **nicht** unterstützt — Token muss direkt gesetzt werden
- Gotify ist automatisch aktiv wenn `DIUN_NOTIF_GOTIFY_ENDPOINT` und `DIUN_NOTIF_GOTIFY_TOKEN` gesetzt sind
- `DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=true` nötig damit alle Container überwacht werden (sonst nur Container mit `diun.enable=true` Label)
**Korrekte Minimal-Konfiguration:**
```yaml
environment:
- DIUN_PROVIDERS_DOCKER=true
- DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=true
- DIUN_NOTIF_GOTIFY_ENDPOINT=http://gotify:80
- DIUN_NOTIF_GOTIFY_TOKEN=${GOTIFY_TOKEN} # via Portainer ENV
```
---
### ddns-updater — Netz-Ausnahme
`ddns-updater` hat keine Web-UI, würde laut Schema in `backend_net` gehören — bleibt aber bewusst in `frontend_net`.
**Begründung:** `backend_net` ist `internal: true` → kein Internetzugang. ddns-updater muss die Cloudflare-API erreichen. `frontend_net` ist hier die einzig funktionierende Option.
→ Dokumentierte Ausnahme in Kapitel 10.
---
### Netzwerk-Standard für Apps mit Datenbanken
#### Architektur-Regel
- App → `frontend_net` + internes Netzwerk
- Datenbank → nur internes Netzwerk (`internal: true`)
* 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`
👉 Vorteil:
* Datenbank vollständig isoliert
* keine externe Erreichbarkeit möglich
→ Datenbank vollständig isoliert, keine externe Erreichbarkeit möglich
---
@@ -615,54 +687,31 @@ Nicht alle Container unterstützen `_FILE`-basierte Secrets.
Bei Änderungen an Datenbanken oder Core-Services:
#### Vorgehen
1. Backup erstellen (`pg_dumpall`)
2. aktuellen Zustand sichern (`docker inspect`)
3. neue Compose in Git definieren
3. neue Compose in Git/Portainer definieren
4. Container stoppen & entfernen
5. neuen Stack deployen
6. Funktion prüfen (Logs + abhängige Services)
#### Grundsatz
→ Daten liegen im Volume und bleiben erhalten — Container sind austauschbar
* Daten liegen im Volume → bleiben erhalten
* Container sind austauschbar
---
👉 Ziel:
### mail-archiver — Hybrid-Dienst
* risikoarme Migration ohne Datenverlust
`mail-archiver` benötigt beide Netze:
## Mail-Archiver Netzwerk-Design
- `backend_net` → Verbindung zu `postgresql17`
- `frontend_net` → IMAP-Abruf von externen Mailservern (GMX, Gmail) + DNS-Auflösung
Der mail-archiver Service benötigt Zugriff auf zwei unterschiedliche Netzwerkbereiche:
### backend_net
- Verbindung zur PostgreSQL-Datenbank (postgresql17)
- rein interne Kommunikation
### frontend_net
- erforderlich für externe Verbindungen:
- IMAP (z. B. GMX, Gmail)
- DNS-Auflösung
- Internetzugriff allgemein
### Begründung
Ein ausschließlich internes Netzwerk (backend_net) ist für mail-archiver nicht ausreichend,
da der Service aktiv E-Mails von externen Mailservern abruft.
Durch die zusätzliche Anbindung an frontend_net wird:
- externer Mail-Zugriff ermöglicht
- gleichzeitig bleibt die Datenbank isoliert im backend_net
### Fazit
mail-archiver ist kein reiner Backend-Service, sondern ein hybrider Dienst:
→ benötigt sowohl interne als auch externe Netzwerkzugriffe
→ Kein reiner Backend-Dienst, sondern Hybrid mit externem Internetzugriff
---
## Schlussformel
Dieses Dokument ist keine lose Notiz, sondern das **operative Masterdokument** für die Docker- und Zugriffsarchitektur des Homelabs.
**Zielbild in einem Satz:**
`frontend_net` für alle Web-UIs, `backend_net` für interne Backends, app-interne Netze nur wenn technisch nötig, Tailscale für Remote-Admin-Zugriff, Traefik als einziger Web-Einstieg, keine produktiven `bridge`-Container mehr.
`frontend_net` für alle Web-UIs und Dienste mit Internetbedarf, `backend_net` für interne Backends, app-interne Netze nur wenn technisch nötig, Tailscale für Remote-Admin-Zugriff, Traefik als einziger Web-Einstieg, keine produktiven `bridge`-Container mehr.