Docker Compose: Mehrere Services orchestrieren – Der praktische Guide
Eine einzelne App braucht selten nur einen Container. Web-Server, Datenbank, Cache, Queue – jeder Service läuft im eigenen Container, und Compose bringt sie alle zusammen. Statt fünf docker run-Befehle mit langen Parametern: eine compose.yaml-Datei, ein docker compose up.
Dieser Guide erklärt, wie Compose funktioniert und wie du reale Setups für Web-Apps, Datenbanken und Entwicklungsumgebungen konfigurierst.
Docker Compose v2: Grundlagen
v2 vs. v1: Der Unterschied
# Alt (v1, deprecated, nicht mehr verwenden):
docker-compose up # mit Bindestrich, separate Binary
# Neu (v2, integriert in Docker CLI):
docker compose up # ohne Bindestrich, Plugin
docker compose version
# Docker Compose version v2.40.0
Keine
version:in compose.yaml mehr nötig! Ab Compose v2.40 wird dasversion:-Feld ignoriert. Einfach weglassen.
Installation prüfen
docker compose version
# Docker Compose version v2.40.0
# Falls nicht vorhanden:
sudo apt install docker-compose-plugin # Ubuntu/Debian
sudo dnf install docker-compose-plugin # Fedora
Grundlegende Befehle
# Services starten (im Hintergrund)
docker compose up -d
# Status anzeigen
docker compose ps
# Logs anzeigen
docker compose logs
docker compose logs -f # live verfolgen
docker compose logs -f webapp # nur ein Service
# Services stoppen (Container bleiben erhalten)
docker compose stop
# Services stoppen und entfernen
docker compose down
# Auch Volumes entfernen (VORSICHT: Datenverlust!)
docker compose down -v
# Service neu starten
docker compose restart webapp
# Einzelnen Service starten
docker compose up -d webapp
# Befehl in laufendem Container ausführen
docker compose exec webapp bash
docker compose exec db mysql -u root -p
# Images neu bauen
docker compose build
docker compose up -d --build # neu bauen und starten
compose.yaml-Struktur
# compose.yaml (moderner Name; docker-compose.yaml/yml ist ebenfalls gültig)
services: # Die Container-Definitionen
service-name: # Frei wählbarer Name (wird im Netzwerk verwendet)
image: ... # Fertig-Image aus Registry
build: ... # Oder: eigenes Dockerfile
ports: ...
volumes: ...
environment: ...
depends_on: ...
volumes: # Benannte Volumes (persistente Daten)
daten:
networks: # Eigene Netzwerke
intern:
Minimales Beispiel
services:
web:
image: nginx:1.28-alpine
ports:
- "80:80"
docker compose up -d
# Container läuft, Port 80 erreichbar
Services konfigurieren
image vs. build
services:
# Fertiges Image
db:
image: mariadb:11.6
# Eigenes Dockerfile im aktuellen Verzeichnis
webapp:
build: .
# Eigenes Dockerfile an anderem Pfad
api:
build:
context: ./backend # Verzeichnis mit Dockerfile
dockerfile: Dockerfile.prod # spezifisches Dockerfile
args:
NODE_VERSION: "22" # Build-Argumente
Ports
services:
web:
image: nginx:1.28-alpine
ports:
# HOST:CONTAINER
- "80:80"
- "443:443"
# Nur auf localhost exponieren (nicht von außen erreichbar)
- "127.0.0.1:8080:80"
# Zufälligen Host-Port verwenden
- "8080"
Restart-Policies
services:
webapp:
image: myapp:latest
restart: unless-stopped # Standard für Produktions-Services
# Optionen:
# no : Kein Neustart (Standard)
# always : Immer neu starten
# on-failure : Nur bei Fehler (Exit-Code != 0)
# unless-stopped : Immer außer bei manuellem Stop
Resource-Limits
services:
webapp:
image: myapp:latest
deploy:
resources:
limits:
cpus: "0.5" # Max 50% einer CPU
memory: 512M # Max 512 MB RAM
reservations:
cpus: "0.1"
memory: 128M
Health-Checks
services:
db:
image: mariadb:11.6
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
interval: 10s # Wie oft prüfen?
timeout: 5s # Timeout pro Prüfung
retries: 5 # Wie oft versuchen vor "unhealthy"?
start_period: 30s # Wartezeit beim Start
webapp:
image: myapp:latest
depends_on:
db:
condition: service_healthy # Warten bis db healthy ist
depends_on
services:
webapp:
depends_on:
- db # Einfach: db startet zuerst
- redis
db:
condition: service_healthy # Wartet bis healthy
Volumes: Persistente Daten
Ohne Volumes gehen Daten beim docker compose down verloren!
Benannte Volumes (empfohlen für Datenbanken)
services:
db:
image: mariadb:11.6
volumes:
- db-daten:/var/lib/mysql # benanntes Volume
volumes:
db-daten: # Compose verwaltet das Volume
# Volumes anzeigen
docker volume ls
docker volume inspect projektname_db-daten
Bind Mounts (für Entwicklung)
services:
webapp:
build: .
volumes:
# Lokales Verzeichnis → Container (für Hot-Reload)
- ./src:/app/src
- ./config:/app/config:ro # :ro = read-only
# Einzelne Datei
- ./nginx.conf:/etc/nginx/nginx.conf:ro
tmpfs-Mounts (RAM, kein Disk)
services:
webapp:
image: myapp
tmpfs:
- /tmp
- /run
Netzwerke: Services verbinden
Compose erstellt automatisch ein Standard-Netzwerk. Services im selben Netzwerk können sich über den Service-Namen ansprechen.
services:
webapp:
image: myapp
# Kann "db" und "redis" direkt über Namen ansprechen
db:
image: mariadb:11.6
redis:
image: redis:7-alpine
# In der App: Verbindung über Service-Name (kein IP nötig!)
db_connection = "mysql://db:3306/meinedatenbank"
redis_connection = "redis://redis:6379"
Eigene Netzwerke
services:
nginx:
networks:
- frontend # Zugriff von außen
- backend # Interne Kommunikation
webapp:
networks:
- backend # Nur intern
db:
networks:
- backend # Nur intern
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # Kein Internetzugang
Externes Netzwerk (über andere Compose-Stacks)
networks:
extern-netz:
external: true # Muss vorher existieren
name: mein-netz
Umgebungsvariablen und .env
.env-Datei (automatisch geladen)
# .env im selben Verzeichnis wie compose.yaml
nano .env
# .env - NIEMALS in Git einchecken!
MARIADB_ROOT_PASSWORD=sicheres-passwort-123
MARIADB_DATABASE=meineapp
MARIADB_USER=appuser
MARIADB_PASSWORD=app-passwort-456
APP_SECRET_KEY=supersecretkey
APP_DEBUG=false
# compose.yaml: Variablen referenzieren
services:
db:
image: mariadb:11.6
environment:
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
MARIADB_DATABASE: ${MARIADB_DATABASE}
MARIADB_USER: ${MARIADB_USER}
MARIADB_PASSWORD: ${MARIADB_PASSWORD}
env_file: Eigene Env-Datei
services:
webapp:
env_file:
- .env # Standard .env
- .env.local # Lokale Überschreibungen (optional)
Unterschied: environment vs. env_file
services:
webapp:
# Direkt in compose.yaml (für Nicht-Secrets):
environment:
NODE_ENV: production
PORT: "3000"
# Variable aus .env übernehmen:
DATABASE_URL: ${DATABASE_URL}
# Aus Datei (für Secrets empfohlen):
env_file:
- .env
Praxisbeispiele
Beispiel 1: Laravel + MariaDB + Redis + Nginx
# compose.yaml für Laravel-App
services:
nginx:
image: nginx:1.28-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- app-daten:/var/www/html:ro
depends_on:
- php-fpm
restart: unless-stopped
php-fpm:
build:
context: .
dockerfile: Dockerfile
volumes:
- app-daten:/var/www/html
- ./.env:/var/www/html/.env:ro
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
db:
image: mariadb:11.6
volumes:
- db-daten:/var/lib/mysql
environment:
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
MARIADB_DATABASE: ${MARIADB_DATABASE}
MARIADB_USER: ${MARIADB_USER}
MARIADB_PASSWORD: ${MARIADB_PASSWORD}
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis-daten:/data
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
restart: unless-stopped
volumes:
app-daten:
db-daten:
redis-daten:
Beispiel 2: Entwicklungsumgebung (Node.js + PostgreSQL)
# compose.yaml für Entwicklung (mit Hot-Reload)
services:
api:
build:
context: .
target: development # Multi-Stage Dockerfile
ports:
- "3000:3000"
- "9229:9229" # Node.js Debugger
volumes:
- .:/app # Lokaler Code im Container
- /app/node_modules # node_modules NICHT überschreiben
environment:
NODE_ENV: development
DATABASE_URL: postgresql://pguser:pgpass@db:5432/devdb
command: npm run dev # Hot-Reload-Befehl
depends_on:
- db
db:
image: postgres:17-alpine
ports:
- "5432:5432" # Direkt zugänglich für DB-Tools
volumes:
- pg-daten:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
environment:
POSTGRES_DB: devdb
POSTGRES_USER: pguser
POSTGRES_PASSWORD: pgpass
adminer:
image: adminer:latest # Web-UI für DB (nur Entwicklung)
ports:
- "8080:8080"
volumes:
pg-daten:
Beispiel 3: Monitoring-Stack (Grafana + Prometheus)
services:
prometheus:
image: prom/prometheus:v3.1.0
ports:
- "127.0.0.1:9090:9090" # Nur localhost
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-daten:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=15d'
restart: unless-stopped
grafana:
image: grafana/grafana:11.4.0
ports:
- "3000:3000"
volumes:
- grafana-daten:/var/lib/grafana
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
GF_USERS_ALLOW_SIGN_UP: "false"
depends_on:
- prometheus
restart: unless-stopped
node-exporter:
image: prom/node-exporter:v1.8.2
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.rootfs=/rootfs'
- '--path.sysfs=/host/sys'
network_mode: host # Direkter Host-Netzwerk-Zugriff
pid: host
restart: unless-stopped
volumes:
prometheus-daten:
grafana-daten:
Profiles und Overrides
Profiles: Optionale Services
services:
webapp:
image: myapp
# Startet immer
db:
image: mariadb:11.6
# Startet immer
adminer:
image: adminer
profiles:
- dev # Startet NUR mit --profile dev
# Standard: webapp + db
docker compose up -d
# Mit adminer (nur für Entwicklung)
docker compose --profile dev up -d
Overrides: dev vs. prod
# compose.yaml (Basis):
services:
webapp:
image: myapp:${TAG:-latest}
restart: unless-stopped
# compose.override.yaml (wird automatisch geladen, für Entwicklung):
services:
webapp:
build: .
volumes:
- .:/app # Hot-Reload für Entwicklung
environment:
DEBUG: "true"
# compose.prod.yaml (explizit angeben, für Produktion):
services:
webapp:
deploy:
resources:
limits:
memory: 512M
# Entwicklung (automatisch compose.yaml + compose.override.yaml)
docker compose up -d
# Produktion (explizit angeben)
docker compose -f compose.yaml -f compose.prod.yaml up -d
Häufige Probleme
Error: No such service
# Tipp-Fehler im Service-Namen?
docker compose ps # Alle Services anzeigen
docker compose config # Gesamte Config anzeigen
# Compose-Datei im falschen Verzeichnis?
ls compose.yaml docker-compose.yaml docker-compose.yml
Service startet immer neu (Restarting)
# Logs prüfen
docker compose logs webapp
# Exit-Code prüfen
docker compose ps
# STATUS: Restarting (1) 3 seconds ago ← Exit-Code 1 = Fehler
# Container interaktiv starten (umgeht restart-policy)
docker compose run --rm webapp bash
Port bereits belegt
# Fehler: Bind for 0.0.0.0:80 failed: port is already allocated
# Wer nutzt Port 80?
sudo ss -tlnp | grep :80
sudo lsof -i :80
# Prozess stoppen oder anderen Port wählen
Volume-Berechtigungsprobleme
# Container läuft als anderer User als Volume-Besitzer
docker compose exec webapp id
# uid=1000(node) ...
ls -la $(docker volume inspect projektname_app-daten --format '{{.Mountpoint}}')
# Korrektur im Dockerfile:
# RUN chown -R node:node /app
# USER node
depends_on wartet nicht richtig
# depends_on wartet nur auf "started", nicht auf "ready"
# Lösung: healthcheck + condition: service_healthy
services:
db:
healthcheck:
test: ["CMD", "mariadb-admin", "ping"]
...
webapp:
depends_on:
db:
condition: service_healthy
Fazit
Docker Compose macht Multi-Service-Setups reproduzierbar und portabel. Die wichtigsten Punkte:
- Kein
version:mehr in compose.yaml (ab v2.40) - Services sprechen sich über Namen an (
db,redis) – kein Hardcoding von IPs - Benannte Volumes für persistente Daten (Datenbanken, Uploads)
.env-Datei für Secrets – nie in Git einchecken- Overrides für dev/prod trennen Entwicklungs- von Produktionsconfig
depends_onmitcondition: service_healthyfür zuverlässige Startreihenfolge