Die Ausführung von Multi-Container-Anwendungen in der Produktion erfordert mehr als eine einfache docker-compose.yml-Datei. Obwohl Docker Compose häufig mit der lokalen Entwicklung assoziiert wird, ist es ein leistungsfähiges Werkzeug für die Bereitstellung von Produktions-Workloads auf Einzelhost- und kleinen Cluster-Umgebungen, wenn es korrekt konfiguriert ist. Diese Anleitung führt Sie durch die Härtung Ihres Docker Compose Setups für die Produktion mit Gesundheitsprüfungen, Neustartrichtlinien, Ressourcenlimits, Geheimnisverwaltung, zentralisierter Protokollierung und Nginx Reverse Proxy mit SSL-Terminierung.
Voraussetzungen
- Linux-Server mit Docker Engine 24+ und Docker Compose v2 installiert
- Domainname, der auf die öffentliche IP-Adresse Ihres Servers zeigt
- Grundlegende Vertrautheit mit Docker-Konzepten (Images, Container, Volumes, Netzwerke)
- SSL-Zertifikat oder Bereitschaft, Let’s Encrypt mit Certbot zu verwenden
- SSH-Zugang zu Ihrem Produktionsserver
- Mindestens 2 GB RAM und 2 CPU-Kerne verfügbar für Ihren Stack
Docker Compose Produktionskonfiguration
Eine produktionsbereite docker-compose.yml unterscheidet sich erheblich von einer Entwicklungskonfiguration. Sie benötigen explizite Neustartrichtlinien, Gesundheitsprüfungen, Ressourcenbeschränkungen und wo möglich schreibgeschützte Dateisysteme.
Neustartrichtlinien
Neustartrichtlinien stellen sicher, dass Ihre Container sich automatisch von Abstürzen, OOM-Kills oder Host-Neustarts erholen:
services:
web:
image: myapp:latest
restart: unless-stopped
deploy:
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
Verwenden Sie unless-stopped für die meisten Dienste. Dies startet Container nach Abstürzen und Host-Neustarts neu, respektiert aber manuelle Stopps. Für kritische Dienste, die immer laufen müssen, verwenden Sie always. Vermeiden Sie no in der Produktion — es lässt abgestürzte Container tot, bis manuell eingegriffen wird.
Gesundheitsprüfungen
Gesundheitsprüfungen ermöglichen es Docker zu überwachen, ob Ihre Anwendung tatsächlich funktioniert, nicht nur ob der Prozess läuft:
services:
api:
image: myapp-api:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
Die start_period gibt Ihrer Anwendung Zeit zur Initialisierung, bevor Docker beginnt, fehlgeschlagene Prüfungen zu zählen. Setzen Sie sie höher als die durchschnittliche Startzeit Ihrer Anwendung. Verwenden Sie spezifische Gesundheitsendpunkte, anstatt nur zu prüfen, ob ein Port offen ist — ein Prozess kann auf einem Port lauschen, während er sich in einem fehlerhaften Zustand befindet.
Ressourcenlimits
Ohne Ressourcenlimits kann ein einzelner außer Kontrolle geratener Container den gesamten Systemspeicher verbrauchen und alles zum Absturz bringen:
services:
api:
image: myapp-api:latest
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
Setzen Sie limits auf das Maximum, das ein Dienst jemals verwenden sollte, und reservations, um Mindestressourcen zu garantieren. Überwachen Sie Ihre Container eine Woche lang mit docker stats, bevor Sie endgültige Limits festlegen. Zu enge Limits verursachen OOM-Kills und beeinträchtigte Leistung.
Schreibgeschützte Dateisysteme
Minimieren Sie die Angriffsfläche, indem Sie Container mit schreibgeschützten Root-Dateisystemen ausführen:
services:
api:
image: myapp-api:latest
read_only: true
tmpfs:
- /tmp
- /var/run
volumes:
- app-data:/app/data
Dies verhindert, dass schädliche Prozesse in das Container-Dateisystem schreiben. Verwenden Sie tmpfs für Verzeichnisse, die temporären Schreibzugriff benötigen, und benannte Volumes für persistente Daten.
Geheimnisse und Umgebungsverwaltung
Fest codierte Zugangsdaten in Ihrer Compose-Datei sind ein Sicherheitsrisiko. Docker Compose unterstützt mehrere Ansätze zur Geheimnisverwaltung.
Verwendung von .env-Dateien
Erstellen Sie eine .env-Datei neben Ihrer Compose-Datei:
# .env - Diese Datei NIEMALS committen
POSTGRES_PASSWORD=your_secure_password_here
API_SECRET_KEY=another_secure_value
REDIS_PASSWORD=redis_password_here
Referenzieren Sie diese in Ihrer Compose-Datei:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
Fügen Sie .env sofort zu Ihrer .gitignore hinzu. Für CI/CD-Pipelines injizieren Sie Umgebungsvariablen aus Ihrem Geheimnis-Manager (GitHub Secrets, AWS Secrets Manager oder HashiCorp Vault).
Docker Secrets
Für eine sicherere Handhabung verwenden Sie Docker Secrets, die als Dateien anstatt als Umgebungsvariablen eingebunden werden:
secrets:
db_password:
file: ./secrets/db_password.txt
services:
db:
image: postgres:16
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
Viele offizielle Docker-Images unterstützen die _FILE-Suffix-Konvention und lesen das Geheimnis aus einer eingebundenen Datei anstatt aus einer Umgebungsvariable. Dies hält Zugangsdaten aus der docker inspect-Ausgabe und den Prozess-Umgebungsauflistungen heraus.
Reverse Proxy und SSL mit Nginx
Anwendungsports direkt freizugeben ist unsicher und unflexibel. Verwenden Sie Nginx als Reverse Proxy mit SSL-Terminierung:
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- certbot-webroot:/var/www/certbot:ro
- certbot-certs:/etc/letsencrypt:ro
depends_on:
api:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
certbot:
image: certbot/certbot
volumes:
- certbot-webroot:/var/www/certbot
- certbot-certs:/etc/letsencrypt
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'"
volumes:
certbot-webroot:
certbot-certs:
Die Nginx-Konfiguration sollte auf Ihre internen Dienste weiterleiten:
upstream api_backend {
server api:8080;
}
server {
listen 80;
server_name example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://api_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Beachten Sie, dass nur der Nginx-Dienst Ports zum Host freigibt. Alle anderen Dienste kommunizieren über das interne Docker-Netzwerk, was die Angriffsfläche erheblich reduziert.
Protokollierung und Überwachung
Produktions-Deployments benötigen strukturierte Protokollierung mit Rotation, um das Volllaufen der Festplatte zu verhindern.
JSON-Datei-Protokollierung mit Rotation
services:
api:
image: myapp-api:latest
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
tag: "{{.Name}}"
Ohne max-size und max-file wachsen Docker-Logs unbegrenzt und können Ihre Festplatte füllen. Der json-file-Treiber ist der Standard und funktioniert für die meisten Setups gut. Für Multi-Host-Umgebungen sollten Sie Logs an ein zentralisiertes System weiterleiten.
Weiterleitung an externe Systeme
Für Produktions-Observability leiten Sie Logs an einen Aggregationsdienst weiter:
services:
api:
image: myapp-api:latest
logging:
driver: syslog
options:
syslog-address: "tcp://logserver:514"
tag: "myapp-api"
Alternativen sind die fluentd- und gelf-Treiber für ELK- oder Graylog-Stacks. Unabhängig von Ihrer Wahl sollten Sie immer Log-Rotation auf Docker-Daemon-Ebene als Sicherheitsnetz in /etc/docker/daemon.json konfigurieren:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Grundlegende Überwachung
Fügen Sie Container-Überwachung mit einem leichtgewichtigen Stack hinzu:
services:
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
deploy:
resources:
limits:
memory: 256M
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
deploy:
resources:
limits:
memory: 128M
Vergleich: Docker Compose vs Kubernetes vs Docker Swarm
| Merkmal | Docker Compose | Kubernetes | Docker Swarm |
|---|---|---|---|
| Setup-Komplexität | Niedrig | Hoch | Mittel |
| Multi-Host-Unterstützung | Nein (Einzelhost) | Ja (Cluster) | Ja (Cluster) |
| Automatische Skalierung | Nein | Ja (HPA) | Eingeschränkt |
| Service Discovery | DNS-basiert | DNS + Ingress | DNS-basiert |
| Geheimnisverwaltung | Dateibasiert | Integriert (etcd) | Integriert |
| Rolling Updates | Manuell | Integriert | Integriert |
| Gesundheitsprüfungen | Ja | Ja (Liveness/Readiness) | Ja |
| Ressourcenlimits | Ja | Ja (Requests/Limits) | Ja |
| Lernkurve | Flach | Steil | Moderat |
| Ideal für | Einzelhost, kleine Teams | Großmaßstab, Multi-Team | Kleine Cluster |
Docker Compose ist die richtige Wahl, wenn Sie auf einem einzelnen Server oder einer kleinen Anzahl von Servern bereitstellen, Ihr Team klein ist und Sie keine automatische Skalierung benötigen. Viele erfolgreiche SaaS-Produkte laufen vollständig mit Docker Compose in der Produktion.
Praxisszenario
Sie stellen eine Webanwendung bereit, die aus einer Node.js-API, einer PostgreSQL-Datenbank, einem Redis-Cache und einem Nginx Reverse Proxy besteht. Der Server hat 4 GB RAM und 2 CPU-Kerne.
Hier ist die vollständige Produktions-Compose-Datei:
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- certbot-certs:/etc/letsencrypt:ro
depends_on:
api:
condition: service_healthy
restart: unless-stopped
deploy:
resources:
limits:
cpus: "0.25"
memory: 64M
logging:
driver: json-file
options:
max-size: "5m"
max-file: "3"
api:
image: myapp-api:1.2.3
env_file: .env
secrets:
- db_password
- api_key
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
db:
image: postgres:16-alpine
volumes:
- pgdata:/var/lib/postgresql/data
secrets:
- db_password
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: myapp
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
cpus: "0.5"
memory: 1G
reservations:
memory: 256M
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
redis:
image: redis:7-alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redisdata:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 3
deploy:
resources:
limits:
cpus: "0.25"
memory: 256M
logging:
driver: json-file
options:
max-size: "5m"
max-file: "3"
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
volumes:
pgdata:
redisdata:
certbot-certs:
Bereitstellen mit:
docker compose -f docker-compose.prod.yml up -d
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs --tail=50
Diese Konfiguration weist ungefähr 1,8 GB der verfügbaren 4 GB zu und lässt Spielraum für das Betriebssystem und Lastspitzen.
Stolperfallen und Sonderfälle
Die Startreihenfolge von Containern garantiert keine Bereitschaft. Die alleinige Verwendung von depends_on wartet nur darauf, dass der Container startet, nicht darauf, dass der Dienst darin bereit ist. Kombinieren Sie depends_on immer mit condition: service_healthy und ordnungsgemäßen Gesundheitsprüfungen.
Verwaiste Container sammeln sich an. Wenn Sie einen Dienst aus Ihrer Compose-Datei entfernen, läuft der alte Container weiter. Führen Sie immer docker compose up -d --remove-orphans aus, um veraltete Container zu bereinigen.
Volume-Berechtigungen verursachen stille Fehler. Wenn Ihr Container als Nicht-Root-Benutzer läuft, ist das eingebundene Volume möglicherweise nicht beschreibbar. Erstellen Sie Volumes vorab mit der richtigen Eigentümerschaft oder verwenden Sie ein Init-Container-Muster.
Docker Compose Rebuilds ziehen standardmäßig keine neuen Images. Das Ausführen von docker compose up -d verwendet gecachte Images wieder. Verwenden Sie docker compose pull && docker compose up -d, um sicherzustellen, dass Sie die neueste Version bereitstellen.
Die Variablenexpansion der .env-Datei kann zu Konflikten führen. Docker Compose interpoliert ${VAR} in der Compose-Datei, bevor es an den Container übergeben wird. Wenn Ihre Anwendungskonfiguration $-Zeichen verwendet, escapen Sie diese mit $$ in der Compose-Datei.
Log-Rotation muss pro Dienst und global konfiguriert werden. Log-Optionen auf Dienstebene überschreiben nicht die Daemon-Standards für andere Container. Konfigurieren Sie beides für vollständige Abdeckung.
Bridge-Netzwerke haben DNS-Auflösungsprobleme mit Unterstrichen. Dienstnamen mit Unterstrichen werden in älteren Docker-Versionen möglicherweise nicht korrekt aufgelöst. Verwenden Sie Bindestriche in Dienstnamen für maximale Kompatibilität.
Zusammenfassung
- Verwenden Sie
restart: unless-stoppedund Gesundheitsprüfungen für jeden Produktionsdienst - Setzen Sie Speicher- und CPU-Limits mit
deploy.resources, um außer Kontrolle geratene Container zu verhindern - Verwalten Sie Geheimnisse mit Docker Secrets oder
.env-Dateien — codieren Sie niemals Zugangsdaten fest ein - Platzieren Sie Nginx als Reverse Proxy vor Anwendungsdiensten mit SSL-Terminierung
- Konfigurieren Sie Log-Rotation mit
max-sizeundmax-file, um das Volllaufen der Festplatte zu verhindern - Verwenden Sie
depends_onmitcondition: service_healthyfür zuverlässige Startreihenfolge - Führen Sie
docker compose up -d --remove-orphansaus, um veraltete Container bei jedem Deployment zu bereinigen - Docker Compose ist produktionsbereit für Einzelhost- und Kleinteam-Deployments