Docker Caddy
Installation
DATEN
| Parameter | Wert | Erläuterung |
|---|---|---|
| Server | caddy | Debian-Server, Grundkonfiguration per Skript |
| Betriebssystem | Debian 13 | |
| FQDN | caddy.it2XX.xinmen.de | Fully Qualified Domain Name |
| DOM | it2XX.xinmen.de | Domain Name |
| Dienst 1 | kuma.it2XX.xinmen.de | Uptime Kuma (Monitoring) |
| Dienst 2 | hedgedoc.it2XX.xinmen.de | HedgeDoc (kollaborative Notizen) |
| IP, GW, NS | per Skript | werden von der Grundkonfiguration gesetzt |
Grundkonfiguration
Der Server wird über ein Skript vorkonfiguriert (Netzwerk, Hostname, Benutzer, Zertifikate).
- Voraussetzung im DNS
Die Namen der Dienste müssen auf den Server zeigen, z. B. als CNAME auf caddy.it2XX.xinmen.de:
kuma.it2XX.xinmen.de CNAME caddy.it2XX.xinmen.de. hedgedoc.it2XX.xinmen.de CNAME caddy.it2XX.xinmen.de.
Container mit Docker: Caddy, Uptime Kuma und HedgeDoc
Auf dem Server caddy.it2XX.xinmen.de laufen Dienste als Docker-Container. Als praktisches Beispiel dienen drei Dienste: Caddy (Reverse Proxy), Uptime Kuma (Monitoring) und HedgeDoc (kollaborative Notizen). Jeder Dienst bekommt sein eigenes Verzeichnis mit eigener compose.yaml. Das entspricht der Praxis, Container-Stacks unabhängig voneinander verwaltbar zu halten (eigenes Update, eigenes Down/Up, ohne andere Dienste zu berühren). Verwaltet werden die Stacks mit docker compose.
Im weiteren Verlauf des Kurses werden diese Container überwacht, z. B. mit Wazuh. Der Aufbau ist deshalb von Anfang an so angelegt, dass Logs an festen Stellen landen (siehe Vorbereitung für die Überwachung).
Was ist Docker
Docker ist eine Container-Engine. Ein Container ist ein normaler Linux-Prozess, der durch Kernel-Funktionen (Namespaces, cgroups) vom Rest des Systems abgeschottet ist und sein eigenes Dateisystem aus einem Image mitbringt.
- Aufbau
- dockerd: Der Docker-Daemon läuft dauerhaft als root und verwaltet alle Container, Images, Netze und Volumes.
- containerd: startet und überwacht die eigentlichen Container-Prozesse.
- docker (CLI): spricht über den Socket
/run/docker.sockmit dem Daemon.
- Sicherheitsrelevant
- Wer auf
/run/docker.sockzugreifen darf, hat praktisch root-Rechte auf dem Host. Mitglieder der Gruppedockersind damit root-gleich. - Der Daemon läuft als root. Ein Ausbruch aus einem Container kann deshalb beim Host-root landen, wenn der Container falsch konfiguriert ist (z. B.
--privileged, gemounteter Docker-Socket).
- Unterschied zu Podman
Podman kommt ohne zentralen Daemon aus und läuft standardmäßig rootless. Die
Befehle sind nahezu identisch, docker und podman lassen sich meist 1:1 austauschen.
- Merksatz
Docker = ein Daemon (root) verwaltet alle Container | Zugriff auf den Docker-Socket = root
Docker installieren
Installiert wird aus dem offiziellen Docker-Repository.
- Schlüssel und Repository einrichten
- apt update
- apt install -y ca-certificates curl
- install -m 0755 -d /etc/apt/keyrings
- curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" > /etc/apt/sources.list.d/docker.list
- Docker und das Compose-Plugin installieren
- apt update
- apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
- Prüfen
- systemctl status docker
- docker version
- docker compose version
- Hinweis
Compose ist heute ein Plugin von Docker. Der Befehl heißt docker compose (mit Leerzeichen),
nicht mehr docker-compose.
Docker-Logging festlegen
Docker schreibt die Ausgabe jedes Containers in eine Datei unter
/var/lib/docker/containers/. Ohne Begrenzung wachsen diese Dateien
unbegrenzt. Die Rotation wird einmal zentral festgelegt.
- Daemon-Konfiguration anlegen
- vim /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
- Docker neu starten
- systemctl restart docker
- Hinweis
Die Einstellung gilt für Container, die danach erstellt werden. Der Treiber
json-file ist wichtig für die spätere Überwachung, weil ein Agent wie
Wazuh diese Dateien direkt lesen kann.
Vorbereitung: Zertifikate
Die Zertifikate liegen bereits vor unter:
/etc/ssl/own.crt/etc/ssl/own.key
Das Zertifikat muss die Namen kuma.it2XX.xinmen.de und hedgedoc.it2XX.xinmen.de enthalten
(oder ein Wildcard-Zertifikat für *.it2XX.xinmen.de sein).
Verzeichnisstruktur
Jeder Dienst bekommt ein eigenes Verzeichnis mit eigener Compose-Datei. Der
Dateiname ist immer compose.yaml, das ist der aktuelle Standardname (der
alte Name docker-compose.yml funktioniert noch, wird aber nicht mehr verwendet).
docker compose sucht beim Aufruf automatisch nach dieser Datei im aktuellen Verzeichnis.
/opt/docker/
├── caddy/
│ ├── compose.yaml
│ └── Caddyfile
├── uptime-kuma/
│ └── compose.yaml
└── hedgedoc/
├── compose.yaml
└── .env
- Struktur anlegen
- mkdir -p /opt/docker/{caddy,uptime-kuma,hedgedoc}
- mkdir -p /var/log/caddy
- Merksatz
Ein Verzeichnis = ein Dienst = ein eigenständiges Compose-Projekt.
Wie Container sich im Netzwerk finden
Docker legt für jedes selbst angelegte Netzwerk ein internes DNS an. Jeder
Container ist darin unter seinem container_name erreichbar, nicht unter
localhost und nicht über die Host-IP.
- Merksatz
container_name = Hostname im internen Netz. Zwei Container im selben Netzwerk
erreichen sich per ping container_name bzw. curl http://container_name:PORT.
Wichtige Besonderheit bei getrennten Verzeichnissen: Jedes Compose-Projekt
legt sein eigenes, isoliertes Netzwerk an. Liegen Caddy und Uptime Kuma in
unterschiedlichen Verzeichnissen, finden sie sich nicht automatisch. Die
Lösung: Ein gemeinsames Netzwerk wird einmalig manuell und
projektübergreifend angelegt, und jede compose.yaml bindet sich als
external ein.
- Einmalig das gemeinsame Netzwerk anlegen
- docker network create proxy
- Prüfen
- docker network ls
Internet / Schulungsnetz
│ 80/443
┌──────┴──────┐
│ caddy │ einziger Container mit Ports am Host
└──────┬──────┘
Netz "proxy" │
┌────────────┴────────────┐
┌──────┴──────┐ ┌──────┴──────┐
│ uptime-kuma │ │ hedgedoc │
│ :3001 │ │ :3000 │
└─────────────┘ └──────┬──────┘
internes Netz │ (nur HedgeDoc-Projekt)
┌──────┴──────┐
│ hedgedoc-db │
│ :5432 │
└─────────────┘
Dienst 1: Caddy (Reverse Proxy)
Caddy ist der einzige Container, der Ports am Host öffnet, weil er der einzige Kontaktpunkt von außen ist. Er terminiert TLS mit dem vorhandenen Zertifikat und leitet intern per HTTP an die Dienste weiter.
- Compose-Datei anlegen
- vim /opt/docker/caddy/compose.yaml
services:
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- /etc/ssl/own.crt:/etc/ssl/own.crt:ro
- /etc/ssl/own.key:/etc/ssl/own.key:ro
- /var/log/caddy:/var/log/caddy
- caddy-data:/data
- caddy-config:/config
networks:
- proxy
volumes:
caddy-data:
caddy-config:
networks:
proxy:
external: true
- Erklärung
restart: unless-stopped: Docker startet den Container nach einem Absturz oder Neustart des Servers wieder, außer man hat ihn selbst gestoppt. Ohne diese Zeile bleibt der Dienst nach einem Reboot aus.:ro: Caddyfile und Zertifikate werden nur lesend eingebunden. Der Container kann sie nicht verändern.443:443/udp: für HTTP/3. Kann entfallen, schadet aber nicht./var/log/caddy: Hier schreibt Caddy seine Zugriffslogs, direkt auf dem Host lesbar.
- Hinweis depends_on
Caddy und die Dienste liegen in getrennten Compose-Projekten, ein depends_on
zwischen ihnen gibt es deshalb nicht (das funktioniert nur innerhalb eines Projekts).
Das ist unproblematisch: Caddy versucht bei jeder Anfrage erneut, den Dienst zu erreichen.
Caddyfile
Die Caddyfile ist Caddys Konfigurationsdatei (reiner Text, kein YAML/JSON). Sie legt pro Domain fest, welches Zertifikat benutzt wird und wohin intern weitergeleitet wird.
- Caddyfile anlegen
- vim /opt/docker/caddy/Caddyfile
(logging) {
log {
output file /var/log/caddy/access.log
format json
}
}
kuma.it2XX.xinmen.de {
tls /etc/ssl/own.crt /etc/ssl/own.key
import logging
reverse_proxy uptime-kuma:3001
}
hedgedoc.it2XX.xinmen.de {
tls /etc/ssl/own.crt /etc/ssl/own.key
import logging
reverse_proxy hedgedoc:3000
}
Aufbau:
(logging) { … }: ein wiederverwendbarer Baustein (Snippet). Er wird in jedem Host-Block mitimport loggingeingebunden.log … format json: Jeder Zugriff wird als eine JSON-Zeile in/var/log/caddy/access.loggeschrieben. JSON lässt sich später von Wazuh ohne eigenen Decoder auswerten.kuma.it2XX.xinmen.de {: für welchen Hostnamen der Block gilt.tls …: welches Zertifikat und welcher Schlüssel verwendet werden.reverse_proxy uptime-kuma:3001: wohin die entschlüsselte Anfrage intern geht (container_name als DNS-Name).
- Caddyfile prüfen, bevor Caddy sie lädt
- docker run --rm -v /opt/docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro caddy:2 caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
Die Prüfung meldet einen Fehler, solange die Zertifikate nicht eingebunden sind. Alternativ nach dem Start:
- docker exec caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
- Caddyfile im laufenden Betrieb neu laden
- docker exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
Dienst 2: Uptime Kuma
Uptime Kuma braucht keinen Port am Host. Es lauscht intern auf Port 3001 und wird ausschließlich über Caddy nach außen gereicht. Die Daten (SQLite-Datenbank, Konfiguration) liegen in einem benannten Volume, damit sie Neustarts und Updates überleben.
- Compose-Datei anlegen
- vim /opt/docker/uptime-kuma/compose.yaml
services:
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
restart: unless-stopped
volumes:
- uptime-kuma-data:/app/data
networks:
- proxy
volumes:
uptime-kuma-data:
networks:
proxy:
external: true
- Wichtig
Kein ports:-Eintrag. Ein Dienst, der nur von Caddy angesprochen wird,
muss nicht am Host offen sein. Ein offener Port wäre sogar ein unnötiges Risiko:
Er läge ungeschützt ohne TLS am Netz.
- Achtung Docker und Firewall
Ports, die mit ports: veröffentlicht werden, trägt Docker selbst in die
Paketfilter-Regeln ein. Sie sind dann von außen erreichbar, auch wenn eine
Host-Firewall (nftables, ufw) den Port eigentlich sperrt. Deshalb nur dort Ports
veröffentlichen, wo es wirklich nötig ist: hier nur bei Caddy.
Dienst 3: HedgeDoc mit Datenbank
HedgeDoc braucht eine Datenbank. Sie läuft als zweiter Container im selben
Compose-Projekt. Die Datenbank hängt nur im internen Netz des Projekts,
HedgeDoc zusätzlich im Netz proxy. Caddy erreicht so HedgeDoc, aber
niemals die Datenbank.
- Passwort in eine eigene Datei
- vim /opt/docker/hedgedoc/.env
DB_PASSWORD=EinLangesPasswortHierEintragen
- chmod 600 /opt/docker/hedgedoc/.env
- Compose-Datei anlegen
- vim /opt/docker/hedgedoc/compose.yaml
services:
database:
image: postgres:16-alpine
container_name: hedgedoc-db
restart: unless-stopped
environment:
- POSTGRES_USER=hedgedoc
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=hedgedoc
volumes:
- hedgedoc-db:/var/lib/postgresql/data
networks:
- intern
hedgedoc:
image: quay.io/hedgedoc/hedgedoc:1.10.0
container_name: hedgedoc
restart: unless-stopped
environment:
- CMD_DB_URL=postgres://hedgedoc:${DB_PASSWORD}@hedgedoc-db:5432/hedgedoc
- CMD_DOMAIN=hedgedoc.it2XX.xinmen.de
- CMD_PROTOCOL_USESSL=true
- CMD_URL_ADDPORT=false
volumes:
- hedgedoc-uploads:/hedgedoc/public/uploads
depends_on:
- database
networks:
- intern
- proxy
volumes:
hedgedoc-db:
hedgedoc-uploads:
networks:
intern:
proxy:
external: true
- Erklärung
.env:docker composeliest diese Datei im Projektverzeichnis automatisch.${DB_PASSWORD}wird beim Start ersetzt. Das Passwort steht so nicht in der compose.yaml.networks: intern: wird vom Projekt selbst angelegt (ohneexternal). Nur die beiden HedgeDoc-Container hängen darin.depends_on: Innerhalb desselben Projekts funktioniert es. Die Datenbank wird vor HedgeDoc gestartet.
- Hinweis Reverse-Proxy-Variablen
HedgeDoc muss wissen, unter welcher URL es von außen erreichbar ist. Sonst erzeugt es Links auf CSS und JavaScript mit falscher Adresse und die Seite erscheint ohne Styling:
CMD_DOMAIN: externer HostnameCMD_PROTOCOL_USESSL=true: Links als https:// erzeugen, obwohl HedgeDoc intern nur HTTP spricht (TLS macht Caddy)CMD_URL_ADDPORT=false: keinen Port an die URLs hängen
- Merksatz
Ein neuer Dienst braucht vier Dinge: eigenes Verzeichnis, eigener container_name,
Anbindung an das Netz proxy mit external: true, eigener Host-Block
in der Caddyfile mit container_name:Port. Nach außen offen sind immer nur 80/443 bei Caddy.
Starten und Stoppen
Jeder Dienst wird aus seinem eigenen Verzeichnis gestartet. Die Reihenfolge ist
egal, solange das Netzwerk proxy existiert.
- Dienste starten
- cd /opt/docker/uptime-kuma && docker compose up -d
- cd /opt/docker/hedgedoc && docker compose up -d
- cd /opt/docker/caddy && docker compose up -d
- Status prüfen
- docker ps
- Einzelnen Dienst stoppen
- cd /opt/docker/caddy && docker compose down
Test
- Aufruf im Browser
https://kuma.it2XX.xinmen.de https://hedgedoc.it2XX.xinmen.de
Beim ersten Aufruf fragt Uptime Kuma nach einem Admin-Account. Danach lassen sich
Monitore anlegen (Ping, TCP-Port, HTTP), z. B. für https://hedgedoc.it2XX.xinmen.de.
- Zugriffslog ansehen
- tail -f /var/log/caddy/access.log
- Nur die wichtigsten Felder (mit jq)
- apt install -y jq
- tail -f /var/log/caddy/access.log | jq -c '{ts, ip: .request.remote_ip, host: .request.host, uri: .request.uri, status}'
Vorbereitung für die Überwachung
Im weiteren Kurs werden die Container überwacht, z. B. mit einem Wazuh-Agenten auf diesem Server. Diese Quellen sind dafür interessant:
| Quelle | Wo | Was steht drin |
|---|---|---|
| Caddy-Zugriffslog | /var/log/caddy/access.log |
jeder HTTP-Zugriff als JSON: Quell-IP, Host, URL, Statuscode, User-Agent |
| Container-Ausgaben | /var/lib/docker/containers/*/*-json.log |
stdout und stderr jedes Containers, z. B. Fehlermeldungen von HedgeDoc oder der Datenbank |
| Docker-Ereignisse | docker events |
Container gestartet, gestoppt, gelöscht, Image gezogen, docker exec ausgeführt
|
| Docker-Daemon | journalctl -u docker |
Meldungen des Daemons selbst |
| Dateien und Socket | /etc/docker, /run/docker.sock, /opt/docker |
Änderungen an Konfiguration und compose-Dateien, Zugriffe auf den Socket (auditd) |
- Docker-Ereignisse live ansehen
- docker events
In einem zweiten Terminal z. B. docker exec -it caddy sh ausführen: Im ersten Terminal
erscheint ein exec_create- und exec_start-Ereignis. Genau solche Ereignisse sind für die Überwachung
interessant, weil ein Angreifer oder ein neugieriger Admin so in einen Container gelangt.
- Merksatz
Was überwacht werden soll, muss zuerst an einer festen Stelle protokolliert werden:
Caddy schreibt nach /var/log/caddy, Docker nach /var/lib/docker/containers, Ereignisse liefert docker events.
Cheat Sheet: Docker und docker compose
Container
- Laufende Container anzeigen
- docker ps
- Alle Container anzeigen (auch gestoppte)
- docker ps -a
- Logs eines Containers anzeigen
- docker logs caddy
- Logs live mitverfolgen (wie tail -f)
- docker logs -f uptime-kuma
- Nur die letzten 50 Zeilen
- docker logs --tail 50 hedgedoc
- Shell im laufenden Container öffnen
- docker exec -it caddy sh
- Einzelnen Befehl im Container ausführen
- docker exec caddy cat /etc/caddy/Caddyfile
- Container neu starten
- docker restart caddy
- Container stoppen / starten
- docker stop caddy
- docker start caddy
- Details zu einem Container (IP, Mounts, Umgebungsvariablen)
- docker inspect uptime-kuma
- Wo liegt die Logdatei eines Containers
- docker inspect --format 'Vorlage:.LogPath' caddy
- Ressourcenverbrauch live anzeigen
- docker stats
Images
- Lokale Images anzeigen
- docker images
- Image aktualisieren (neue Version ziehen)
- docker pull louislam/uptime-kuma:1
- Ungenutzte Images aufräumen
- docker image prune
Netzwerke und Volumes
- Netzwerke anzeigen
- docker network ls
- Details zu einem Netzwerk (welche Container hängen drin, IPs)
- docker network inspect proxy
- Volumes anzeigen
- docker volume ls
- Wo liegt ein Volume auf dem Host
- docker volume inspect uptime-kuma_uptime-kuma-data
- Hinweis
docker compose stellt Volume-Namen den Projektnamen voran, also dem Verzeichnisnamen:
aus uptime-kuma-data im Verzeichnis uptime-kuma wird uptime-kuma_uptime-kuma-data.
docker compose (im jeweiligen Dienst-Verzeichnis)
- Stack starten (im Hintergrund)
- docker compose up -d
- Stack stoppen und Container entfernen (Volumes bleiben erhalten)
- docker compose down
- Status der Container dieses Projekts
- docker compose ps
- Logs aller Container dieses Projekts
- docker compose logs
- Logs live mitverfolgen
- docker compose logs -f
- Dienst aktualisieren (neues Image ziehen, dann neu erstellen)
- docker compose pull
- docker compose up -d
- Fertige Konfiguration anzeigen (mit eingesetzten Variablen aus .env)
- docker compose config
Troubleshooting-Reihenfolge
Wenn ein Dienst nicht erreichbar ist, immer in dieser Reihenfolge prüfen:
- Läuft der Container überhaupt? →
docker ps - Was sagen die Logs? →
docker logs -f containername - Hängt er im richtigen Netz? →
docker network inspect proxy - Ist der Dienst intern erreichbar? →
docker exec -it caddy sh, dannwget -O- http://uptime-kuma:3001 - Stimmt die Caddyfile? →
docker exec caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile - Kommt die Anfrage bei Caddy an? →
tail -f /var/log/caddy/access.log - Zeigt der Name auf den Server? →
host kuma.it2XX.xinmen.de
- Merksatz
docker logs -f = erste Anlaufstelle bei jedem Problem | docker exec -it NAME sh = das Problem von innen ansehen | docker network inspect = wer hängt wirklich in welchem Netz
Merksatz (Zusammenfassung)
docker compose = ein YAML-File statt mehrerer docker-run-Befehle | getrenntes Verzeichnis pro Dienst = unabhängige Compose-Projekte | gemeinsames Netz einmalig anlegen und in jeder compose.yaml als external einbinden | container_name = DNS-Name im internen Netz | nur Caddy veröffentlicht Ports | restart: unless-stopped, sonst bleibt der Dienst nach dem Reboot aus | Logs an feste Stellen schreiben, damit sie später überwacht werden können