Zum Inhalt springen
SONOYA.com
Web & Code

Reverse Proxy mit Caddy: HTTPS für Dienste im Heimnetz

Caddy als Reverse Proxy mit Docker: gültige Let's-Encrypt-Zertifikate für Home Assistant und Co. per DNS-Challenge, ohne Portfreigabe im Heimnetz.

Von

Ein Pfeil, der sich in drei Richtungen aufteilt, als Sinnbild für Caddy, der HTTPS-Anfragen an mehrere Dienste im Heimnetz verteilt

Home Assistant läuft auf 192.168.178.20:8123, Uptime Kuma auf Port 3001, Vaultwarden auf einem dritten Port, und jeder Browser warnt bei jedem Aufruf vor einer unsicheren Verbindung. Ein Reverse Proxy räumt damit auf: Du rufst Deine Dienste unter Namen wie ha.home.deinedomain.de auf, und jede Verbindung ist mit einem echten, von allen Geräten anerkannten Zertifikat verschlüsselt.

Diese Anleitung richtet Caddy als Reverse Proxy mit Docker Compose ein. Die Zertifikate holt Caddy per DNS-Challenge bei Let’s Encrypt, deshalb muss keiner Deiner Dienste aus dem Internet erreichbar sein, und Du brauchst keine Portfreigabe im Router. Diesen internen Weg gehen wir vollständig durch, vom API-Token bis zum Namenseintrag im Heimnetz.

Was ein Reverse Proxy für Dich erledigt

Ein Reverse Proxy ist ein Webserver, der vor Deinen anderen Diensten steht. Alle Browser sprechen nur mit ihm, auf den Standardports 80 und 443. Er schaut, welcher Name aufgerufen wurde, und reicht die Anfrage an den passenden Dienst weiter: ha.home.deinedomain.de an Home Assistant, kuma.home.deinedomain.de an Uptime Kuma. Die Verschlüsselung übernimmt er für alle gemeinsam, Deine Dienste selbst müssen von HTTPS nichts wissen.

Für ein gültiges Zertifikat muss Let’s Encrypt prüfen, dass Dir die Domain gehört. Der übliche Weg, die HTTP-Challenge, ruft dafür eine Datei auf Deinem Server über Port 80 aus dem Internet ab. Im Heimnetz ohne Portfreigabe klappt das nicht. Bei der DNS-Challenge legt Caddy stattdessen über die API Deines DNS-Anbieters kurz einen TXT-Eintrag an, den Let’s Encrypt prüft. Dein Server bleibt dabei unsichtbar. Laut Let’s Encrypt ist die DNS-Challenge außerdem der einzige Weg zu einem Wildcard-Zertifikat wie *.home.deinedomain.de, das alle Deine Subdomains auf einmal abdeckt.

Warum Caddy

Caddy holt und erneuert Zertifikate von sich aus, ohne Certbot und ohne Cronjob, und ein weitergeleiteter Dienst braucht im Caddyfile genau drei Zeilen. Der Haken: Für die DNS-Challenge brauchst Du ein Zusatzmodul für Deinen DNS-Anbieter, und das ist im offiziellen Image nicht enthalten. Du baust Dir deshalb einmalig ein eigenes Image, dafür reichen vier Zeilen. Nginx Proxy Manager ist die Alternative mit Weboberfläche und Klick-Konfiguration, Traefik konfiguriert sich über Labels an den Containern und spielt seine Stärken eher in größeren Umgebungen aus. Für ein Heimnetz mit einer Handvoll Diensten ist Caddy der kürzeste Weg.

Was Du brauchst

  • Einen Linux-Rechner im Heimnetz mit Docker und dem Compose-Plugin, etwa einen Raspberry Pi 4 oder 5 mit 64-Bit-System oder einen Mini-PC. In dieser Anleitung hat er die Adresse 192.168.178.10.
  • Eine eigene Domain, deren DNS bei Cloudflare liegt. Der kostenlose Plan reicht. Als Platzhalter steht hier deinedomain.de.
  • Zum Auflösen der Namen im Heimnetz entweder Pi-hole oder eine FRITZ!Box.

Getestet ist der Aufbau mit Caddy 2.11.9, dem Modul caddy-dns/cloudflare in Version 0.2.4, Docker Engine 29 und Docker Compose 5: Build, Caddyfile, Weiterleitung an Uptime Kuma 2 und Vaultwarden 1.37.5, WebSockets, die Weiterleitung von HTTP auf HTTPS und die Nutzung des Wildcard-Zertifikats für alle Subdomains. Weil die Testumgebung keine Domain bei Cloudflare hatte, stellte dort Caddys interne Zertifizierungsstelle das Zertifikat aus. Der Abruf bei Let’s Encrypt selbst folgt der offiziellen Doku.

Schritt 1: API-Token bei Cloudflare anlegen

Caddy braucht ein Token, mit dem er DNS-Einträge Deiner Domain ändern darf, und zwar nur dieser einen Domain. Öffne im Cloudflare-Dashboard My Profile > API Tokens und klicke auf Create Token. Wähle die Vorlage Edit zone DNS mit Use template.

Unter Permissions steht bereits Zone / DNS / Edit. Füge mit + Add more eine zweite Zeile Zone / Zone / Read hinzu, denn das Cloudflare-Modul für Caddy braucht beide Rechte. Unter Zone Resources wählst Du Include / Specific zone / deinedomain.de. Klicke auf Continue to summary und dann auf Create Token. Cloudflare zeigt das Token nur ein einziges Mal an, kopier es also sofort in Deinen Passwortmanager.

Ob das Token funktioniert, prüfst Du auf dem Docker-Host:

Code
curl "https://api.cloudflare.com/client/v4/user/tokens/verify" --header "Authorization: Bearer DEIN_TOKEN"

Ersetze DEIN_TOKEN durch das kopierte Token. In der Antwort muss This API Token is valid and active stehen.

Schritt 2: Caddy mit dem DNS-Modul bauen

Melde Dich per SSH auf dem Docker-Host an und lege die Ordner für Caddy an. Das gemeinsame Docker-Netz proxy verbindet Caddy später mit Deinen Containern:

Code
docker network create proxy
mkdir -p ~/caddy/conf
cd ~/caddy

Leg mit nano Dockerfile die Bauanleitung für das eigene Image an:

Code
FROM caddy:2.11.9-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare

FROM caddy:2.11.9
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

Das ist das Verfahren aus der Beschreibung des offiziellen Caddy-Images: Das Builder-Image kompiliert mit xcaddy ein Caddy-Programm samt Cloudflare-Modul, das fertige Programm landet dann im normalen, schlanken Caddy-Image. Beide Zeilen tragen dieselbe Versionsnummer. Welche aktuell ist, siehst Du auf der Release-Seite von Caddy. Speichere mit Strg + O, bestätige mit Enter und schließe den Editor mit Strg + X.

Schritt 3: compose.yaml und Token hinterlegen

Leg mit nano compose.yaml die Compose-Datei an:

Code
services:
  caddy:
    build: .
    image: caddy-cloudflare:2.11.9
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    environment:
      CF_API_TOKEN: ${CF_API_TOKEN}
    volumes:
      - ./conf:/etc/caddy
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - proxy

volumes:
  caddy_data:
  caddy_config:

networks:
  proxy:
    external: true

build: . baut das Image aus dem Dockerfile im selben Ordner. Port 443 über UDP ist für HTTP/3 gedacht, das Caddy von Haus aus anbietet. Den Ordner conf hängst Du komplett ein und nicht nur die einzelne Datei, damit Caddy Änderungen am Caddyfile zuverlässig sieht. Im Volume caddy_data liegen Zertifikate und Schlüssel. Die Doku des Images betont, dass dieses Verzeichnis kein Cache ist, also lösche es nie mit docker compose down -v. Was external: true bedeutet und wie Compose Volumes verwaltet, erklärt der Artikel Docker Compose verstehen: Volumes, Netzwerke und .env ausführlich.

Das Token gehört nicht in die compose.yaml, sondern in die Datei .env daneben. Öffne sie mit nano .env und trag diese Zeile ein:

Code
CF_API_TOKEN=DEIN_TOKEN

Ersetze DEIN_TOKEN wieder durch Dein Token, ohne Anführungszeichen. Danach darf nur noch Dein Benutzer die Datei lesen:

Code
chmod 600 .env

Schritt 4: Das Caddyfile für drei Dienste

Jetzt kommt die eigentliche Konfiguration. Öffne sie mit nano conf/Caddyfile:

Code
{
	email du@deinedomain.de
}

*.home.deinedomain.de {
	tls {
		dns cloudflare {env.CF_API_TOKEN}
	}
	abort
}

ha.home.deinedomain.de {
	reverse_proxy 192.168.178.20:8123
}

kuma.home.deinedomain.de {
	reverse_proxy uptime-kuma:3001
}

vault.home.deinedomain.de {
	reverse_proxy vaultwarden:80
}

Ganz oben steht Deine E-Mail-Adresse für das Konto bei der Zertifizierungsstelle. Der Block *.home.deinedomain.de holt per DNS-Challenge ein Wildcard-Zertifikat, mit dem Token aus der Umgebungsvariable. Sein abort beendet Verbindungen zu Subdomains, die Du nicht eingerichtet hast. Seit Version 2.10 verwendet Caddy dieses Wildcard-Zertifikat laut Doku zu den Caddyfile-Mustern automatisch für alle darunter aufgeführten Subdomains, statt für jede ein eigenes Zertifikat zu beantragen. Im Test beantragte Caddy genau ein Zertifikat, das für *.home.deinedomain.de. Angenehmer Nebeneffekt: Deine einzelnen Dienstnamen tauchen nicht in den öffentlichen Zertifikatsprotokollen auf.

Darunter folgt je Dienst ein Block mit einer Zeile reverse_proxy und dem Ziel. Home Assistant läuft auf einem anderen Gerät und wird über IP-Adresse und Port angesprochen. Uptime Kuma und Vaultwarden laufen als Container im Netz proxy, deshalb reicht ihr Dienstname mit dem Port im Container. Ersetze überall deinedomain.de, die E-Mail-Adresse und die IP-Adresse durch Deine Werte. Prüf bei Home Assistant den Port: Laut Doku der HTTP-Integration ist der Standardport bei Home Assistant OS seit Version 2026.8 Port 80, bei Home Assistant Container weiter 8123. Welcher Port bei Dir eingestellt ist, siehst Du in Home Assistant unter Einstellungen > System > Netzwerk.

Schritt 5: Dienste ins Netz von Caddy hängen

Damit Caddy einen Container über seinen Namen findet, muss der im Netz proxy hängen. Uptime Kuma bekommt einen eigenen Ordner:

Code
mkdir -p ~/uptime-kuma
cd ~/uptime-kuma

Leg darin mit nano compose.yaml diese Datei an:

Code
services:
  uptime-kuma:
    image: louislam/uptime-kuma:2
    restart: unless-stopped
    volumes:
      - kuma-daten:/app/data
    networks:
      - proxy

volumes:
  kuma-daten:

networks:
  proxy:
    external: true

Auffällig ist, was fehlt: Es gibt keinen Abschnitt ports. Uptime Kuma ist nur noch über Caddy erreichbar, nicht mehr direkt über Port 3001. Speichere wie oben und starte den Container mit docker compose up -d. Bei einem bestehenden Dienst ergänzt Du nur die beiden networks-Abschnitte und entfernst die Portfreigabe.

Vaultwarden bindest Du nach demselben Muster an, etwa im Ordner ~/vaultwarden. Neu ist nur die Variable DOMAIN, die exakt auf die Adresse hinter Caddy zeigen muss:

Code
services:
  vaultwarden:
    image: vaultwarden/server:1.37.5
    restart: unless-stopped
    environment:
      DOMAIN: "https://vault.home.deinedomain.de"
    volumes:
      - vw-daten:/data
    networks:
      - proxy

volumes:
  vw-daten:

networks:
  proxy:
    external: true

Admin-Token und das Schließen der Registrierung erklärt Vaultwarden auf einem Droplet: eigener Passwortmanager. Den Caddy-Container aus jener Anleitung lässt Du weg, diese Rolle übernimmt hier Dein zentraler Caddy.

Um Header und WebSockets musst Du Dich nicht kümmern. Laut Doku von reverse_proxy reicht Caddy den aufgerufenen Namen unverändert weiter und setzt X-Forwarded-For, X-Forwarded-Proto und X-Forwarded-Host selbst. WebSocket-Verbindungen, die Home Assistant, Uptime Kuma und Vaultwarden für Live-Aktualisierungen nutzen, leitet Caddy ohne weitere Zeile durch. Im Test kam die Vaultwarden-Benachrichtigung über WebSocket durch Caddy mit 101 Switching Protocols an.

Schritt 6: Caddy starten und das Zertifikat prüfen

Zurück im Ordner ~/caddy baust und startest Du Caddy:

Code
cd ~/caddy
docker compose up -d --build

Der erste Build lädt das Builder-Image und kompiliert Caddy, auf einem Raspberry Pi kann das einige Minuten dauern. Danach verfolgst Du die Ausgabe:

Code
docker compose logs -f caddy

Sobald Let’s Encrypt den TXT-Eintrag geprüft hat, erscheint eine Zeile mit certificate obtained successfully und *.home.deinedomain.de. Mit Strg + C beendest Du die Anzeige, Caddy läuft weiter. Ob das Modul wirklich im Image steckt, zeigt die Modulliste, in der dns.providers.cloudflare stehen muss:

Code
docker compose exec caddy caddy list-modules

Änderst Du später das Caddyfile, etwa für einen vierten Dienst, lädst Du es ohne Neustart neu:

Code
docker compose exec -w /etc/caddy caddy caddy reload

Schritt 7: Namen im Heimnetz auflösen

Das Zertifikat ist da, aber noch weiß kein Gerät in Deinem Netz, dass ha.home.deinedomain.de zu 192.168.178.10 gehört. Wichtig: Alle Namen zeigen auf den Docker-Host mit Caddy, nicht auf das Gerät, auf dem der Dienst läuft. Es gibt zwei Wege, je nachdem, was in Deinem Netz die Namen auflöst.

Mit Pi-hole

Öffne die Weboberfläche von Pi-hole und gehe zu Settings > Local DNS Records. Trag dort unter Domain den Namen ha.home.deinedomain.de und unter Associated IP die Adresse 192.168.178.10 ein und bestätige mit dem Plus-Symbol. Wiederhole das für kuma und vault. Die Namen bleiben so komplett privat, im öffentlichen DNS steht nichts davon. Falls Pi-hole bei Dir noch nicht der DNS-Server im Heimnetz ist, zeigt Pi-hole mit Unbound: eigener DNS-Resolver ohne Google, wie Du es einrichtest und den Router darauf zeigen lässt.

Nur mit der FRITZ!Box

Die FRITZ!Box kann keine eigenen Einträge für fremde Domains anlegen. Du legst den Eintrag deshalb öffentlich bei Cloudflare an: unter DNS > Records mit Add record einen Eintrag vom Typ A, Name *.home, IPv4-Adresse 192.168.178.10, und den Schalter Proxy status auf DNS only. Eine private Adresse im öffentlichen DNS ist von außen nutzlos, sie verrät nur Deine interne Adresse.

Die FRITZ!Box blockiert solche Antworten aber zum Schutz vor DNS-Rebinding. Öffne deshalb in ihrer Oberfläche Heimnetz > Netzwerk > Netzwerkeinstellungen und trag im Bereich DNS-Rebind-Schutz jeden Namen vollständig in eine eigene Zeile ein, also ha.home.deinedomain.de, kuma.home.deinedomain.de und vault.home.deinedomain.de. Bei neueren FRITZ!OS-Versionen liegt der Bereich hinter der Schaltfläche für die weiteren Netzwerkeinstellungen.

Schritt 8: Home Assistant den Proxy erlauben

Home Assistant lehnt Anfragen über einen Reverse Proxy ab, bis Du ihn ausdrücklich erlaubst. Öffne in Home Assistant Einstellungen > System > Netzwerk und such den Abschnitt zum HTTP-Server. Schalte dort die Option ein, die dem Header X-Forwarded-For vertraut (englisch „Trust X-Forwarded-For”), und trag als vertrauenswürdigen Proxy („Trusted proxies”) die Adresse 192.168.178.10 ein. Das ist die Adresse des Docker-Hosts, denn Verbindungen aus dem Caddy-Container zu anderen Geräten tragen dessen Adresse als Absender. Home Assistant startet nach dem Speichern neu und bittet Dich, die Einstellung zu bestätigen.

Vor Version 2026.8 stehen dieselben Einstellungen in der configuration.yaml, danach startest Du Home Assistant neu:

Code
http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 192.168.178.10

Öffne jetzt https://ha.home.deinedomain.de im Browser. Das Schloss in der Adresszeile zeigt ein Zertifikat für *.home.deinedomain.de, ausgestellt von Let’s Encrypt oder, falls Caddy ausweichen musste, von ZeroSSL, und der Aufruf mit http:// landet automatisch auf der HTTPS-Adresse.

Wenn es nicht klappt

API token '' appears invalid im Log heißt, dass das Token nicht im Container angekommen ist. Die .env liegt dann nicht neben der compose.yaml, oder der Variablenname weicht ab. Mit docker compose config siehst Du, welcher Wert eingesetzt wird.

Das Zertifikat kommt nicht, im Log steht etwas von propagate oder timed out. Caddy prüft vor der Freigabe, ob der TXT-Eintrag sichtbar ist. Stört dabei der DNS-Server im Heimnetz, gib Caddy einen öffentlichen Resolver vor, wie es auch die Anleitung des Cloudflare-Moduls vorsieht. Ergänze dazu im tls-Block unter der Zeile mit dns cloudflare die Zeile resolvers 1.1.1.1 und lade das Caddyfile neu. Bleibt der Fehler, prüf, ob das Token beide Rechte hat und auf die richtige Zone beschränkt ist.

502 Bad Gateway im Browser bedeutet, dass Caddy den Dienst nicht erreicht. Die Ursache steht in docker compose logs caddy. Bei lookup uptime-kuma ... no such host hängt der Container nicht im Netz proxy oder läuft nicht, bei connection refused stimmt der Port nicht. Gemeint ist immer der Port im Container, nicht eine frühere Portfreigabe auf dem Host.

400: Bad Request bei Home Assistant heißt, dass Schritt 8 fehlt oder die eingetragene Adresse nicht passt. Im Log von Home Assistant steht dann „A request from a reverse proxy was received from …” mit der Adresse, die Du eintragen musst.

address already in use beim Start heißt, dass schon ein anderes Programm Port 80 oder 443 belegt. Häufig ist es Pi-hole 6 auf demselben Rechner, dessen Weboberfläche diese Ports nutzt. Leg sie mit sudo pihole-FTL --config webserver.port '8080o,[::]:8080o' auf Port 8080 um und starte Pi-hole mit sudo systemctl restart pihole-FTL neu.

Der Name wird nicht aufgelöst. Prüf mit nslookup ha.home.deinedomain.de auf Deinem Rechner, ob 192.168.178.10 zurückkommt. Kommt nichts, nutzt das Gerät einen anderen DNS-Server als Pi-hole oder die FRITZ!Box, oder der Rebind-Schutz greift noch.

Und von unterwegs?

Dieser Aufbau ist bewusst nur im Heimnetz erreichbar, und das ist für Home Assistant und Vaultwarden die sichere Grundeinstellung. Für den Zugriff von unterwegs öffnest Du deshalb keine Ports, sondern nimmst ein VPN. Für Home Assistant zeigt Home Assistant von unterwegs: Tailscale oder Cloudflare die einfachsten Wege. Wenn Du mehrere Dienste mit einem gemeinsamen Login schützen willst, ist Authentik einrichten: ein Login für alle eigenen Dienste der nächste Schritt, und Caddy hast Du dafür schon.

Für ein Update auf eine neue Caddy-Version änderst Du die Nummer in beiden Zeilen des Dockerfiles und im Image-Namen der compose.yaml und baust mit docker compose up -d --build neu. Zertifikate und Konto bleiben im Volume caddy_data erhalten.

Weiterlesen.