
Docker Compose vs. Kubernetes 2026 – Wann sich was lohnt
Docker Compose vs. Kubernetes 2026: Kosten, Komplexität und Skalierung im direkten Vergleich – mit Entscheidungsmatrix, Preisen und Praxisbeispielen.
Container sind 2026 aus kaum einer Produktionsumgebung wegzudenken – und genau deshalb lohnt sich der Blick auf die Standardeinstellungen. Ein nacktes docker run nginx startet den Prozess als UID 0, mit einem vollen Set an Linux-Capabilities und Schreibzugriff auf das komplette Container-Filesystem. Wer aus dem Container ausbricht oder ihn über eine Applikationslücke übernimmt, landet in vielen Fällen direkt auf einem Kernel mit deutlich mehr Rechten als nötig.
Die Zahlen sind ernüchternd: Analysen öffentlicher Registries zeigen seit Jahren, dass ein großer Teil der Images veraltete Basis-Layer mit bekannten CVEs enthält. Gleichzeitig steigt die Zahl der Angriffe auf CI/CD-Pipelines und Container-Registries deutlich – Supply-Chain-Angriffe wie kompromittierte npm- oder PyPI-Pakete sind 2026 der Normalfall, nicht die Ausnahme.
Dieser Guide zeigt dir einen konkreten, umsetzbaren Härtungspfad für Docker in elf Schritten. Alle Beispiele laufen auf einem normalen Linux-VPS (z. B. Hetzner Cloud CX22 mit 2 vCPU und 4 GB RAM ab rund 4,51 €/Monat) und sind ohne Kubernetes umsetzbar. Wer mehr braucht, findet am Ende eine Checkliste und Kostenübersicht.
Bevor du härtest, musst du wissen, was du eigentlich verteidigst. Docker besteht aus mehreren Komponenten mit sehr unterschiedlichem Risikoprofil: dem Daemon (dockerd), der als Root läuft, dem Docker-Socket /var/run/docker.sock, den Container-Runtimes (runc, crun) und dem Netzwerk-Stack (iptables/nftables, bridge, overlay).
Die vier realistischen Angriffswege in der Praxis:
/var/run/docker.sock.docker inspect, Logs oder Crash-Dumps landen.Ein wichtiger Punkt: Der Docker-Daemon selbst ist 2026 weiterhin ein Root-Dienst. Wer Zugriff auf den Socket hat, hat faktisch Root auf dem Host – deshalb ist der Socket-Schutz kein Nice-to-have, sondern die wichtigste Einzelmaßnahme überhaupt.
Jede zusätzliche Binary im Image ist potenzielle Angriffsfläche. Ein ubuntu:24.04-Image bringt rund 80 MB Nutzlast und hunderte Pakete mit, von denen deine Applikation vielleicht fünf braucht. Der Wechsel auf alpine, distroless oder scratch reduziert die CVE-Oberfläche oft um 80 bis 95 Prozent.
Ein typisches gehärtetes Multi-Stage-Dockerfile sieht so aus:
# Build-Stage
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/app ./cmd/app
# Runtime-Stage
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
Der entscheidende Vorteil: Im finalen Image existiert weder eine Shell noch ein Paketmanager. Ein Angreifer, der Code-Ausführung erlangt, kann keine zusätzlichen Tools nachladen. Prüfe deine Image-Größe mit docker images und vergleiche – alles über 150 MB sollte kritisch hinterfragt werden.
Standardmäßig läuft der Container-Prozess als Root. Auch wenn User Namespaces das auf dem Host abmildern, ist es im Container selbst weiterhin UID 0 – mit allen Konsequenzen für Schreibrechte und Kernel-Interaktion. Setze deshalb immer einen expliziten User.
FROM alpine:3.20
RUN addgroup -g 10001 -S app && adduser -u 10001 -S app -G app
WORKDIR /app
COPY --chown=10001:10001 . .
USER 10001:10001
CMD ["./server"]
Beim Start kannst du das zusätzlich erzwingen, falls das Image es nicht sauber setzt:
docker run -d \
--user 10001:10001 \
--read-only \
--cap-drop=ALL \
--security-opt no-new-privileges:true \
myapp:1.0
Das Flag --security-opt no-new-privileges:true verhindert, dass ein Prozess über setuid-Binaries doch noch Root-Rechte erlangt. In Kombination mit einem Non-Root-User ist das eine der wirksamsten Einzelmaßnahmen und kostet praktisch keine Performance.
Docker gibt Containern per Default 14 Linux-Capabilities – darunter CAP_CHOWN, CAP_NET_RAW und CAP_SETUID. Die meisten davon braucht eine normale Web-Applikation nie. Der saubere Ansatz lautet: alles droppen, dann gezielt einzelne Capabilities zurückgeben.
| Capability | Wofür nötig | Empfehlung |
|---|---|---|
| CAP_NET_BIND_SERVICE | Ports unter 1024 binden | Nur wenn nötig (besser: Port 8080 nutzen) |
| CAP_NET_RAW | Ping, Raw Sockets | Immer droppen |
| CAP_SYS_ADMIN | Mounts, Namespaces | Niemals vergeben |
| CAP_CHOWN | Dateibesitzer ändern | Meist droppen |
| CAP_SYS_PTRACE | Debugging anderer Prozesse | Niemals vergeben |
docker run -d \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt no-new-privileges \
nginx:1.27-alpine
Prüfe im laufenden Container mit capsh --print (falls vorhanden) oder über docker inspect, welche Capabilities tatsächlich aktiv sind. In Docker Compose setzt du das über cap_drop: [ALL] und cap_add in der Service-Definition.
Docker nutzt seit 1.10 ein Standard-seccomp-Profil, das rund 44 von über 300 Syscalls blockiert. Das ist ein guter Start, aber nicht ausreichend: Viele gefährliche Syscalls wie ptrace oder keyctl sind nur teilweise eingeschränkt.
Ein eigenes, restriktives Profil erstellst du am einfachsten, indem du das Default-Profil kopierst und weiter einschränkst:
# Default-Profil als Basis
curl -sSL https://raw.githubusercontent.com/moby/moby/master/profiles/seccomp/default.json -o seccomp.json
# Container mit eigenem Profil starten
docker run -d \
--security-opt seccomp=./seccomp.json \
--security-opt apparmor=docker-default \
myapp:1.0
Für AppArmor gilt dasselbe Prinzip: Auf Ubuntu und Debian läuft docker-default automatisch, aber du kannst mit aa-genprof ein passgenaues Profil für deine Applikation erzeugen. Der Aufwand lohnt sich besonders für langlebige Dienste wie Datenbanken oder Reverse Proxies. Alternativ: --security-opt seccomp=unconfined ist niemals eine Option in Produktion.
Ein beschreibbares Root-Filesystem ist ein Geschenk für Angreifer: Sie können Binaries ablegen, Cronjobs schreiben oder Webshells persistieren. Setze deshalb --read-only und gib nur explizit benötigte Pfade als tmpfs oder Volume frei.
docker run -d \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /run:rw,noexec,nosuid,size=16m \
-v app-uploads:/var/www/uploads:rw \
--memory=512m \
--memory-swap=512m \
--cpus=1.0 \
--pids-limit=200 \
myapp:1.0
Die Ressourcen-Limits sind genauso wichtig wie die Dateisystem-Härtung. Ohne --pids-limit kann ein Fork-Bomb-Prozess den Host lahmlegen, ohne --memory frisst ein Memory-Leak die ganze Maschine. Als Faustregel gilt: --memory auf etwa 70 Prozent des erwarteten Peak-Verbrauchs setzen und --pids-limit auf das Zwei- bis Dreifache der normalen Prozessanzahl.
Das Flag noexec auf tmpfs verhindert zusätzlich, dass ein Angreifer ein heruntergeladenes Binary direkt ausführt – ein kleiner Aufwand mit großer Wirkung.
Im Docker-Default-Bridge-Netzwerk erreicht jeder Container jeden anderen. Das ist für Laterale Bewegung ideal. Erstelle stattdessen pro Anwendung ein eigenes, internes Netzwerk und verbinde nur, was wirklich kommunizieren muss.
docker network create --internal backend
docker network create frontend
docker run -d --network backend --name db postgres:16-alpine
docker run -d --network backend --name api myapi:1.0
docker run -d --network frontend -p 443:8443 --name proxy nginx:1.27-alpine
docker network connect frontend api
Das --internal-Flag sorgt dafür, dass Container in diesem Netz keinen Zugang nach außen haben – perfekt für Datenbanken. Noch konsequenter ist --network=none für Container, die überhaupt keine Netzwerkverbindung brauchen.
Der Docker-Socket ist der kritischste Punkt überhaupt. Mounte /var/run/docker.sock niemals in einen Container, den du nicht vollständig kontrollierst – wer den Socket hat, kann einen privilegierten Container starten und ist damit Root auf dem Host. Falls du unbedingt Docker-in-Docker brauchst, nutze einen Socket-Proxy wie tecnativa/docker-socket-proxy, der nur bestimmte API-Endpunkte freigibt.
Umgebungsvariablen sind praktisch, aber kein Secret-Store. Sie tauchen in docker inspect, in /proc/<pid>/environ, in Crash-Dumps und in manchen Logging-Systemen auf. Nutze stattdessen Docker Secrets oder gemountete Dateien.
# Secret aus Datei anlegen (Swarm-Modus)
printf "supersecret" | docker secret create db_password -
# In Compose: Secrets als Datei mounten
services:
api:
image: myapi:1.0
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Im Container liegt das Secret dann unter /run/secrets/db_password als Datei mit restriktiven Rechten. Für Kubernetes-Umgebungen ist ein externer Store wie HashiCorp Vault oder der External Secrets Operator die bessere Wahl. Wichtig: Rotiere Secrets regelmäßig – spätestens alle 90 Tage, bei kritischen Zugängen alle 30 Tage.
Härtung beginnt vor dem Deployment. Scanne jedes Image in der CI-Pipeline und blockiere Builds mit kritischen Findings. Die drei wichtigsten Tools 2026 sind Trivy, Grype und Docker Scout.
# Trivy: Scan mit Severity-Filter
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:1.0
# SBOM erzeugen
syft myapp:1.0 -o spdx-json > sbom.json
# Image signieren
cosign sign --key cosign.key registry.example.com/myapp:1.0
# Signatur verifizieren
cosign verify --key cosign.pub registry.example.com/myapp:1.0
Ergänzend solltest du Images immer mit festem Digest statt Tag pinnen: nginx@sha256:abc123... statt nginx:latest. Das verhindert, dass ein manipulierter Tag unbemerkt ein anderes Image zieht. In der CI gehört --exit-code 1 bei HIGH und CRITICAL zum Standard, sonst wird der Scan ignoriert.
Der Daemon selbst braucht Aufmerksamkeit. In /etc/docker/daemon.json setzt du sinnvolle Defaults für alle Container:
{
"icc": false,
"no-new-privileges": true,
"userns-remap": "default",
"live-restore": true,
"userland-proxy": false,
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
userns-remap bildet Container-Root auf einen unprivilegierten Host-User ab – ein Container-Escape landet dann nicht mehr als Root. Noch konsequenter ist der Rootless Mode: dockerd-rootless-setuptool.sh install startet den Daemon komplett als normaler User. Das ist seit Docker 20.10 stabil und heute der empfohlene Standard für Single-Host-Setups.
Für Runtime-Erkennung setzt du Falco ein, das Syscalls auf Anomalien prüft (z. B. Shell-Start in einem Container, der keine Shell hat):
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco
Für maximale Isolation bieten sich gVisor oder Kata Containers an. Beide kapseln den Container in einer eigenen Kernel-Sandbox bzw. einer leichtgewichtigen VM, kosten aber 5 bis 20 Prozent Performance und deutlich mehr RAM (Kata: mindestens 1 GB pro Container). Für Multi-Tenant-Setups ist das den Aufpreis wert – ein VPS mit 16 GB RAM (ab ca. 15–20 €/Monat) trägt problemlos 10 bis 15 Kata-Container.
Führe nach jeder Änderung docker-bench-security aus und vergleiche die Score-Entwicklung. Das Tool prüft über 100 Punkte aus dem CIS Docker Benchmark und gibt konkrete Handlungsempfehlungen aus:
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /etc:/etc:ro \
docker/docker-bench-security
Für Monitoring gilt: Logs mit max-size begrenzen (siehe daemon.json), Container-Events über docker events oder einen zentralen Collector (Loki, Elastic) sammeln und Alarme auf unerwartete Neustarts, OOM-Kills und neue Images setzen.
| Maßnahme | Aufwand | Wirkung |
|---|---|---|
| Non-Root + no-new-privileges | 15 Min | Sehr hoch |
| cap-drop=ALL | 10 Min | Sehr hoch |
| Read-Only + tmpfs | 20 Min | Hoch |
| Seccomp/AppArmor | 1–2 Std | Hoch |
| Rootless Mode | 1 Std | Sehr hoch |
| Image-Scanning in CI | 2 Std | Hoch |
| Falco Runtime-Detection | 3–4 Std | Mittel–Hoch |
Kostenmäßig ist Hardening fast kostenlos – es kostet Zeit, nicht Geld. Wer allerdings Runtime-Isolation (gVisor/Kata) oder ein Managed-Kubernetes mit Pod Security Standards braucht, zahlt für einen Cluster schnell 30 bis 80 €/Monat. Ein einfacher gehärteter Docker-Host auf einem VPS mit 4 vCPU und 8 GB RAM liegt bei etwa 8 bis 12 €/Monat und reicht für die meisten Self-Hosting-Szenarien völlig aus.
Nein. Non-Root ist die wichtigste Einzelmaßnahme, aber ohne --cap-drop=ALL, no-new-privileges und ein Read-Only-Filesystem bleibt zu viel Angriffsfläche. Erst die Kombination aus User, Capabilities, seccomp und Ressourcen-Limits ergibt eine belastbare Härtung.
Die klassischen Maßnahmen (Non-Root, cap-drop, seccomp, Read-Only) kosten praktisch keine Performance – der Overhead liegt unter 1 Prozent. Nur gVisor und Kata Containers sind spürbar: Rechne mit 5 bis 20 Prozent Leistungsverlust und deutlich höherem RAM-Verbrauch (Kata ab etwa 1 GB pro Container).
Bei userns-remap läuft der Daemon weiterhin als Root, nur die Container-UIDs werden auf unprivilegierte Host-UIDs abgebildet. Beim Rootless Mode läuft der gesamte Daemon als normaler User – das ist die konsequentere und sicherere Variante, hat aber Einschränkungen bei Ports unter 1024 und einigen Storage-Treibern.
Mindestens bei jedem Build in der CI, zusätzlich täglich per Cron für bereits deployte Images. Neue CVEs erscheinen laufend – ein Image, das vor drei Monaten sauber war, kann heute kritische Lücken haben. Automatisiere das mit Trivy und einem Scheduled Job.
Swarm ist nicht unsicher, aber funktional eingefroren. Für einfache Single-Host-Setups mit Docker Compose und den hier gezeigten Maßnahmen brauchst du kein Kubernetes. Sobald du Multi-Tenancy, Pod Security Standards oder automatisches Secret-Rotieren willst, ist Kubernetes mit den Pod Security Admission Levels die bessere Wahl.