Docker Compose Basics: Mehrere Container orchestrieren
Wie du mehrere Container zu einem Stack zusammensteckst — Services, Netzwerke, Volumes und Profile. Mit einem voll lauffähigen Beispiel aus App + Datenbank + Cache.
Zuletzt aktualisiert: 11. Mai 2026
Mit docker run bekommst du einen Container ans Laufen. Sobald
deine Anwendung aus App + Datenbank + Cache + Worker besteht, wird das
schnell unhandlich. Docker Compose beschreibt den gesamten Stack
in einer YAML-Datei und startet ihn mit einem einzigen Befehl.
Voraussetzung: Du kennst die Konzepte aus dem Kurs Docker Basics (Image, Container, Volume, Netzwerk).
Lernziele
- Du verstehst Services, Netzwerke und Volumes in Compose
- Du schreibst eine
compose.yamlfür einen realistischen Stack - Du kennst die wichtigsten Subkommandos
- Du nutzt
profiles,depends_onund Healthchecks sinnvoll
Das Bild im Kopf
flowchart LR
subgraph stack["docker compose up"]
W[web<br/><small>nginx</small>]
A[api<br/><small>node</small>]
D[db<br/><small>postgres</small>]
C[cache<br/><small>redis</small>]
W -- HTTP --> A
A -- SQL --> D
A -- cache --> C
end
User[Browser] -- :8080 --> WEin Service ist ein Container (oder mehrere Replikate desselben Images). Alle Services in derselben Compose-Datei landen automatisch im gleichen Netzwerk und erreichen sich über den Service-Namen.
Dein erstes Compose-File
Lege im Projekt eine Datei compose.yaml an:
services:
web:
image: nginx:alpine
ports:
- "8080:80"
depends_on:
- api
api:
build: ./api
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 5
cache:
image: redis:7-alpine
volumes:
db-data:Starten:
docker compose up -d # alles im Hintergrund starten
docker compose ps # was läuft?
docker compose logs -f api # Logs eines Services
docker compose down # alles stoppen + entfernen
docker compose down -v # zusätzlich Volumes löschenModern ist
docker compose(mit Leerzeichen, Plugin). Das altedocker-compose(Bindestrich, separates Binary) ist deprecated.
Was Compose automatisch tut
| Konzept | Automatik |
|---|---|
| Netzwerk | Ein Default-Netzwerk pro Compose-Projekt |
| DNS | Services sind über ihren Namen erreichbar (db, cache, ...) |
| Projektname | Vom Ordnernamen abgeleitet; mit -p überschreibbar |
| Restart | Mit restart: unless-stopped automatisch wieder hoch |
Im Beispiel oben spricht api zu db:5432 und cache:6379 — als wären
es lokale Hosts. Compose macht das DNS-Mapping für dich.
build: vs image:
Ein Service kann entweder ein bestehendes Image nutzen oder lokal gebaut werden:
services:
api:
build:
context: ./api # Verzeichnis mit Dockerfile
dockerfile: Dockerfile # optional, default ist Dockerfile
args:
NODE_VERSION: "20"
image: myapp/api:dev # optional: Name fürs gebaute Imagedocker compose build # nur bauen
docker compose up --build # bauen + startenVolumes & Bind Mounts
services:
api:
build: ./api
volumes:
- ./api/src:/app/src # Bind Mount (Dev: Hot-Reload)
- api-node-modules:/app/node_modules # Named Volume (keep host's modules out)
db:
image: postgres:16-alpine
volumes:
- db-data:/var/lib/postgresql/data # persistente DB-Daten
volumes:
db-data:
api-node-modules:Der zweite Mount im api-Service ist ein häufiger Trick: damit
überschreibt das Host-node_modules (oft inkompatibel, falsche
Plattform) nicht das Container-node_modules.
Umgebungsvariablen — drei Wege
services:
api:
image: myapp
# 1. Inline
environment:
NODE_ENV: production
PORT: "3000"
# 2. Aus .env-Datei laden
env_file:
- .env
- .env.local
# 3. Vom Host übernehmen (Substitution mit ${VAR})
environment:
API_KEY: ${API_KEY}Compose lädt automatisch eine .env im selben Ordner für die
${...}-Substitution in der YAML-Datei. Das ist nicht dasselbe wie
env_file: — das gibt Variablen in den Container, ${...} ersetzt
sie vor dem Parsen des YAML.
depends_on & Healthchecks
services:
api:
depends_on:
db:
condition: service_healthy # wartet auf Healthcheck
cache:
condition: service_started # nur warten, dass es startet
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s| Condition | Bedeutung |
|---|---|
service_started |
Container läuft (sagt nichts über Bereitschaft) |
service_healthy |
Healthcheck ist healthy |
service_completed_successfully |
Service ist mit Exit 0 fertig (Init-Jobs) |
Wichtig: depends_on ohne condition: service_healthy wartet
nicht darauf, dass eine App wirklich bereit ist — nur darauf, dass
sie startet. Ohne Healthcheck musst du in deiner App selbst
retry-fähig sein.
Profiles — optionale Services
Nicht alle Services sollen immer hochgehen. Beispiel: ein optionaler Mailserver für lokale Tests.
services:
api:
build: ./api
mailhog:
image: mailhog/mailhog
profiles: ["dev"]
ports:
- "8025:8025"
loadgen:
image: myorg/loadgen
profiles: ["bench"]docker compose up # nur api
docker compose --profile dev up # api + mailhog
docker compose --profile bench up loadgen # nur loadgenOverrides — Dev vs Prod
Compose liest standardmäßig zwei Dateien zusammen:
compose.yaml(Basis)compose.override.yaml(Dev-Overrides, optional)
Du kannst aber auch explizit kombinieren:
docker compose \
-f compose.yaml \
-f compose.prod.yaml \
up -dSpätere Dateien überschreiben Felder aus früheren. Klassisches Muster:
# compose.yaml — gemeinsame Basis
services:
api:
image: myapp/api
environment:
NODE_ENV: production# compose.override.yaml — automatisch für Dev geladen
services:
api:
build: ./api
volumes:
- ./api/src:/app/src
environment:
NODE_ENV: developmentSubkommandos im Alltag
docker compose up -d # Stack starten
docker compose ps # Status
docker compose logs -f api # Logs eines Service
docker compose exec api sh # Shell im laufenden Container
docker compose run --rm api npm test # einmalig was ausführen
docker compose restart api # einzelnen Service neu starten
docker compose pull # neueste Images holen
docker compose down # stoppen + Netzwerk weg
docker compose down -v # zusätzlich Volumes weg
runvsexec:execläuft im bestehenden Container,runstartet einen neuen. Für Migrationen und Einmal-Tasks istrun --rmdie richtige Wahl.
Häufige Stolperfallen
„Mein Service erreicht den anderen nicht"
- Verwendest du Service-Namen als Host, nicht
localhost?localhostim Container ist der Container selbst, nicht der Host. - Sind beide Services im gleichen Compose-Projekt? Compose
erkennt das am Ordnernamen —
docker compose -psetzt ihn explizit.
„Datenbank ist leer nach down"
Hast du down -v benutzt? Das löscht Volumes. Für reines
Stoppen reicht down (ohne -v).
„Port ist schon belegt"
Ein anderer Compose-Stack oder ein lokaler Prozess hört auf dem Port.
docker compose ps und lsof -i :8080 zeigen, wer.
„Änderungen am Dockerfile greifen nicht"
docker compose up --build # explizit neu bauen
docker compose build --no-cache # ohne Cache neu bauenPraxis-Übung
Im Ordner compose-demo/:
compose.yaml:
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
depends_on:
- api
api:
image: hashicorp/http-echo
command: ["-text", "Hallo von der API"]
expose:
- "5678"html/index.html:
<!doctype html>
<title>Compose-Demo</title>
<h1>Hallo aus nginx</h1>
<p>Die API ist im Compose-Netz unter <code>http://api:5678</code>.</p>docker compose up -d
curl localhost:8080
docker compose exec web wget -qO- http://api:5678
docker compose downWenn der zweite curl/wget funktioniert, hast du gerade
Compose-DNS in Aktion gesehen — web erreicht api per Name, ohne
dass du irgendetwas explizit eingerichtet hast.
Weiterführendes
- Offizielle Docs: https://docs.docker.com/compose/
- Compose-Spec (das YAML-Schema): https://github.com/compose-spec/compose-spec
- Best Practices: https://docs.docker.com/compose/production/
- Awesome Compose (Beispiel-Stacks): https://github.com/docker/awesome-compose
Compose ist Docker, aber deklarativ. Wer den Stack beschreiben kann, muss ihn nicht mehr von Hand zusammenklicken. 🧩