Files
homelab-infra/HOMELAB_ARCHITECTURE_MASTER_V2.md
T

717 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HOMELAB_ARCHITECTURE — MASTER v2
> **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
1. [Systemüberblick](#1-systemüberblick)
2. [Architektur-Prinzipien](#2-architektur-prinzipien)
3. [Finales Netzwerk-Zielbild](#3-finales-netzwerk-zielbild)
4. [Zugangsmodell: Traefik vs. Tailscale](#4-zugangsmodell-traefik-vs-tailscale)
5. [Globale Sicherheitsregeln](#5-globale-sicherheitsregeln)
6. [Einordnungsschema für neue Container](#6-einordnungsschema-für-neue-container)
7. [Container-Zielbild (vollständig)](#7-container-zielbild-vollständig)
8. [Traefik-Label-Standard](#8-traefik-label-standard)
9. [Migrationsstrategie (Blöcke AF)](#9-migrationsstrategie-blöcke-a-f)
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)
---
## 1. Systemüberblick
| Eigenschaft | Wert |
|---|---|
| Host-OS | Unraid |
| Hostname | Kallilabcore |
| Reverse Proxy | Traefik v3 |
| VPN / Remote-Zugang | Tailscale (`Tailscale-Docker`, host-Netz) |
| DNS-Stack | Pi-hole (host) → Unbound (`dns_net`) |
| Basis-Domain | `kaleschke.info` |
| TLS | Let's Encrypt via Cloudflare DNS Challenge |
| Certresolver | `le` |
| 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/` |
| Grundsatz | Keine neuen Dockerman-Einzelcontainer |
---
## 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), `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_mealie_internal`, `immich_default`, `dns_net`)
Es gibt **keine künstlichen globalen Zusatznetze** wie `admin_net`, `monitoring_net` oder `media_net`.
### P3 — Datenbanken gehören nie ins `frontend_net`
Postgres, Redis und ähnliche Dienste laufen ausschließlich in `backend_net` oder einem eigenen internen Compose-Netz.
### P4 — Admin-UIs sind nicht öffentlich
Portainer, Dozzle, filebrowser, scrutiny, UptimeKuma, dashdot, code-server, luckyBackup, Traefik-Dashboard, netdata, Glances und netalertx sind standardmäßig **Tailscale-only** oder hinter Traefik **mit zentraler Middleware** abgesichert.
### P5 — Compose-first
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 als Portainer Environment Variables mit `${VARIABLE}` in der Compose.
Ziel: kein sensibles Secret mehr sichtbar in `docker inspect`.
### P7 — `restart: unless-stopped` ist Pflichtstandard
Jeder produktive Container nutzt `restart: unless-stopped`, außer eine Ausnahme ist dokumentiert.
### P8 — Least Privilege
- `security_opt: ["no-new-privileges:true"]` standardmäßig ergänzen
- `privileged: true` nur mit dokumentierter Begründung
- `read_only: true` und Non-Root nur nach getesteter Image-Kompatibilität
- Docker-Socket standardmäßig vorsichtig behandeln; **PortainerCE ist eine dokumentierte Ausnahme**
---
## 3. Finales Netzwerk-Zielbild
### 3.1 Netz-Logik
| Netzwerk | Typ | Zweck | Status |
|---|---|---|---|
| `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_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)
```text
Internet
traefik (80/443)
└── frontend_net
├── ö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-ngx
App-interne Netze
├── mealie_mealie_internal (internal: true) ✅
└── immich_default (internal: true ausstehend) ⏳
Host-Sonderfälle
├── Tailscale-Docker
├── binhex-official-pihole
├── Plex-Media-Server
├── netdata
├── Glances
└── netalertx
```
### 3.3 Architekturentscheidung (final)
**Wir bleiben final bei wenigen Netzen.**
Das ist bewusst näher an aktuellen Homelab-Traefik-Setups als ein künstlich übersegmentiertes Docker-Netzmodell.
Trennung erfolgt primär über:
- Traefik
- Auth-/Security-Middlewares
- Tailscale
- kein direktes Port-Publishing
- app-interne Netze
- Host-Sonderfälle nur mit Begründung
---
## 4. Zugangsmodell: Traefik vs. Tailscale
### 4.1 Öffentlich über Traefik
Diese Dienste dürfen final über echte `*.kaleschke.info`-Domains erreichbar sein:
- `homepage`
- `vaultwarden`
- `mealie`
- `paperless-ngx`
- `gotify`
- `gitea` (Web)
- `immich_server`
- `mail-archiver`
- `backrest`
### 4.2 Nicht öffentlich / nur Tailscale oder Traefik + Middleware
Diese Dienste sind **keine Public Apps**:
- `PortainerCE`
- `Dozzle`
- `UptimeKuma`
- `dashdot`
- `filebrowser`
- `scrutiny`
- `luckyBackup`
- `code-server`
- `Traefik-Dashboard`
- `netdata`
- `Glances`
- `netalertx`
### 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:
- Traefik sie routet
- zentrale Middleware aktiv ist
- keine direkten Host-Ports bestehen
- Zugriff zusätzlich durch Tailscale bzw. Auth begrenzt ist
---
## 5. Globale Sicherheitsregeln
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` 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`
8. Placeholder-Domains (`yourdomain.tld`) sind verboten
9. `privileged: true` nur mit Begründung
10. Volume-Mounts so klein und so read-only wie möglich
11. Neue Dienste nur via Compose Manager / YAML
12. Änderungen immer gegen dieses Dokument prüfen
---
## 6. Einordnungsschema für neue Container
### Schritt 1 — Hat der Dienst eine Web-UI?
- **Ja** → `frontend_net`
- **Nein** → weiter zu Schritt 2
### 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 4 — Ist es eine Datenbank oder ein Cache?
- **Ja** → niemals `frontend_net`, nur `backend_net` oder internes Compose-Netz
### 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 6 — Braucht der Dienst Host-/Discovery-/L2-Sicht?
- **Ja** → `host` nur mit dokumentierter Begründung
- **Nein** → kein `host`
### 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
---
## 7. Container-Zielbild (vollständig)
Legende Status:
- `✅` = umgesetzt
- `⏳` = noch zu migrieren / zu korrigieren
### 7.1 Infrastruktur / Core
| Container | Status | Soll-Netz(e) | Finaler Zugang | Finaler Sollzustand | Offene Punkte |
|---|---|---|---|---|---|
| `traefik` | ✅ | `frontend_net`, `backend_net` | öffentlich | zentraler Ingress, 80/443 direkt | Middleware-File-Provider sauber pflegen |
| `homepage` | ✅ | `frontend_net` | Traefik | öffentliche Startseite | — |
| `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 | `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 | 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 | kein Host-Port, `POSTGRES_PASSWORD_FILE` | — |
| `Redis` | ✅ | `backend_net` | intern | intern-only Cache | optional named volume statt anonym |
| `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 | 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 | — |
### 7.5 Admin / Operations
| Container | Status | Soll-Netz(e) | Finaler Zugang | Finaler Sollzustand | Offene Punkte |
|---|---|---|---|---|---|
| `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 | 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 |
---
## 8. Traefik-Label-Standard
Jeder Dienst mit Traefik-Routing nutzt dieses Muster:
```yaml
labels:
- traefik.enable=true
- traefik.docker.network=frontend_net
- traefik.http.routers.<name>.rule=Host(`<subdomain>.kaleschke.info`)
- traefik.http.routers.<name>.entrypoints=websecure
- traefik.http.routers.<name>.tls=true
- traefik.http.routers.<name>.tls.certresolver=le
- traefik.http.services.<name>.loadbalancer.server.port=<interner-port>
```
### Zusatz für Admin-Dienste
```yaml
- traefik.http.routers.<name>.middlewares=dashboard-auth@file,secure-headers@file
```
### Regeln
- `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
---
## 9. Migrationsstrategie (Blöcke AF)
**Letzte Aktualisierung:** 2026-03-26
### Block A — Quick Wins ✅ ABGESCHLOSSEN
```text
[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 ✅ ABGESCHLOSSEN
```text
[x] vaultwarden
- aus bridge, in frontend_net
- Host-Port entfernt
- ADMIN_TOKEN_FILE aktiv
- Traefik aktiv
[x] postgresql17
- Port 5432 entfernt
- nur backend_net
- POSTGRES_PASSWORD_FILE aktiv
[x] diun
- frontend_net beigetreten
- gotify via Container-Name (http://gotify:80) erreichbar
- Token via Portainer ENV
[x] mealie-postgres
- aus frontend_net raus
- nur mealie_mealie_internal
```
### Block C — Frontend-Stack finalisieren
```text
[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
[ ] theme-park
- nur wenn wirklich benötigt veröffentlichen
- direkte Ports 80/443 entfernen
```
### Block D — Bridge-/Dockerman-Container in Compose
```text
[x] vaultwarden ✅
[x] postgresql17 ✅
[x] mail-archiver ✅
[x] scrutiny ✅ (Git-Stack)
[x] filebrowser ✅
[ ] luckyBackup
[ ] Stash
[ ] Tailscale-Docker
[ ] netdata
[ ] Plex-Media-Server
[ ] binhex-official-pihole
```
### Block E — Secrets-Migration
```text
[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
[ ] 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
- leere CLAIM-Vars entfernen
[ ] scrutiny
- später prüfen, ob privileged reduziert werden kann
[ ] Tailscale-Docker
- später prüfen, ob TS_USERSPACE/privileged bereinigt werden kann
[ ] Pi-hole
- zuletzt konsolidieren, nicht als Erstprojekt
```
---
## 10. Bekannte Ausnahmen und Begründungen
| Container | Ausnahme | Begründung |
|---|---|---|
| `traefik` | Host-Ports 80/443 | zentraler Reverse Proxy |
| `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 |
| `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 |
---
## 11. Projektorganisation und Arbeitsmodus
### 11.1 Unser Arbeitsprinzip
Dieses Projekt wird **blockweise** umgesetzt, nicht wild containerweise.
### 11.2 Reihenfolge der Umsetzung
| 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:
1. Zielbild in diesem Dokument prüfen
2. nur den aktuellen Block anfassen
3. Compose-Datei ändern
4. deployen
5. testen
6. dokumentieren / abhaken
7. erst dann nächster Schritt
### 11.4 Source-of-Truth-Hierarchie
1. **Dieses Dokument**
2. Compose-Dateien
3. operative Checklisten
4. ad-hoc Notizen / Chat
---
## 12. Nutzung mit KI / Kontext-Regel
Wenn mit einer KI gearbeitet wird, gilt immer:
> **„Lies zuerst `HOMELAB_ARCHITECTURE_MASTER_V2.md`, dann beantworte meine Frage."**
Damit ist sofort klar:
- welche Netze Standard sind
- welche Container wohin gehören
- welche Dienste öffentlich sein dürfen
- welche Dienste nur intern/VPN-only sind
- welche Migrationen noch offen sind
- welche Ausnahmen bewusst dokumentiert sind
---
## 13. Betriebserfahrungen und Entscheidungs-Log
### Secrets in Portainer Stacks
Bei Deployments über Portainer gilt:
- 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
**Standardlösung:** Portainer Environment Variables + `${VARIABLE_NAME}` in der Compose
```yaml
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
```
→ kein Secret im Git, funktionierender Deployment-Flow
---
### Umgang mit `_FILE` Variablen
Nicht alle Container unterstützen `_FILE`-basierte Secrets:
| 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 |
**Regel:** Wenn `_FILE` nicht unterstützt wird → Portainer Environment Variable
---
### 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
- 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`
→ Datenbank vollständig isoliert, keine externe Erreichbarkeit möglich
---
### Migrations-Standard für kritische Services
Bei Änderungen an Datenbanken oder Core-Services:
1. Backup erstellen (`pg_dumpall`)
2. aktuellen Zustand sichern (`docker inspect`)
3. neue Compose in Git/Portainer definieren
4. Container stoppen & entfernen
5. neuen Stack deployen
6. Funktion prüfen (Logs + abhängige Services)
→ Daten liegen im Volume und bleiben erhalten — Container sind austauschbar
---
### mail-archiver — Hybrid-Dienst
`mail-archiver` benötigt beide Netze:
- `backend_net` → Verbindung zu `postgresql17`
- `frontend_net` → IMAP-Abruf von externen Mailservern (GMX, Gmail) + DNS-Auflösung
→ 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 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.