Deployn

Caddy Reverse Proxy mit Docker: Caddyfile, Load Balancing und Fail2ban

Caddy als Reverse Proxy für Docker einrichten: automatisches HTTPS, Caddyfile-Syntax, Load Balancing mit Health Checks, Header, IP-Filter, Basic Auth, Fehlerseiten und Fail2ban.

Veröffentlicht am Aktualisiert am
Caddy Reverse Proxy mit Docker: Caddyfile, Load Balancing und Fail2ban hero image
Inhalt
Getestet mit
Caddy 2.11Docker 29.9Docker Compose 5.7Debian 13
Zuletzt geprüft am

Du hast einen Server mit 20 Docker-Containern, die alle aus dem Internet erreichbar sein sollen. Das Problem ist klassisch: Du hast nur eine öffentliche IP-Adresse, und die Ports 80 und 443 kannst du nur ein einziges Mal belegen. Wie bekommst du Immich, Paperless, Nextcloud, Home Assistant und deine Webseite sicher unter eine Adresse? Die Antwort ist ein Reverse Proxy.

Der bekannteste Reverse Proxy ist vermutlich der Nginx Proxy Manager. In meinen Anleitungen nutze ich aber oft die moderne Alternative Caddy. Caddy holt HTTPS-Zertifikate bei Let’s Encrypt von allein, erneuert sie automatisch, und eine komplette Konfiguration für eine Webseite besteht aus drei Zeilen. In dieser Anleitung richte ich Caddy auf einem Server ein und gehe die Syntax der Konfigurationsdatei (Caddyfile) Schritt für Schritt durch: Load Balancing, Health Checks, Header, IP-Filter, Passwortschutz, Weiterleitungen, Fehlerseiten und Fail2ban.

Das lernst du in diesem Beitrag

  • Wie ein Reverse Proxy arbeitet und wie sich Caddy von Nginx Proxy Manager, Traefik und Zoraxy unterscheidet
  • Server und Domain vorbereiten (DNS mit Wildcard-Eintrag)
  • Caddy mit Docker Compose starten, mit gemeinsamem Docker-Netzwerk und praktischen Aliasen
  • Die Caddyfile-Syntax: Reverse Proxy, Load Balancing, Health Checks, Header, Unterverzeichnisse, Redirects, Snippets, Logging
  • Zugriff beschränken: IP-Filter, Basic Auth, Fail2ban und Rate Limiting
  • Eigene Fehlerseiten und eine Webseite mit API-Weiterleitung aus einer Datei
  • Optional: Wildcard-Zertifikat per DNS-Challenge und HTTP/3

Was du brauchst

  • Einen Server mit Docker. Zu Hause funktioniert das auch, dann musst du in deinem Router die Ports 80 und 443 an den Server weiterleiten. Hast du einen DS-Lite-Anschluss (keine eigene IPv4-Adresse), sind Weiterleitungen nicht möglich. Dann schau dir den Beitrag Reverse Proxy mit Pangolin an.
  • Eine Domain, die auf den Server zeigt, und die Möglichkeit, DNS-Einträge zu setzen.

Caddy braucht kaum Ressourcen. Im Test belegte der Container nach mehreren Dutzend Anfragen etwa 26 MB Arbeitsspeicher. Du brauchst einen größeren Server nur, wenn du viele weitere Anwendungen daneben starten möchtest.

Caddy, Nginx Proxy Manager, Traefik oder Zoraxy?

Mehrere Reverse Proxys sind gut. Meine Einordnung:

Proxy Stärke Schwäche
Nginx Proxy Manager Weboberfläche, sehr einsteigerfreundlich Jede Änderung klickst du von Hand, die Konfiguration steckt in einer Datenbank
Caddy Sehr kurze Textkonfiguration, HTTPS automatisch, HTTP/3 eingebaut Keine Oberfläche, Container trägst du selbst in die Datei ein
Traefik Findet Docker-Container selbst und liest Labels Mehr Begriffe, steilere Lernkurve
Zoraxy Weboberfläche, viele Zusatzfunktionen Jüngeres Projekt

Mit Caddy bekommst du eine einzige Textdatei, die du in Git sichern und lesen kannst. Das gefällt mir. Wenn du später das Gefühl hast, dass dir das manuelle Eintragen zu mühsam wird, gibt es noch das Plugin caddy-docker-proxy. Es liest wie Traefik Labels von Containern.

Die Konfiguration kannst du auch per Formular erzeugen: Der Caddyfile-Generator baut dir die Datei zusammen.

Server erstellen

Ich nutze wieder einen VPS von Hetzner. Weitere Hinweise zur Serverwahl stehen im VPS-Leitfaden.

In der Cloud Console klickst du auf „Server erstellen“ und wählst:

  • Standort: zum Beispiel Falkenstein
  • Image: Debian 13 (Ubuntu 24.04 geht genauso)
  • Typ: ein kleiner Server, etwa der CX23 mit 2 vCPUs und 4 GB RAM. Für Caddy allein reicht noch weniger.
  • Netzwerk: eine öffentliche IPv4-Adresse

Bei der Cloud-Config nutze ich den Hetzner-Cloud-Init-Generator. Dort stelle ich ein: Hauptbenutzer deployn, mein SSH Public Key, Docker installieren, Root-Login verbieten und nur Anmeldung per SSH-Schlüssel erlauben. Wie du einen SSH-Schlüssel erzeugst, steht im VPS-Leitfaden. Dann klickst du auf „Kopieren“, fügst den Text in das Feld „Cloud-Config“ ein und erstellst den Server. Der Server startet nach der Einrichtung einmal neu. Das dauert zwei bis drei Minuten.

Wenn der Server läuft, kannst du dich mit deinem Benutzer verbinden und prüfen, ob Docker da ist:

Terminalbash
ssh {{username}}@{{server_ip}}
docker --version
docker compose version
Docker version 29.9.3, build 5457199
Docker Compose version v5.7.0

Den Root-Login habe ich geprüft: ssh root@... endet mit Permission denied (publickey).

Statt im Terminal kannst du auch Visual Studio Code verwenden. Mit der Erweiterung „Remote - SSH“ klickst du links unten auf das grüne Symbol, wählst „Connect current window to Host“ und gibst deployn@SERVER-IP ein. Dann öffnest du über „Open Folder“ dein Heimverzeichnis /home/deployn. Das hat den Vorteil, dass du Dateien in einem Editor bearbeitest und trotzdem ein Terminal auf dem Server hast. Wie das genau geht, erkläre ich im Beitrag VS Code mit Server über SSH verbinden.

Domain auf den Server zeigen lassen

Damit Caddy Zertifikate bekommt, muss deine Domain auf die IP-Adresse des Servers zeigen. Ich setze zwei A-Einträge bei meinem Anbieter (bei mir Netcup):

Typ Host Wert
A @ SERVER-IP
A * SERVER-IP

Der Eintrag mit @ ist die Domain selbst (domain.de). Das Sternchen ist ein Wildcard: Jede Subdomain (app1.domain.de, blog.domain.de, irgendwas.domain.de) zeigt damit auf den Server. Dadurch musst du für neue Dienste nie wieder einen DNS-Eintrag anlegen. Hat dein Server eine IPv6-Adresse, legst du die gleichen Einträge als AAAA an.

Danach dauert es, bis die Änderung überall bekannt ist. Offiziell sind es bis zu 48 Stunden, meist geht es in Minuten bis wenigen Stunden. Du kannst auf dnschecker.org nachsehen, welche IP die DNS-Server weltweit liefern, oder selbst im Terminal prüfen:

Terminalbash
dig +short app1.{{domain}}
203.0.113.10

Steht dort die IP-Adresse deines Servers, kann es weitergehen. Mit ping app1.domain.de siehst du dieselbe Adresse. Solange du noch die alte siehst, ist die Änderung bei deinem DNS-Server noch nicht angekommen. Dann kann Let’s Encrypt kein Zertifikat ausstellen, deshalb lohnt sich das Warten. Zu viele fehlgeschlagene Versuche kann Let’s Encrypt zeitweise sperren.

Caddy mit Docker Compose starten

Auf dem Server legen wir ein Verzeichnis für Caddy an. Darin kommen die Compose-Datei, die Konfigurationsdatei Caddyfile und zwei Ordner für Daten:

Terminalbash
mkdir -p ~/caddy/data ~/caddy/config
cd ~/caddy
touch compose.yaml Caddyfile

Öffne compose.yaml und trage ein:

~/caddy/compose.yamlyaml
services:
  caddy:
    image: caddy:2.11
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./data:/data
      - ./config:/config
    networks:
      - proxy

networks:
  proxy:
    external: true
  • image: caddy:2.11: Das offizielle Image. Ich nenne die Version ausdrücklich. Mit 2.11 bekommst du alle Patch-Updates der Reihe 2.11 (derzeit 2.11.8), aber keine unerwartete neue Hauptversion. Von latest rate ich ab, weil ein Update dir sonst unbemerkt etwas ändern kann.
  • container_name: Der Name, unter dem wir den Container später ansprechen.
  • restart: unless-stopped: Stürzt der Container ab oder startet der Server neu, startet Caddy von allein wieder.
  • ports: Port 80 (HTTP) und 443 (HTTPS) vom Server gehen an Caddy. Die dritte Zeile leitet 443 zusätzlich per UDP weiter. Das braucht Caddy für HTTP/3. Lässt du die Zeile weg, funktioniert alles weiter, nur ohne HTTP/3.
  • volumes: Die Caddyfile ist unsere Konfiguration. Ohne sie sieht Caddy keine Einstellungen. Der Ordner data ist wichtig: Dort speichert Caddy Zertifikate und Schlüssel. Wenn er verloren geht, muss Caddy alle Zertifikate neu anfordern, und dabei kannst du an die Limits von Let’s Encrypt stoßen. Im Ordner config landet die automatisch gespeicherte Konfiguration.
  • networks: Das Netzwerk proxy, das wir gleich anlegen. Warum es nötig ist, siehst du in einem Moment.

Das Docker-Netzwerk anlegen

Docker isoliert Container voneinander. Container, die nicht im selben Netzwerk hängen, können sich nicht sehen. Wir legen daher ein eigenes Netzwerk an, in das Caddy und alle Dienste kommen, die Caddy veröffentlichen soll:

Terminalbash
docker network create proxy

Der Name proxy ist frei gewählt. In der Compose-Datei steht external: true, weil das Netzwerk nicht zu diesem Projekt gehört, sondern schon existiert. So können später beliebig viele Compose-Projekte hineinkommen.

Caddyfile und erster Start

Die Datei Caddyfile ist noch leer. Wir tragen zunächst nichts ein, sondern starten den Container nur zum Test:

Terminalbash
cd ~/caddy
docker compose up -d
docker compose ps
NAME      IMAGE        COMMAND                  SERVICE   CREATED        STATUS        PORTS
caddy     caddy:2.11   "caddy run --config …"   caddy     1 second ago   Up 1 second   0.0.0.0:80->80/tcp, [::]:80->80/tcp, 0.0.0.0:443->443/tcp, [::]:443->443/tcp, 0.0.0.0:443->443/udp, [::]:443->443/udp, 2019/tcp

up -d startet den Container im Hintergrund (-d für „detached“). Caddy läuft, hat aber noch nichts zu tun.

Aliase für weniger Tipparbeit

Mit der Compose-Datei in ~/caddy müsstest du bei jedem Befehl erst in den Ordner wechseln. Mit der Option -f gibst du den Pfad der Compose-Datei an und kannst den Befehl von überall ausführen. Damit du den langen Befehl nicht ständig tippen musst, lege ich Aliase an. Das sind Abkürzungen für die Shell. Hänge diese Zeilen an ~/.bashrc an (zum Beispiel mit nano ~/.bashrc):

~/.bashrcbash
alias caddy-up="docker compose -f $HOME/caddy/compose.yaml up -d"
alias caddy-down="docker compose -f $HOME/caddy/compose.yaml down"
alias caddy-logs="docker compose -f $HOME/caddy/compose.yaml logs -f"
alias caddy-reload="docker compose -f $HOME/caddy/compose.yaml exec caddy caddy reload --config /etc/caddy/Caddyfile"

Dann lädst du die Datei neu: source ~/.bashrc. Ab jetzt starten, stoppen und beobachten diese Befehle Caddy von jedem Ordner aus:

Alias Wirkung
caddy-up Container starten
caddy-down Container stoppen und entfernen
caddy-logs Logs anzeigen und mitverfolgen (mit Strg + C beenden)
caddy-reload Caddyfile neu laden, ohne den Container neu zu starten

caddy-reload ist der wichtigste: Caddy wendet die neue Konfiguration an, ohne dass Verbindungen abbrechen. Ändert sich dagegen die compose.yaml (zum Beispiel ein neues Volume), brauchst du caddy-down und caddy-up.

Eine Test-App einrichten

Um den Reverse Proxy auszuprobieren, brauchen wir einen Dienst. Ich nehme traefik/whoami: ein winziger Webserver, der anzeigt, welche Anfrage bei ihm angekommen ist. Er eignet sich perfekt, um Routing, Header und Load Balancing zu sehen. Wir starten gleich zwei Instanzen, app1 und app2.

Terminalbash
mkdir ~/app
cd ~/app
nano compose.yaml
~/app/compose.yamlyaml
services:
  app1:
    image: traefik/whoami:v1.12
    container_name: app1
    restart: unless-stopped
    networks:
      - proxy

  app2:
    image: traefik/whoami:v1.12
    container_name: app2
    restart: unless-stopped
    networks:
      - proxy

networks:
  proxy:
    external: true

Wichtig sind die container_name-Einträge und das Netzwerk proxy. Caddy erreicht die Container im Netzwerk über ihren Namen (app1, app2). Starte sie:

Terminalbash
docker compose up -d

Die Container haben keinen ports-Eintrag. Sie sind von außen nicht erreichbar, sondern nur über Caddy. Das ist gewollt: So kommt niemand an Caddy vorbei.

Was passiert, wenn das Netzwerk fehlt?

Das ist der häufigste Fehler. Caddy antwortet dann mit 502 Bad Gateway, und im Log steht:

dial tcp: lookup app1 on 127.0.0.11:53: no such host

Caddy findet den Namen app1 nicht, weil der Container in einem anderen Netzwerk hängt. Du musst das Netzwerk deshalb an zwei Stellen eintragen: Im Block networks: ganz unten (hier external: true) und beim Dienst selbst unter services → networks. Fehlt eines davon, kommt keine Verbindung zustande. Nach einer Änderung der Netzwerke musst du die Container neu erstellen.

Die Caddyfile: Dein erster Reverse Proxy

Die Caddyfile besteht aus Seiten-Blöcken. Ein Block beginnt mit der Domain und enthält in geschweiften Klammern, was dort passieren soll. Öffne ~/caddy/Caddyfile:

~/caddy/Caddyfilecaddy
app1.{{domain}} {
	reverse_proxy app1:80
}

Das war es schon. Die Zeile reverse_proxy app1:80 sagt: Alle Anfragen an app1.domain.de gehen an den Container app1 auf Port 80. Die Einrückung machst du mit Tabs. Dann lädst du die Konfiguration neu:

Terminalbash
caddy-reload

Rufst du jetzt https://app1.domain.de auf, siehst du die Ausgabe von whoami. Im Terminal:

Terminalbash
curl https://app1.{{domain}}/abc
Hostname: 23a36a0a1600
IP: 127.0.0.1
IP: ::1
IP: 172.18.0.4
RemoteAddr: 172.18.0.2:40794
GET /abc HTTP/1.1
Host: app1.domain.de
User-Agent: curl/8.14.1
Accept: */*
Accept-Encoding: gzip
Via: 2.0 Caddy
X-Forwarded-For: 203.0.113.10
X-Forwarded-Host: app1.domain.de
X-Forwarded-Proto: https

Was du hier siehst:

  • Die Anfrage /abc kam beim Container an.
  • Caddy hat die Header X-Forwarded-For (deine IP), X-Forwarded-Host (die ursprüngliche Domain) und X-Forwarded-Proto (https) gesetzt. Damit weiß die Anwendung, wer sie wirklich aufgerufen hat. Das brauchen viele Programme für Logs und Weiterleitungen.
  • Im Browser siehst du das Schloss: eine HTTPS-Verbindung mit einem Zertifikat von Let’s Encrypt. Das hat Caddy beim ersten Aufruf selbst besorgt. Im Log (caddy-logs) steht:
{"level":"info","logger":"tls.obtain","msg":"certificate obtained successfully","identifier":"app1.domain.de","issuer":"acme-v02.api.letsencrypt.org-directory"}

Einfacher kann ein Reverse Proxy mit HTTPS kaum werden. Eine Weiterleitung von HTTP auf HTTPS legt Caddy automatisch an. Du musst dazu nichts eintragen.

Der globale Block

Am Anfang der Datei kann ein Block ohne Domain stehen. Dort stehen Einstellungen für ganz Caddy:

Caddyfilecaddy
{
	email admin@domain.de
	# debug
}

Eine E-Mail-Adresse war früher sinnvoll, damit Let’s Encrypt dich über ablaufende Zertifikate informiert. Das macht Let’s Encrypt seit 2025 nicht mehr, die Adresse ist also optional. Mit debug läuft Caddy im Debug-Modus und protokolliert mehr, was bei der Fehlersuche hilft. Im Test habe ich beides weggelassen, die Zeilen dienen nur zur Orientierung.

Format prüfen und Fehler finden

Beim Laden warnt Caddy dich, wenn die Datei nicht sauber formatiert ist:

Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies

Das ist nur eine Warnung, die Konfiguration gilt trotzdem. Ich korrigiere die Formatierung mit dem eingebauten Formatierer. Das geht nur, wenn die Datei nicht :ro eingebunden ist:

Terminalbash
docker exec caddy caddy fmt --overwrite /etc/caddy/Caddyfile

Bevor du neu lädst, kannst du eine Datei außerdem prüfen, ohne dass sie angewendet wird:

Terminalbash
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
Valid configuration

Bei einem Fehler nennt Caddy die Zeile. Ein Reload mit fehlerhafter Datei bricht ab, und Caddy läuft mit der alten Konfiguration weiter. Dir kann also nichts kaputtgehen.

Load Balancing

Stell dir vor, du betreibst eine Anwendung zweimal, zum Beispiel für mehr Leistung oder Ausfallsicherheit. Caddy verteilt dann die Anfragen. Dazu nennst du einfach mehrere Ziele hinter reverse_proxy:

Caddyfilecaddy
lb.{{domain}} {
	reverse_proxy app1:80 app2:80
}

Nach dem Reload rufe ich die Adresse achtmal auf. So verteilen sich die Anfragen (nach Hostname des Containers gezählt):

      3 Hostname: 23a36a0a1600
      5 Hostname: ddd7674b5f61

Beide Container bekommen Anfragen, aber unregelmäßig. Das ist die Standardeinstellung: random, also ein zufälliges Ziel pro Anfrage.

Die Strategie wählen

Wie Caddy verteilt, bestimmt die lb_policy. Die Liste der Möglichkeiten steht in der Caddy-Dokumentation. Die wichtigsten:

lb_policy Verhalten
random Zufälliges Ziel (Standard)
round_robin Reihum, jedes Ziel kommt abwechselnd dran
least_conn Das Ziel mit den wenigsten offenen Verbindungen
first Immer das erste erreichbare Ziel, die anderen sind Reserve
ip_hash Dieselbe Client-IP landet immer beim selben Ziel
cookie Ein Cookie merkt sich das Ziel (Sticky Sessions)

Mit round_robin:

Caddyfilecaddy
lb.{{domain}} {
	reverse_proxy app1:80 app2:80 {
		lb_policy round_robin
	}
}

Jetzt wechselt das Ziel bei jeder Anfrage:

      1 Hostname: ddd7674b5f61
      1 Hostname: 23a36a0a1600
      1 Hostname: ddd7674b5f61
      1 Hostname: 23a36a0a1600

Wenn ein Ziel ausfällt: Health Checks

Was passiert, wenn app1 ausfällt? Ich stoppe den Container mit docker stop app1 und rufe die Seite sechsmal auf:

200 502 200 502 200 502

Jede zweite Anfrage scheitert mit 502 Bad Gateway, weil Caddy weiterhin an das tote Ziel schickt. Das ist ärgerlich. Die Lösung sind Health Checks: Caddy fragt regelmäßig bei jedem Ziel nach, ob es antwortet, und schickt nur noch Anfragen an funktionierende Ziele.

Caddyfilecaddy
lb.{{domain}} {
	reverse_proxy app1:80 app2:80 {
		lb_policy round_robin
		health_uri /
		health_interval 5s
		health_timeout 2s
	}
}
  • health_uri: Der Pfad, den Caddy abfragt. Er sollte eine Seite sein, die bei einem gesunden Dienst 200 liefert.
  • health_interval: Wie oft Caddy nachfragt (hier alle 5 Sekunden).
  • health_timeout: Wie lange Caddy auf eine Antwort wartet, bevor das Ziel als ausgefallen gilt.

Nach dem Reload, mit gestopptem app1:

200 200 200 200 200 200

Alle Anfragen gehen jetzt an app2. Starte ich app1 wieder, nimmt Caddy es nach spätestens fünf Sekunden zurück in die Verteilung auf.

Ziele auf anderen Servern

Die Ziele müssen nicht Container im selben Netzwerk sein. Du kannst auch eine Domain oder eine IP-Adresse eines anderen Servers eintragen. Ich habe das mit einem zweiten Server getestet, auf dem ein whoami-Container auf Port 8080 läuft:

Caddyfilecaddy
remote.{{domain}} {
	reverse_proxy app1:80 203.0.113.20:8080 {
		lb_policy round_robin
		health_uri /
		health_interval 5s
		health_timeout 2s
	}
}

Ergebnis bei sechs Anfragen: drei beim lokalen app1 und drei beim Container auf dem anderen Server. Habe ich den anderen Server gestoppt, gingen alle sechs Anfragen mit 200 an app1. So senkst du das Ausfallrisiko deutlich, zum Beispiel für zwei Server an verschiedenen Standorten. Beachte: Der Port des Zielservers ist dafür offen. Beschränke ihn mit der Firewall auf die IP-Adresse deines Caddy-Servers.

Sticky Sessions

Manche Anwendungen merken sich den Benutzer im Speicher des Servers. Dann soll ein Besucher immer beim selben Ziel landen. Dafür gibt es lb_policy cookie:

Caddyfilecaddy
sticky.{{domain}} {
	reverse_proxy app1:80 app2:80 {
		lb_policy cookie
	}
}

Caddy setzt dabei ein Cookie:

set-cookie: lb=937544e36d856811e8…; Path=/; HttpOnly; Secure; SameSite=None

Ein Client, der das Cookie mitschickt (curl -b), landet bei vier Anfragen viermal beim selben Container. Ohne Cookie wählt Caddy neu.

Header ändern

Du kannst Header an die Anwendung weitergeben. Mit header_up änderst du Header, die zum Ziel gehen, mit header_down Header der Antwort, die zum Besucher zurückgehen:

Caddyfilecaddy
secret.{{domain}} {
	reverse_proxy app1:80 {
		header_up X-Secret-Token "mein geheimes Token"
	}
}

Im whoami-Output taucht der Header jetzt auf:

X-Secret-Token: mein geheimes Token

Praktisch ist das zum Beispiel für Anwendungen, die einen festen Header erwarten, oder um Header zu entfernen: Mit header_up -Cookie bekommt das Ziel keine Cookies.

Sicherheits-Header setzen

Mit dem Befehl header (ohne _up) setzt du Header auf der Antwort. Das ist der einfachste Weg, ein paar Sicherheits-Header zu aktivieren. Ein vorangestelltes Minuszeichen entfernt einen Header:

Caddyfilecaddy
sicher.{{domain}} {
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		X-Frame-Options "SAMEORIGIN"
		Referrer-Policy "same-origin"
		-Server
	}
	reverse_proxy app1:80
}

Mit curl -I https://sicher.domain.de prüfst du das Ergebnis. Die vier Header sind da, und der Server-Header fehlt:

referrer-policy: same-origin
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN

Zusätzlich komprimiert encode zstd gzip die Antworten und spart Bandbreite. Du trägst es einfach in den Seiten-Block ein.

Unterverzeichnisse statt Subdomains

Statt app1.domain.de und app2.domain.de kannst du auch Pfade wie tools.domain.de/app1/ nutzen. Dafür gibt es handle_path:

Caddyfilecaddy
tools.{{domain}} {
	handle_path /app1/* {
		reverse_proxy app1:80
	}
	handle_path /app2/* {
		reverse_proxy app2:80
	}
}

handle_path ist hier wichtig und nicht reverse_proxy allein, weil es den Pfad abschneidet: Ruft jemand tools.domain.de/app1/abc auf, bekommt der Container GET /abc. Das Präfix /app1 kennt der Container ja gar nicht. Die Ausgabe von whoami beweist es:

GET /abc HTTP/1.1

Zwei Stolperfallen:

  • Der Pfad /app1/* passt nur mit abschließendem Schrägstrich. Ruft jemand tools.domain.de/app1 ohne / auf, trifft kein Block, und Caddy antwortet mit einer leeren Seite (200 ohne Inhalt). Das gilt auch für die Hauptadresse tools.domain.de/.
  • Nicht jede Anwendung funktioniert unter einem Unterverzeichnis, weil sie Links als /css/style.css ausgibt. Subdomains sind darum meist zuverlässiger.

Externe Seiten weiterleiten

Ein Reverse Proxy kann auch Seiten außerhalb deines Servers ausliefern. Dabei gibt es eine Besonderheit beim Host-Header. Ein Beispiel mit example.com, das nur per HTTP (ohne HTTPS) angesprochen wird:

Caddyfilecaddy
plain.{{domain}} {
	reverse_proxy http://example.com
}

Das scheitert. Die Anfrage kommt mit dem Host plain.domain.de beim Ziel an, und das Ziel weiß nicht, was es damit tun soll. In meinem Test antwortete der Dienst mit Status 409 und einer Fehlerseite (“DNS resolution error”). Du musst Caddy sagen, dass es sich als Ziel ausgibt:

Caddyfilecaddy
plain.{{domain}} {
	reverse_proxy http://example.com {
		header_up Host {upstream_hostport}
	}
}

{upstream_hostport} ist ein Platzhalter, den Caddy mit Host und Port des Ziels füllt. Jetzt liefert der Aufruf die Seite von example.com mit Status 200. Bei einem Ziel mit https:// ist das nicht mehr nötig: Caddy setzt den Host dann selbst auf das Ziel. Ich habe die Zeile dort getestet, Caddy meldet sie als „Unnecessary header_up Host“.

Zugriff auf IP-Adressen beschränken

Eine Verwaltungsoberfläche soll nicht jeder sehen. Mit einem Matcher wählst du Anfragen aus. Ein Matcher beginnt mit @ und einem frei wählbaren Namen. Hier einer für bestimmte IP-Adressen:

Caddyfilecaddy
admin.{{domain}} {
	@internal remote_ip 192.168.0.0/16 10.0.0.0/8 203.0.113.10
	handle @internal {
		reverse_proxy app1:80
	}
	respond "Zugriff verweigert" 403
}
  • @internal remote_ip ... definiert den Matcher @internal: Er trifft auf Anfragen zu, die von diesen Adressen kommen. 192.168.0.0/16 und 10.0.0.0/8 sind private Netze, die letzte Adresse ist ein einzelner Rechner (ersetze sie durch deine eigene, die du zum Beispiel über eine Suchmaschine erfährst).
  • handle @internal { ... } führt den Reverse Proxy nur für diese Anfragen aus.
  • respond gilt für alle anderen: Sie bekommen den Text „Zugriff verweigert“ mit dem Status 403.

Ich habe das mit meinem Server (erlaubt) und meinem Rechner zu Hause (nicht in der Liste) geprüft. Vom erlaubten Rechner kommt 200 mit der Seite, vom anderen 403 mit „Zugriff verweigert“. Im Browser siehst du das auch in den Entwicklerwerkzeugen unter „Netzwerk“.

Statt die Netze selbst zu schreiben, kennt Caddy die Abkürzung private_ranges für alle privaten Adressbereiche. Das ist praktisch, wenn ein Dienst nur aus deinem lokalen Netz erreichbar sein soll:

Caddyfilecaddy
intern.{{domain}} {
	@blocked not remote_ip private_ranges
	respond @blocked 403
	reverse_proxy app1:80
}

Passwortschutz mit Basic Auth

Wenn du keine feste IP-Adresse hast, schützt du eine Seite mit Benutzername und Passwort. Caddy speichert das Passwort als Hash. Du erzeugst ihn mit dem Befehl im Container:

Terminalbash
docker exec caddy caddy hash-password --plaintext 'DeinStarkesPasswort'
$2a$14$FTDGpmbYqg5.K…

Den ausgegebenen Hash kopierst du in die Caddyfile:

Caddyfilecaddy
auth.{{domain}} {
	basic_auth {
		admin $2a$14$FTDGpmbYqg5.K…
	}
	reverse_proxy app1:80
}

Ohne Zugangsdaten antwortet die Seite mit 401 Unauthorized, mit admin und dem richtigen Passwort mit 200. Das Passwort steht nirgends im Klartext, aber der Befehl landet in deinem Terminalverlauf. Setzt du vor den Befehl ein Leerzeichen, speichert die Bash ihn normalerweise nicht.

Weiterleitungen und Umschreibungen

Von www auf die Hauptdomain

Wer eine Webseite betreibt, sollte sich für www.domain.de oder domain.de entscheiden und die andere Variante weiterleiten. Das ist auch für Suchmaschinen wichtig, damit sie nicht zwei Versionen derselben Seite sehen:

Caddyfilecaddy
www.{{domain}} {
	redir https://{{domain}}{uri} permanent
}

Das {uri} am Ende ist wichtig: Es hängt Pfad und Parameter der ursprünglichen Anfrage an. Dann landet www.domain.de/hallo bei domain.de/hallo. Ohne {uri} kämen alle Besucher auf der Startseite an.

Das Wort permanent ist ebenfalls wichtig. Ohne diese Angabe sendet Caddy den Statuscode 302 (vorübergehend). Mit permanent ist es ein 301 (dauerhaft), und Suchmaschinen übernehmen die neue Adresse:

HTTP/2 301
location: https://domain.de/hallo

Pfade umschreiben

Mit rewrite änderst du den Pfad, ohne dass der Besucher es merkt. Das ist praktisch, wenn du eine alte Anwendung migrierst und alte Adressen weiter funktionieren sollen. Alle Anfragen an /alt gehen dann an /neu:

Caddyfilecaddy
app1.{{domain}} {
	rewrite /alt /neu
	reverse_proxy app1:80
}

Die Adresse im Browser bleibt /alt, aber der Container sieht GET /neu. Ein redir dagegen schickt den Browser zu einer neuen Adresse, und der Besucher sieht sie in der Adresszeile.

Snippets: Wiederverwendbare Bausteine

Hast du mehrere Seiten mit gleichen Einstellungen, schreibst du sie einmal als Snippet. Der Name steht in runden Klammern. Eingebunden wird das Snippet mit import:

Caddyfilecaddy
(alt-neu) {
	rewrite /alt /neu
}

app1.{{domain}} {
	import alt-neu
	reverse_proxy app1:80
}

app2.{{domain}} {
	import alt-neu
	reverse_proxy app2:80
}

Bei einem Einzeiler lohnt sich das nicht. Aber bei einem Block aus Sicherheits-Headern, Kompression und Logging, den du bei zehn Diensten brauchst, sparst du viel und änderst ihn später an einer Stelle.

Logging

Caddy kann jede Anfrage protokollieren. Mit log in einem Seiten-Block schreibst du das Protokoll in eine Datei. Ich nutze das JSON-Format, weil Werkzeuge wie Fail2ban es leicht auswerten:

Caddyfilecaddy
(log-json) {
	log {
		output file /data/access.log
		format json
	}
}

app1.{{domain}} {
	import log-json
	reverse_proxy app1:80
}

Die Datei liegt im Ordner data, den wir in den Container eingebunden haben, also auf dem Server unter ~/caddy/data/access.log. Sie gehört dem Benutzer root, du kannst sie also nur mit sudo lesen:

Terminalbash
sudo tail -n 1 ~/caddy/data/access.log
{"level":"info","ts":1793880253.2,"logger":"http.log.access.log0","msg":"handled request","request":{"remote_ip":"203.0.113.10","client_ip":"203.0.113.10","proto":"HTTP/2.0","method":"GET","host":"app1.domain.de","uri":"/neu", …},"status":200, …}

Jede Anfrage ist eine Zeile mit Zeit (ts), Besucher-IP (client_ip), Domain (host), Pfad (uri) und Statuscode (status). Du siehst im Beispiel auch, dass uri schon der umgeschriebene Pfad ist. Weitere Optionen (Rotation, Filter) beschreibt die Dokumentation zu log. Caddy rotiert Logdateien in diesem Modus standardmäßig ab 100 MB und behält 10 Dateien.

Fail2ban: Angreifer automatisch sperren

Mit den Logs kannst du Angreifer sperren. Fail2ban liest das Protokoll, erkennt wiederholte Fehlversuche (zum Beispiel falsche Passwörter bei Basic Auth, also 401) und sperrt die IP-Adresse für eine Zeit in der Firewall. Auf einem Server, der im Internet hängt, ist das nützlich. Ich habe im Test gesehen, dass nach wenigen Minuten schon fremde Scanner in der Sperrliste standen.

Installiere Fail2ban auf dem Server (nicht im Container):

Terminalbash
sudo apt install -y fail2ban

Der Filter sagt, welche Zeilen als Fehlversuch zählen. Er sucht in der JSON-Zeile nach der Client-IP und einem Status 401 oder 403. Lege /etc/fail2ban/filter.d/caddy-status.conf an:

/etc/fail2ban/filter.d/caddy-status.confini
[Definition]
failregex = ^.*"client_ip":"<HOST>".*"status":(?:401|403).*$
datepattern = "ts":{EPOCH}

Dann der Jail, der Filter, Logdatei und Sperrdauer verbindet. Datei /etc/fail2ban/jail.d/caddy.local:

/etc/fail2ban/jail.d/caddy.localini
[caddy-status]
enabled = true
port = http,https
filter = caddy-status
logpath = /home/{{username}}/caddy/data/access.log
maxretry = 5
findtime = 10m
bantime = 1h
banaction = iptables-multiport
chain = DOCKER-USER
  • maxretry, findtime, bantime: Wer in 10 Minuten 5 Fehlversuche macht, wird für eine Stunde gesperrt.
  • chain = DOCKER-USER ist wichtig. Die Standardregeln von Fail2ban greifen im Filter INPUT. Anfragen an Docker-Container laufen aber an dieser Kette vorbei. Eine Sperre in INPUT würde deshalb bei Caddy nichts bewirken. Die Kette DOCKER-USER wird für Docker-Verkehr vor Dockers eigenen Regeln durchlaufen, dort greift die Sperre.
  • Hinter einem Proxy: Fail2ban sperrt in der Firewall. Steht vor Caddy ein weiterer Proxy (zum Beispiel Cloudflare), sieht die Firewall nur dessen Adresse. Enthält das Log die Adresse des Proxys, sperrst du alle Besucher. Enthält es die echte Besucher-IP, läuft die Sperre ins Leere.

Dann Fail2ban neu starten und prüfen:

Terminalbash
sudo systemctl restart fail2ban
sudo fail2ban-client status caddy-status

Mit sudo fail2ban-regex ~/caddy/data/access.log /etc/fail2ban/filter.d/caddy-status.conf prüfst du, ob dein Filter die Zeilen erkennt. Bei mir passte der Filter auf 42 von 52 Zeilen, die anderen waren Zeilen mit anderen Statuscodes.

Wie das aussieht, wenn es greift: Ich habe von meinem Rechner siebenmal falsche Zugangsdaten an auth.domain.de geschickt. Die ersten sechs Anfragen bekamen 401, die siebte lief in ein Timeout, und meine Adresse stand in der Sperrliste:

Chain DOCKER-USER (1 references)
target            prot opt source     destination
f2b-caddy-status  tcp  --  0.0.0.0/0  0.0.0.0/0   multiport dports 80,443

Mit sudo fail2ban-client set caddy-status unbanip DEINE-IP hebst du eine Sperre wieder auf.

Rate Limiting: Anfragen pro IP begrenzen

Fail2ban reagiert auf Fehlversuche. Ein Bot, der eine Schnittstelle mit gültigen Anfragen flutet, fällt dort nicht auf. Dagegen hilft ein Rate Limit: Caddy lässt pro IP-Adresse nur eine bestimmte Zahl Anfragen in einem Zeitfenster durch und antwortet danach mit 429 Too Many Requests.

Caddy bringt kein Rate Limit mit. Du brauchst das Plugin caddy-ratelimit und damit ein eigenes Image, so wie beim Wildcard-Zertifikat weiter unten. Das Dockerfile in ~/caddy:

~/caddy/Dockerfiledockerfile
FROM caddy:2.11-builder AS builder
RUN xcaddy build --with github.com/mholt/caddy-ratelimit

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

In der compose.yaml baust du das Image jetzt selbst, statt das offizielle zu laden:

~/caddy/compose.yamlyaml
services:
  caddy:
    build: .
    image: caddy-ratelimit:2.11
    image: caddy:2.11

Dann bauen und starten. Das Kompilieren dauert einige Minuten:

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

Das Limit schreibst du in die Caddyfile. Als Beispiel begrenze ich POST-Anfragen unter /api/ auf drei pro Minute und IP-Adresse:

Caddyfilecaddy
api.{{domain}} {
	route {
		rate_limit {
			zone pro_ip {
				match {
					method POST
					path /api/*
				}
				key {remote_host}
				events 3
				window 1m
			}
		}
		reverse_proxy app1:80
	}
}
  • route: Plugin-Direktiven haben keine feste Position in der Reihenfolge von Caddy. Im route-Block läuft alles so, wie du es hinschreibst, das Limit also vor reverse_proxy.
  • zone pro_ip: Eine Zone ist ein benannter Zähler. Der Name ist frei wählbar.
  • match: Nur Anfragen, die zu diesem Matcher passen, werden gezählt. Lesende Zugriffe bleiben unbegrenzt.
  • key {remote_host}: Jede IP-Adresse bekommt einen eigenen Zähler. events 3 und window 1m erlauben drei Anfragen pro Minute.

Eine zweite Zone mit key static zählt alle Besucher zusammen. Das fängt Angriffe von vielen verschiedenen Adressen ab, trifft bei einem Angriff aber auch echte Besucher:

Caddyfilecaddy
		zone alle {
			match {
				method POST
				path /api/*
			}
			key static
			events 10
			window 1m
		}

Zum Testen schickst du sechs Anfragen hintereinander:

Terminalbash
for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w '%{http_code} ' -X POST https://api.{{domain}}/api/test; done; echo

Die ersten Anfragen erreichen die Anwendung, danach antwortet Caddy mit 429.

Eigene Fehlerseiten und statische Webseiten

Caddy kann nicht nur weiterleiten, sondern auch Dateien ausliefern. Das nutzen wir für eine Wartungsseite, die erscheint, wenn ein Dienst ausfällt, und für eine statische Webseite mit Weiterleitung an eine API.

Lege einen Ordner webseite im Caddy-Verzeichnis an und binde ihn in die compose.yaml ein:

~/caddy/compose.yamlyaml
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./data:/data
      - ./config:/config
      - ./webseite:/webseite

Danach brauchst du caddy-down und caddy-up, weil sich die Compose-Datei geändert hat. Erstelle die Seiten:

~/caddy/webseite/wartungsarbeiten.htmlhtml
<!doctype html>
<html lang="de">
	<head>
		<meta charset="utf-8" />
		<title>Wartungsarbeiten</title>
	</head>
	<body>
		<h1>Ein Fehler ist aufgetreten. Wir sind bald zurück.</h1>
	</body>
</html>
~/caddy/webseite/index.htmlhtml
<!doctype html>
<html lang="de">
	<head>
		<meta charset="utf-8" />
		<title>Hauptseite</title>
	</head>
	<body>
		<h1>Hauptseite</h1>
		<p>Das ist eine statische Webseite.</p>
	</body>
</html>

Fehlerseite bei Ausfall

Mit handle_errors bestimmst du, was bei einem Fehler passiert. Hier liefern wir statt der Fehlermeldung die Wartungsseite aus:

Caddyfilecaddy
error.{{domain}} {
	reverse_proxy app1:80
	handle_errors {
		rewrite * /wartungsarbeiten.html
		root * /webseite
		file_server
	}
}
  • rewrite * /wartungsarbeiten.html schreibt jeden Pfad auf die Wartungsseite um.
  • root * /webseite legt fest, in welchem Ordner Caddy nachsieht.
  • file_server aktiviert die Auslieferung statischer Dateien.

Läuft app1, siehst du die Anwendung. Stoppe ich den Container (docker stop app1), sehe ich stattdessen meine Seite mit „Ein Fehler ist aufgetreten“. Der Statuscode bleibt dabei 502, das ist für Suchmaschinen und Überwachung richtig.

Statische Seite und API kombinieren

Das Schöne an Caddy: Du kannst beides in einer Domain mischen. Alles unter /api/ geht an einen Container, alles andere liefert Caddy aus deinem Ordner webseite:

Caddyfilecaddy
hybrid.{{domain}} {
	handle /api/* {
		reverse_proxy app2:80
	}
	handle {
		root * /webseite
		try_files {path} /index.html
		file_server
	}
}
  • handle /api/* leitet Anfragen unter /api/ an app2 weiter (im Gegensatz zu handle_path bleibt das Präfix erhalten, die API sieht /api/test).
  • Der zweite handle-Block ohne Pfad gilt für alles andere.
  • try_files {path} /index.html sucht die angefragte Datei und liefert, falls sie fehlt, die index.html. Das ist das übliche Muster für Webseiten, die im Browser ihre Seiten selbst steuern (Single-Page-Apps).

Das Ergebnis: hybrid.domain.de/irgendwas zeigt die Hauptseite, und hybrid.domain.de/api/test landet bei app2 (GET /api/test). So hast du eine statische Seite im Frontend und kannst im Hintergrund weitere Dienste ansprechen.

Soll eine Domain direkt eine Webseite zeigen, reichen zwei Zeilen:

Caddyfilecaddy
{{domain}} {
	root * /webseite
	file_server
}

Wildcard-Zertifikat per DNS-Challenge (optional)

Bisher holt Caddy für jede Subdomain ein eigenes Zertifikat per HTTP-Challenge. Das reicht für fast alle. Ein Wildcard-Zertifikat (*.domain.de) brauchst du, wenn Subdomains nicht im öffentlichen Zertifikatsregister (Certificate Transparency) stehen sollen oder der Server nicht aus dem Internet erreichbar ist (zum Beispiel ein Homeserver). Dafür verlangt Let’s Encrypt die DNS-Challenge: Caddy legt über die API deines DNS-Anbieters einen Eintrag an. Das ist in der Standard-Version von Caddy nicht eingebaut. Du brauchst ein eigenes Image mit dem Plugin deines Anbieters. Das baust du mit xcaddy in einem Dockerfile. Beispiel für Netcup:

~/caddy/Dockerfiledockerfile
FROM caddy:2.11-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/netcup

FROM caddy:2.11
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
Terminalbash
docker build -t caddy-netcup:2.11 ~/caddy
docker run --rm caddy-netcup:2.11 caddy list-modules | grep dns.providers
dns.providers.netcup

Das Image lässt sich also bauen, und das Plugin ist enthalten. In der compose.yaml ersetzt du image: caddy:2.11 durch image: caddy-netcup:2.11 und reichst die Zugangsdaten als Umgebungsvariablen weiter. In der Caddyfile steht dann im globalen Block der Anbieter, und eine Adresse mit Sternchen gilt für alle Subdomains:

Caddyfilecaddy
{
	acme_dns netcup {
		customer_number {env.NETCUP_CUSTOMER_NUMBER}
		api_key {env.NETCUP_API_KEY}
		api_password {env.NETCUP_API_PASSWORD}
	}
}

*.{{domain}}, {{domain}} {
	@app1 host app1.{{domain}}
	handle @app1 {
		reverse_proxy app1:80
	}
	handle {
		abort
	}
}

Die Datei hat caddy validate als gültig bestätigt. Das Ausstellen des Zertifikats selbst habe ich nicht getestet, weil dafür echte Zugangsdaten zu meinem Netcup-Konto nötig wären. Für andere Anbieter (Cloudflare, Hetzner, IONOS und viele mehr) gibt es Plugins im Repository caddy-dns. Die Optionen stehen jeweils in deren Dokumentation. Teste mit dem Staging-Server von Let’s Encrypt (Option acme_ca https://acme-staging-v02.api.letsencrypt.org/directory im globalen Block), bevor du echte Zertifikate anforderst.

HTTP/3

Caddy spricht HTTP/3 ohne weitere Einstellung, wenn Port 443 per UDP erreichbar ist. Das haben wir in der Compose-Datei mit "443:443/udp" erledigt. Im Antwort-Header alt-svc: h3=":443" kündigt Caddy HTTP/3 an. Ich habe es von einem zweiten Server aus mit einem curl-Container geprüft, der HTTP/3 beherrscht:

Terminalbash
docker run --rm ymuski/curl-http3 curl -sI --http3 https://app1.{{domain}}
HTTP/3 200
via: 1.1 Caddy

Im Browser siehst du das in den Entwicklerwerkzeugen unter „Netzwerk“ in der Spalte „Protokoll“ (h3). Hast du eine Firewall, öffne auch UDP 443, sonst fällt der Browser auf HTTP/2 zurück. Das ist ohne Fehlermeldung möglich und kostet nichts außer etwas Geschwindigkeit.

Updates und Sicherung

Caddy aktualisieren: Mit caddy:2.11 bleibst du bei den Patch-Versionen der Reihe 2.11. Zum Aktualisieren lädst du das Image neu:

Terminalbash
cd ~/caddy
docker compose pull
docker compose up -d

Auf eine neue Hauptreihe wechselst du bewusst, indem du den Tag änderst. Lies vorher die Release-Notes.

Sicherung: Wichtig sind die Caddyfile, die compose.yaml und der Ordner data (Zertifikate und Schlüssel). Mit der Datei und dem Ordner hast du Caddy nach einer Neuinstallation in Minuten zurück, ohne neue Zertifikate anfordern zu müssen. Wie du Docker-Daten sicherst, steht im Beitrag zu Backups mit Duplicati.

Fehlersuche

Wenn etwas nicht klappt, schaust du zuerst mit caddy-logs ins Protokoll. Die häufigsten Fälle:

  • 502 Bad Gateway und im Log no such host: Der Container hängt nicht im Netzwerk proxy oder hat einen anderen Namen. Prüfe container_name, networks und den Namen in der Caddyfile.
  • 502 Bad Gateway und im Log connection refused: Der Name stimmt, aber unter dem Port lauscht nichts. Prüfe den Port, den die Anwendung im Container nutzt. Der Port auf dem Server ist hier egal.
  • Kein Zertifikat, Fehler im Log: Zeigt die Domain schon auf den Server (dig +short)? Sind Port 80 und 443 in Firewall und Router offen? Bei Hetzner Cloud-Firewalls prüfst du sie in der Cloud Console. Warte ruhig und teste nicht dauernd neu, denn Let’s Encrypt begrenzt fehlgeschlagene Versuche.
  • Reload ändert nichts: Eventuell hat ein Werkzeug die Datei ersetzt, und der Container sieht die alte Datei (siehe Hinweis oben). caddy-down und caddy-up lösen das.
  • Reload meldet einen Fehler: Caddy behält die alte Konfiguration. Die Fehlermeldung nennt die Zeile. Mit caddy validate prüfst du vorab.
  • Seite lädt, aber die Anwendung zeigt falsche Links: Die Anwendung kennt ihre öffentliche Adresse nicht. Viele Programme haben dafür eine Einstellung (BASE_URL oder ähnlich) und lesen die Header X-Forwarded-*.
  • Docker-Ports sind trotz Firewall offen: Docker umgeht ufw. Veröffentliche nur Caddy die Ports und lasse alle anderen Container ohne ports. Dann erreichen sie sich nur über das Netzwerk.

Häufige Fragen

Was ist der Unterschied zwischen Caddy, Nginx Proxy Manager und Traefik?

Alle drei sind Reverse Proxys. Nginx Proxy Manager hat eine Weboberfläche, in der du jede Seite von Hand einträgst. Caddy kommt mit einer sehr kurzen Textdatei aus und holt HTTPS-Zertifikate automatisch, du trägst Container aber selbst ein. Traefik liest Docker-Container aus und konfiguriert sich über Labels. Dafür hat Traefik mehr Begriffe und eine steilere Lernkurve.

Muss ich für Caddy Zertifikate selbst beantragen?

Nein. Sobald eine Domain in der Caddyfile steht und auf den Server zeigt, holt Caddy das Zertifikat bei Let's Encrypt und erneuert es rechtzeitig. Die Schlüssel liegen im Ordner data. Sichere ihn, damit du nach einer Neuinstallation nicht alles neu anfordern musst.

Wie lade ich die Caddyfile neu, ohne den Container neu zu starten?

Mit docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile. Die neue Konfiguration gilt sofort, bestehende Verbindungen bleiben bestehen. Ist die Datei fehlerhaft, bricht der Reload ab, und Caddy läuft mit der alten Konfiguration weiter.

Warum bekomme ich 502 Bad Gateway?

Caddy erreicht das Ziel nicht. Meist hängt der Container nicht im selben Docker-Netzwerk wie Caddy oder der Port in der Caddyfile stimmt nicht. Im Log steht bei einem falschen Netzwerk no such host, bei einem falschen Port connection refused.

Kann Caddy auch ohne Domain laufen?

Für HTTPS mit einem vertrauenswürdigen Zertifikat brauchst du eine Domain. Let's Encrypt stellt keine Zertifikate für reine IP-Adressen aus. Für Tests im eigenen Netz kannst du Caddy mit http:// und einer Adresse ohne Zertifikat verwenden.

Wie funktioniert Caddy mit mehreren Servern?

Hinter reverse_proxy kannst du mehrere Ziele nennen, auch Adressen auf anderen Servern. Mit lb_policy bestimmst du, wie Anfragen verteilt werden, und mit health_uri, health_interval und health_timeout fällt ein ausgefallenes Ziel automatisch aus der Verteilung.

Wie sperre ich Angreifer mit Caddy und Fail2ban?

Schreibe die Anfragen im JSON-Format in eine Datei (log mit output file und format json), lege einen Fail2ban-Filter für die Statuscodes 401 und 403 an und nutze im Jail die Kette DOCKER-USER, weil Docker-Verkehr die normale INPUT-Kette umgeht. Die Einrichtung zeige ich im Abschnitt zu Fail2ban.

Kann ich ein Wildcard-Zertifikat mit Caddy nutzen?

Ja, über die DNS-Challenge. Dafür brauchst du ein Caddy-Image mit dem DNS-Plugin deines Anbieters, das du mit xcaddy baust, und API-Zugangsdaten für deine Domain. Die Standard-Version von Caddy kann das nicht.

Kann ich Caddy auch ohne Docker nutzen?

Ja. Caddy ist eine einzelne Programmdatei und lässt sich auch über die Paketquellen installieren. Mit Docker bleibt der Server aufgeräumt, und du kannst Dienste und Caddy mit Compose gemeinsam verwalten. Die Caddyfile-Syntax ist in beiden Fällen gleich.

Läuft Caddy mit DS-Lite?

Caddy selbst schon, aber bei DS-Lite fehlt dir die öffentliche IPv4-Adresse, und du kannst keine Ports weiterleiten. Eine Lösung ist ein Tunnel über einen kleinen VPS, zum Beispiel mit Pangolin. Das erkläre ich in einem eigenen Beitrag.

Fazit

Mit Caddy hast du in wenigen Minuten einen Reverse Proxy mit automatischem HTTPS. Die Konfiguration bleibt lesbar, auch wenn sie wächst: eine Textdatei, in der jede Seite ein Block ist. Load Balancing mit Health Checks, Header, IP-Filter, Basic Auth, Weiterleitungen, Fehlerseiten und eine statische Webseite mit API-Weiterleitung hast du auf einem Server getestet. Mit Fail2ban sperrst du Angreifer, die ständig falsche Zugangsdaten probieren, und mit einem Rate Limit bremst du Bots, die eine Schnittstelle fluten.

Als Nächstes kannst du echte Dienste hinter Caddy stellen, zum Beispiel Immich, Vaultwarden, Paperless oder Gitea. Der Caddyfile-Generator hilft bei der Syntax. Wenn du lieber eine Oberfläche hast oder Docker-Container automatisch erkennen lassen willst, findest du in meiner Traefik-Anleitung die Alternative. Schreib mir gern in die Kommentare: Welchen Reverse Proxy nutzt du, und hast du dich bewusst für einen entschieden?

Teilen
XRedditE-Mail
Zurück zu allen Beiträgen

Änderungsprotokoll

  1. Abschnitt zu Rate Limiting mit dem Plugin caddy-ratelimit ergänzt, Hinweis zu Fail2ban hinter einem Proxy ergänzt, die Beschreibung von maxretry korrigiert und einen Hinweis zur passenden HTTP-Methode ergänzt.

Verwandte Artikel

Kommentare