Dokumentation zum Setup für externen Zugriff auf Docker-Container über Caddy als Reverse Proxy, mit Wildcard-DNS bei Hostinger.
Architektur-Übersicht
Internet
│
▼
Router (Port-Forwarding 80→8880, 443→8443)
│
▼
Caddy-Container (hört auf 8880/8443, intern 80/443)
│ (Docker-Netzwerk: caddy-net)
├── papra:1221
├── grimmory:PORT
└── ...weitere Dienste
Caddy ist der einzige Container mit Port-Freigabe nach außen. Alle Anwendungscontainer bleiben ohne eigenes externes Port-Mapping und sind nur über Caddy erreichbar (sofern kein zusätzliches ports:-Mapping für internen Zugriff besteht, siehe unten).
Komponenten
1. DDNS-Container (ddns-hostinger)
- Pfad:
/volume2/docker-compose/webserver/ddns-hostinger/docker-compose.yml - Script:
/volume2/appdata/ddns-hostinger/update-dns.sh - Läuft alle 5 Minuten, prüft öffentliche IP über
ifconfig.me - Bei Änderung: holt aktuelle DNS-Zone von Hostinger per API (GET), ersetzt/ergänzt nur den Wildcard-A-Record (
*), sendet komplette Zone zurück (PUT mitoverwrite: true) - Wichtig: Alle anderen Records (MX, TXT/SPF, CNAME, root-A) werden dabei unangetastet mitgeschickt – sonst würden sie bei jedem Update gelöscht
- Cache der letzten IP:
/volume2/appdata/ddns-hostinger/data/last_ip.txt - Container braucht
curlundjq(wird beim Start perapk addinstalliert, sieheentrypointin der compose)
Aktueller Inhalt update-dns.sh:
#!/bin/sh
set -e
API_TOKEN="<TOKEN>"
DOMAIN="ddung.de"
SUBDOMAIN="*"
CACHE_FILE="/data/last_ip.txt"
CURRENT_IP=$(curl -s https://ifconfig.me)
LAST_IP=$(cat "$CACHE_FILE" 2>/dev/null || echo "")
if [ "$CURRENT_IP" = "$LAST_IP" ]; then
echo "$(date): IP unverändert ($CURRENT_IP), kein Update nötig."
exit 0
fi
echo "$(date): IP geändert: $LAST_IP -> $CURRENT_IP. Hole aktuelle Zone..."
CURRENT_ZONE=$(curl -s -X GET "https://developers.hostinger.com/api/dns/v1/zones/${DOMAIN}" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json")
NEW_ZONE=$(echo "$CURRENT_ZONE" | jq \
--arg sub "$SUBDOMAIN" \
--arg ip "$CURRENT_IP" \
'[.[] | select(.type != "A" or (.name != $sub and .name != "docs"))] + [{"type":"A","name":$sub,"ttl":300,"records":[{"content":$ip}]}]')
echo "$(date): Update Hostinger DNS..."
curl -s -X PUT "https://developers.hostinger.com/api/dns/v1/zones/${DOMAIN}" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"overwrite\": true, \"zone\": $NEW_ZONE}"
echo "$CURRENT_IP" > "$CACHE_FILE"
echo "$(date): Update fertig."2. Caddy Reverse Proxy
- Pfad:
/volume2/docker-compose/caddy/docker-compose.yml - Caddyfile:
/volume2/appdata/caddy/Caddyfile - Zertifikate/Config (persistent):
/volume2/appdata/caddy/data,/volume2/appdata/caddy/config - Holt automatisch Let’s-Encrypt-Zertifikate per HTTP-01-Challenge für jede Domain in der Caddyfile
docker-compose.yml:
services:
caddy:
image: caddy:latest
container_name: caddy
restart: unless-stopped
ports:
- "8880:80"
- "8443:443"
volumes:
- /volume2/appdata/caddy/Caddyfile:/etc/caddy/Caddyfile
- /volume2/appdata/caddy/data:/data
- /volume2/appdata/caddy/config:/config
networks:
- caddy-net
networks:
caddy-net:
external: trueCaddyfile (Beispiel mit Papra):
papra.ddung.de {
reverse_proxy papra:1221
}
3. Docker-Netzwerk caddy-net
Gemeinsames externes Netzwerk, in dem Caddy und alle extern erreichbaren Anwendungscontainer hängen, damit Caddy sie über den Container-Namen ansprechen kann.
docker network create caddy-net4. Router / Port-Forwarding
| Extern | Intern (NAS) |
|---|---|
| 80 | 8880 |
| 443 | 8443 |
Diese Ports werden vom Router auf die NAS weitergeleitet, dort mappt Caddy sie im Container auf 80/443.
DNS bei Hostinger
- Wildcard-A-Record
*→ aktuelle öffentliche IP (automatisch vonddns-hostingergepflegt) - Root-A-Record
@→ separate, feste IP (nicht von DDNS betroffen) - MX, TXT (SPF, apple-domain), CNAME (
www,sig1._domainkey) → unverändert, iCloud-Mail-bezogen
Jede Subdomain (papra.ddung.de, grimmory.ddung.de, etc.) löst dank Wildcard automatisch auf, ohne dass bei neuen Diensten ein neuer DNS-Eintrag nötig ist.
Neuen Dienst extern erreichbar machen
Wiederholbarer Ablauf für jeden weiteren Container:
- Container ins
caddy-nethängen – in dessendocker-compose.yml:
networks:
- default
- caddy-net
networks:
default:
caddy-net:
external: trueDanach: docker compose up -d im jeweiligen Verzeichnis.
- Neuen Block in der Caddyfile ergänzen:
neuerdienst.ddung.de {
reverse_proxy <container-name>:<port>
}
- Caddy neu laden (ohne Neustart):
docker exec caddy caddy reload --config /etc/caddy/CaddyfileKein DNS-Eintrag nötig – der Wildcard-Record deckt das automatisch ab.
Papra – Sonderfall interner + externer Zugriff
Papra ist sowohl extern (https://papra.ddung.de über Caddy) als auch intern (http://192.168.2.200:1221 direkt) erreichbar, da die ports: - 1221:1221-Zeile in der Papra-compose erhalten bleibt.
services:
papra:
image: ghcr.io/papra-hq/papra:latest
container_name: papra
restart: unless-stopped
ports:
- 1221:1221
environment:
- AUTH_SECRET=<ROTIERT, NICHT HIER EINTRAGEN>
- APP_BASE_URL=https://papra.ddung.de
- INGESTION_FOLDER_IS_ENABLED=true
volumes:
- /volume2/appdata/papra/app/app-data:/app/app-data
- /volume1/documents/papra-consume:/app/ingestion
user: 1000:10
networks:
- default
- caddy-net
networks:
default:
caddy-net:
external: trueBekanntes Risiko: APP_BASE_URL ist auf die externe Domain gesetzt. Falls das bei internem IP-Zugriff zu Cookie-/Login-Problemen führt, zwei Optionen:
- Lokale DNS-Auflösung einrichten, sodass
papra.ddung.deauch intern auf die NAS-IP zeigt (z.B. Pi-hole oderhosts-Datei) ports:-Zeile aus Papra-compose entfernen, um Papra ausschließlich über Caddy erreichbar zu machen (dann aber vorher lokale DNS-Lösung sicherstellen, sonst kein interner Zugriff mehr)
Verwandte Notizen
- Quartz-Updates – Container-Management für Quartz (kann über diesen Reverse Proxy exponiert werden)
- NAS Docker-Berechtigungen – für alle Containers in diesem Setup
- Docker Compose Beispiele – Muster für weitere Container im caddy-net
Sicherheitshinweise
- Nicht jeder Dienst sollte öffentlich erreichbar sein. Bei sensiblen Diensten (Home Assistant, Backup-Tools) lieber über VPN (z.B. WireGuard) statt direktem Caddy-Exposing erwägen.
- API-Token und Secrets (Hostinger API-Token,
AUTH_SECRETvon Papra) regelmäßig rotieren, falls sie versehentlich geteilt wurden. - Bei extern erreichbaren Diensten auf starke Passwörter/2FA achten, ggf. fail2ban oder Crowdsec vor Caddy erwägen.
Troubleshooting-Hinweise (aus diesem Setup gelernt)
jqfehlt im DDNS-Container: In derentrypoint-Zeile der composeapk add --no-cache curl jqstatt nurcurl, danachdocker compose up -d --force-recreate.- DDNS-Script macht nichts beim Testen: Cache-Datei
data/last_ip.txtlöschen, um ein Update zu erzwingen, auch wenn sich die IP nicht geändert hat. - DNS-Auflösung prüfen:
nslookup <subdomain>.ddung.de– sollte auf die öffentliche IP zeigen (TTL 300s, also kurze Wartezeit bei Änderungen einplanen). - Netzwerk-Kontrolle:
docker network inspect caddy-netzeigt, welche Container tatsächlich im Netzwerk hängen.