Wie das alles zusammenspielt
Jeder selbst gehostete Dienst bekommt eine eigene .local-Domain.
Pi-hole löst diese Domains auf die FortiGate WAN-IP auf.
Die FortiGate leitet Port 443 per VIP an Nginx weiter.
Nginx entscheidet per server_name welcher Dienst gemeint ist —
und proxied den Traffic intern, auch zu Diensten auf anderen Servern.
Das Ergebnis: jeder Dienst ist unter einer lesbaren HTTPS-URL erreichbar, egal auf welchem Server er läuft. Nginx ist der einzige der ein Zertifikat braucht.
Browser tippt: https://paperless.local │ DNS-Anfrage ▼ Pi-hole paperless.local → 203.0.113.10 (FortiGate WAN-IP) │ HTTPS :443 ▼ FortiGate VIP-HTTPS → 10.0.1.71:443 (Server1) │ intern HTTP ▼ Nginx auf Server1 server_name paperless.local proxy_pass → 10.0.1.82:8000 (Server2) │ ▼ Paperless-ngx antwortet ✓
Geräte im Mesh-Netz (192.168.68.0/22) können das FortiGate-LAN
(10.0.1.0/24) nicht direkt erreichen — die Subnetze sind getrennt.
Alle Zugriffe müssen über die FortiGate WAN-IP laufen.
Deshalb zeigen alle lokalen DNS-Einträge auf die FortiGate WAN-IP,
nicht auf die Server direkt.
Lokale CA erstellen
mkcert erstellt eine lokale Certificate Authority (CA) und generiert damit
vertrauenswürdige Zertifikate für beliebige Domains — auch .local-Domains
die es im öffentlichen DNS nicht gibt. Die CA wird einmalig auf Server1 erstellt
und danach auf alle Geräte verteilt.
libnss3-tools für die Firefox-Integration.sudo apt install -y libnss3-tools
wget https://dl.filippo.io/mkcert/latest?for=linux/amd64 -O mkcert chmod +x mkcert sudo mv mkcert /usr/local/bin/mkcert
mkcert -install # CA-Pfad anzeigen — merken! mkcert -CAROOT # Ausgabe z.B.: /root/.local/share/mkcert
Ein SAN-Zertifikat für alle Domains
Statt für jeden Dienst ein eigenes Zertifikat zu generieren, deckt ein einziges SAN-Zertifikat (Subject Alternative Name) alle lokalen Domains ab. Das vereinfacht die Verwaltung erheblich — ein Update, eine Datei, alle Dienste.
cd /opt/docker/nginx/certs mkcert \ navidrome.local \ pihole.local \ homepage.local \ portainer.local \ portainer2.local \ uptime.local \ mealie.local \ paperless.local \ linkwarden.local \ aliasvault.local \ joplin.local \ cloud.local # mkcert benennt die Datei nach dem ersten Domain-Namen + Anzahl weiterer # Umbenennen für saubere Referenz in allen Nginx-Configs: mv navidrome.local+11.pem homelab.pem mv navidrome.local+11-key.pem homelab-key.pem
Kommt ein neuer Dienst dazu, muss das Zertifikat neu generiert werden —
mit allen bisherigen Domains plus der neuen.
Danach Nginx neu laden: docker exec nginx nginx -s reload.
Auf Windows zusätzlich den HSTS-Cache leeren:
chrome://net-internals/#hsts → Domain eintragen → Delete.
Auf Android die CA neu installieren.
Docker Compose & Verzeichnisstruktur
/opt/docker/nginx/ ├── docker-compose.yml ├── certs/ │ ├── homelab.pem ← SAN-Zertifikat │ └── homelab-key.pem ← privater Schlüssel └── conf.d/ ├── 00-redirect.conf ← HTTP → HTTPS global ├── navidrome.local.conf ├── paperless.local.conf ├── joplin.local.conf └── … eine .conf pro Domain
services: nginx: image: nginx:alpine container_name: nginx restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./conf.d:/etc/nginx/conf.d:ro - ./certs:/etc/nginx/certs:ro extra_hosts: - "host.docker.internal:host-gateway" networks: default: name: proxy external: true
server { listen 80 default_server; server_name _; return 301 https://$host$request_uri; }
docker system prune gelöscht, dann neu anlegen.docker network create proxy
docker run --rm \ -v /opt/docker/nginx/conf.d:/etc/nginx/conf.d:ro \ -v /opt/docker/nginx/certs:/etc/nginx/certs:ro \ nginx:alpine nginx -t
cd /opt/docker/nginx docker compose up -d docker compose logs -f
Server-Blöcke pro Dienst
Jeder Dienst bekommt eine eigene .conf-Datei in conf.d/.
Das Muster ist immer gleich — nur server_name und proxy_pass
ändern sich. Dienste auf anderen Servern werden einfach per IP referenziert.
server { listen 443 ssl; server_name dienst.local; ssl_certificate /etc/nginx/certs/homelab.pem; ssl_certificate_key /etc/nginx/certs/homelab-key.pem; location / { proxy_pass http://10.0.1.71:PORT; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WebSocket (für Dienste die es brauchen) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }
| Dienst | server_name | proxy_pass | Besonderheit |
|---|---|---|---|
| Pi-hole | pihole.local | 10.0.1.71:8888 | Redirect / → /admin/ |
| Navidrome | navidrome.local | 10.0.1.71:4533 | WebSocket nötig |
| Portainer | portainer.local | 10.0.1.71:9000 | proxy_read_timeout 86400 |
| Paperless | paperless.local | 10.0.1.82:8000 | Server2 — andere IP! |
| Joplin | joplin.local | 10.0.1.83:22300 | Server3 — nur Web-UI |
| AliasVault | aliasvault.local | 10.0.1.83:8300 | client_max_body_size 20M |
| Nextcloud | cloud.local | 10.0.1.82:PORT | client_max_body_size 1G |
Die Pi-hole Web-UI liegt unter /admin/, nicht unter /.
Ohne Redirect landet man auf einer leeren Seite. Lösung: in der Location-Block
return 301 https://$host/admin/; für location / und
dann einen separaten Block für location /admin/ mit dem proxy_pass.
Lokale DNS-Einträge
Im Pi-hole Admin unter Local DNS → DNS Records für jede Domain einen Eintrag anlegen. Alle zeigen auf die FortiGate WAN-IP — niemals auf die Server direkt.
Root-CA auf alle Geräte bringen
Die mkcert Root-CA muss auf jedem Gerät installiert werden das den lokalen Domains vertrauen soll. Ohne CA-Installation erscheint im Browser eine Zertifikatswarnung trotz gültigem Zertifikat.
# CA-Datei in Home-Verzeichnis kopieren cp $(mkcert -CAROOT)/rootCA.pem ~/homelab-ca.pem # Von Windows PowerShell herunterladen: scp user@203.0.113.10:~/homelab-ca.pem C:\Users\DeinUser\Downloads\
.crt umbenennen2. Doppelklick → Zertifikat installieren
3. Lokaler Computer
4. Vertrauenswürdige Stammzertifizierungsstellen
5. Browser neu starten
.pem-Datei aufs Gerät2. Einstellungen → Sicherheit
3. Verschlüsselung & Anmeldedaten
4. CA-Zertifikat installieren
5. Datei auswählen
Einstellungen → Datenschutz → Zertifikate anzeigen → Importieren
Auch wenn Windows die CA kennt, muss Firefox sie separat importieren.
Wird das mkcert-Zertifikat neu generiert (neue Domain hinzugefügt), speichert
Chrome unter Windows den alten HSTS-Eintrag. Die Seite bleibt mit Zertifikatsfehler
stecken obwohl alles korrekt ist. Fix: chrome://net-internals/#hsts
öffnen, Domain eintragen, Delete klicken. Danach neu laden.
Was schiefgehen kann
host.docker.internal in Configs verwendet wird und der
Test-Container den Hostnamen nicht auflösen kann. Fix: in allen Configs direkt
die Server-IP verwenden. Schnell per
sed -i 's/host.docker.internal/10.0.1.71/g' conf.d/*.conf.
proxy muss vor dem Start existieren.
docker compose up legt es nicht automatisch an wenn es als
external: true definiert ist. Fix: docker network create proxy.
Nach docker system prune erneut nötig.
127.0.0.1:port statt auf die Server-IP.
Nginx auf Server1 kann localhost von Server2 nicht erreichen.
In der Docker Compose des Dienstes den Port auf die Server-IP binden:
10.0.1.82:8000:8000 statt 127.0.0.1:8000:8000.
HOMEPAGE_ALLOWED_HOSTS=homepage.local.
Ohne diese Variable funktioniert der Nginx-Proxy nicht.
VIP-HTTP (Port 80) auf der FortiGate fehlt oder die zugehörige
Firewall Policy ist nicht angelegt. Nginx kann den HTTP→HTTPS-Redirect nur
ausführen wenn Port 80 überhaupt an Nginx weitergeleitet wird.