Docker Compose: Multi-Container-Stacks aufbauen
Mit Docker Compose beschreibst du einen kompletten Multi-Container-Stack deklarativ in einer compose.yaml und steuerst ihn mit wenigen Befehlen. Diese Anleitung zeigt dir Compose-Datei, Kernbefehle, .env-Variablen, einen Beispiel-Stack aus Web-App und Datenbank sowie den Update-Workflow.

Mit Docker Compose beschreibst du einen kompletten Multi-Container-Stack deklarativ in einer einzigen Datei und startest, stoppst und aktualisierst ihn mit wenigen Befehlen. Statt mehrere docker run-Aufrufe von Hand zu jonglieren, legst du Services, Images, Ports, Volumes, Variablen, Netzwerke und Abhängigkeiten in einer compose.yaml fest und reproduzierst den Stack jederzeit identisch. Diese Anleitung richtet sich an Admins im Mittelstand, die Self-Hosting betreiben oder Dienste sauber containerisieren wollen. Du lernst den Aufbau der Compose-Datei, die wichtigsten Befehle, die .env-Datei, einen vollständigen Beispiel-Stack aus Web-App und Datenbank, Healthchecks und einen sauberen Update-Workflow. Die Docker-Installation selbst ist nicht Thema dieser Anleitung – die behandeln wir separat.
Kurzfassung: Lege eine compose.yaml an (ohne obsoletes version:-Feld), definiere unter services: deine Container mit image, ports, volumes, environment, restart, depends_on und networks. Named Volumes und eigene Netzwerke deklarierst du zusätzlich in den Top-Level-Blöcken volumes: und networks:. Geheimnisse und Konfig legst du in eine .env-Datei neben der Compose-Datei und reichst sie per ${VAR} durch. Gestartet wird mit docker compose up -d, Status mit docker compose ps, Logs mit docker compose logs -f. Aktualisiert wird sauber mit docker compose pull gefolgt von docker compose up -d. docker compose down entfernt Container und Netze, lässt Named Volumes aber stehen – erst down -v löscht die Daten.
Voraussetzungen
Bevor du loslegst, sollten diese Grundlagen auf deinem Linux-Server (Debian 12 oder Ubuntu 24.04 LTS) stehen:
- Docker Engine inkl. Compose-Plugin installiert: Compose v2 ist das offizielle Plugin und wird als
docker compose(mit Leerzeichen) aufgerufen. Das altedocker-compose(mit Bindestrich, v1, Python) ist End-of-Life – nutze es nicht. Die Installation behandelt unsere separate Anleitung. - Shell-Zugang mit ausreichenden Rechten: entweder als Mitglied der Gruppe
dockeroder persudo. - Ein Arbeitsverzeichnis je Stack: üblich ist ein Pfad wie
/opt/stacks/DEIN-STACK/. Dorthin gehörencompose.yamlund.env. - Grundverständnis von Volumes und Netzwerken: hilfreich, aber nicht zwingend – die Kernkonzepte streifen wir hier.
Prüfe zunächst, dass das Plugin verfügbar ist:
docker compose version
# Beispiel-Ausgabe: Docker Compose version v2.x.xSchritt 1: Aufbau der compose.yaml verstehen
Die compose.yaml ist eine YAML-Datei, in der du deinen Stack deklarativ beschreibst. Der bevorzugte Dateiname ist compose.yaml; gelesen werden auch compose.yml, docker-compose.yaml und docker-compose.yml. Die drei wichtigsten Top-Level-Blöcke sind services: (deine Container), volumes: (persistente Datenträger) und networks: (eigene Netze).
Wichtig: Das früher übliche Top-Level-Feld version: ist obsolet. Compose v2 ignoriert es, validiert immer gegen das aktuelle Schema und gibt bei Verwendung eine Warnung aus. Lass es ersatzlos weg.
Diese Felder eines Service brauchst du am häufigsten:
FeldBedeutung
image
das zu verwendende Container-Image, z. B. postgres:16
container_name
fester Name statt des generierten; praktisch fürs Wiedererkennen, aber pro Host eindeutig
ports
Port-Mapping in der Kurzsyntax [HOST:]CONTAINER[/PROTOCOL], z. B. "8080:80"
volumes
persistente Daten als Named Volume name:/pfad oder Bind-Mount /host:/container
environment
Umgebungsvariablen IM Container
restart
Neustart-Verhalten, für Self-Hosting meist unless-stopped
depends_on
Startreihenfolge und optional Warten auf Bereitschaft
networks
Zuordnung zu eigenen Netzwerken
healthcheck
Bereitschaftsprüfung des Dienstes
Ein minimaler Single-Service sieht so aus:
services:
web:
image: nginx:1.27
container_name: web
ports:
- "127.0.0.1:8080:80"
restart: unless-stoppedrestart-Werte richtig setzen
Die gängigen Werte sind no (Default), always, on-failure (optional mit Limit, z. B. on-failure:3) und unless-stopped. Für dauerhaft laufende Self-Hosting-Dienste ist unless-stopped die beste Wahl: Der Container startet nach Neustart oder Absturz wieder, bleibt aber gestoppt, wenn du ihn bewusst angehalten hast.
Achtung:restart: noohne Anführungszeichen wird in YAML als Booleanfalseinterpretiert. Schreibe es immer als String"no".
Schritt 2: Variablen mit der .env-Datei steuern
Geheimnisse wie Datenbank-Passwörter und umgebungsabhängige Werte gehören nicht hart in die Compose-Datei. Lege sie in eine .env-Datei, die standardmäßig neben der compose.yaml liegt. Compose liest sie automatisch und ersetzt damit Platzhalter in der Compose-Datei (sogenannte Interpolation), bevor Docker sie verarbeitet.
# .env – liegt neben der compose.yaml
POSTGRES_PASSWORD=ein-sehr-langes-zufaelliges-passwort
POSTGRES_DB=appdb
POSTGRES_USER=appuser
WEB_PORT=8080In der Compose-Datei greifst du mit der Interpolations-Syntax darauf zu:
SyntaxVerhalten
${VAR}
setzt den Wert von VAR ein
${VAR:-default}
nimmt default, wenn VAR leer oder nicht gesetzt ist
${VAR-default}
nimmt default nur, wenn VAR gar nicht gesetzt ist
${VAR:?fehler}
bricht mit Fehlermeldung ab, wenn VAR leer oder nicht gesetzt ist
${VAR:+ersatz}
nimmt ersatz, wenn VAR gesetzt und nicht leer ist
Wichtig ist der Unterschied zwischen zwei Dingen, die leicht verwechselt werden:
- Interpolation per
.env: ersetzt${VAR}-Platzhalter in der Compose-Datei. environment:/env_file:: setzt Variablen im laufenden Container.
Eine Variable nur in der .env zu definieren reicht also nicht, damit sie im Container ankommt – du musst sie über ${VAR} in einem environment:-Eintrag durchreichen oder eine env_file: angeben. Zur Precedence: Variablen aus der echten Shell-Umgebung haben Vorrang vor Werten aus der .env. Single-Quotes in der .env sind literal, dort findet keine Interpolation statt.
Die.enventhält Klartext-Geheimnisse. Schränke die Dateirechte ein (chmod 600 .env) und nimm sie in die.gitignoreauf, damit sie nicht in der Versionsverwaltung landet.
Schritt 3: Named Volumes und eigene Netzwerke deklarieren
Damit Daten einen down überleben, brauchst du Named Volumes. Sie werden von Docker verwaltet und bleiben bei docker compose down erhalten – erst down -v löscht sie. Ein Named Volume musst du sowohl beim Service als auch zusätzlich im Top-Level-volumes:-Block deklarieren, sonst bricht Compose mit volume ... is undefined ab.
services:
db:
image: postgres:16
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:Alternativ gibt es Bind-Mounts (/host/pfad:/container/pfad, optional mit :ro für read-only). Relative Quellpfade beziehen sich auf das Verzeichnis der Compose-Datei. Für Datenbankdaten sind Named Volumes meist die bessere Wahl, für eigene Konfigdateien Bind-Mounts.
Ein eigenes Netzwerk deklarierst du im Top-Level-networks:-Block und weist es den Services zu. Container im selben Netz erreichen sich über den Servicenamen als DNS-Hostname – die Web-App spricht die Datenbank also als db an, ganz ohne IP-Adressen. Compose legt ohnehin automatisch ein projekteigenes Default-Netz an; ein explizites Netz macht die Struktur aber klar und erlaubt feinere Trennung.
services:
web:
networks:
- appnet
db:
networks:
- appnet
networks:
appnet:Schritt 4: Vollständigen Beispiel-Stack zusammenbauen
Jetzt setzt du alles zu einem produktionsnahen Stack zusammen: eine Web-App, die eine PostgreSQL-Datenbank nutzt, mit Named Volume, eigenem Netzwerk, Healthcheck und Variablen aus der .env. Lege die folgende Datei als /opt/stacks/app/compose.yaml an:
services:
db:
image: postgres:16
container_name: app-db
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD fehlt in .env}
volumes:
- db_data:/var/lib/postgresql/data
networks:
- appnet
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
web:
image: nginx:1.27
container_name: app-web
restart: unless-stopped
ports:
- "127.0.0.1:${WEB_PORT:-8080}:80"
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy
networks:
- appnet
volumes:
db_data:
networks:
appnet:Was hier zusammenspielt:
- Der Datenbank-Container speichert seine Daten im Named Volume
db_dataund überlebt damit jedendownohne-v. - Alle Geheimnisse und der Port kommen per
${VAR}aus der.env;${POSTGRES_PASSWORD:?...}bricht mit klarer Meldung ab, falls das Passwort fehlt. - Die Web-App erreicht die Datenbank über den Hostnamen
dbim gemeinsamen Netzappnet. - Der Port ist mit
127.0.0.1bewusst nur an den Loopback gebunden – siehe Schritt 6. - Dank
depends_onmitcondition: service_healthystartet die Web-App erst, wenn der Healthcheck der Datenbank besteht.
Healthcheck kurz erklärt
Ein healthcheck prüft, ob ein Dienst wirklich bereit ist. Die wichtigsten Felder: test (das Kommando, z. B. ["CMD-SHELL", "..."] oder ["CMD", ...]), interval (Prüfabstand), timeout (max. Dauer pro Prüfung), retries (Fehlversuche bis unhealthy) und start_period (Karenzzeit beim Start, in der Fehlversuche noch nicht zählen). So weiß Compose, wann condition: service_healthy erfüllt ist.
Schritt 5: Den Stack mit den Kernbefehlen steuern
Alle Befehle führst du im Verzeichnis der compose.yaml aus; Compose liest compose.yaml und .env dann automatisch. Das sind die Kernbefehle:
docker compose up -d # Stack im Hintergrund (detached) starten/aktualisieren
docker compose ps # Status der Services: laufend / Health / Ports
docker compose logs -f # Logs aller Services folgen
docker compose logs -f web # nur Logs des Service 'web' folgen
docker compose exec db psql -U postgres # Befehl im laufenden Container ausfuehren
docker compose down # Container + Netzwerke entfernen; Named Volumes bleiben
docker compose down -v # zusaetzlich Named + anonyme Volumes loeschen (Datenverlust!)Drei Befehle, die du oft beim Debuggen brauchst:
# Compose-Datei nach Interpolation der .env-Variablen rendern und validieren
docker compose config
# auch Container von nicht mehr definierten Services aufraeumen
docker compose down --remove-orphans
# explizite Pfade fuer Datei und Env (statt aus dem aktuellen Verzeichnis)
docker compose -f /opt/stacks/app/compose.yaml --env-file /opt/stacks/app/.env up -ddocker compose exec führt einen Befehl im bereits laufenden Container aus und startet keinen neuen. Das ist der richtige Weg, um etwa eine Datenbank-Shell zu öffnen oder schnell etwas zu prüfen.
Schritt 6: Ports sicher binden und Reverse Proxy davor setzen
Ein Port-Mapping wie "8080:80" bindet an alle Interfaces (0.0.0.0) und ist damit potenziell öffentlich erreichbar. Brisant: Docker bearbeitet die iptables-Regeln direkt und kann so an einer host-basierten Firewall vorbei Ports öffnen. Binde Dienste deshalb standardmäßig an den Loopback:
ports:
- "127.0.0.1:8080:80" # nur lokal erreichbarDen öffentlichen Zugriff regelst du dann sauber über einen Reverse Proxy mit TLS davor – so terminierst du HTTPS zentral und exponierst die Container nicht direkt. Wie das geht, zeigt unsere separate Anleitung zum Reverse Proxy.
Schritt 7: Sauberer Update-Workflow
Images aktualisierst du nicht durch einen kompletten Neustart des Stacks, sondern in zwei Schritten. Zuerst ziehst du die neuen Images, dann lässt du Compose nur die Container ersetzen, deren Image oder Konfiguration sich geändert hat:
docker compose pull # neue Images fuer alle Services ziehen
docker compose up -d # nur geaenderte Container neu erstellen
# beides in einem Aufruf
docker compose pull && docker compose up -d
# nur ein einzelnes Image aktualisieren
docker compose pull dbUnveränderte Container laufen dabei weiter – es gibt keinen vollen Stack-Neustart. Da die Datenbankdaten im Named Volume liegen, bleiben sie über das Update hinweg erhalten. Nach dem Update lohnt ein Blick auf docker compose ps und docker compose logs -f, um den gesunden Zustand zu bestätigen.
Troubleshooting
- Fehler
volume ... is undefined: Du nutzt ein Named Volume im Service, hast es aber nicht im Top-Level-volumes:-Block deklariert. Trage es dort nach. - Variable kommt im Container nicht an: Eine Definition allein in der
.envsetzt sie nur als Platzhalter für die Compose-Datei, nicht im Container. Reiche sie per${VAR}inenvironment:durch oder nutzeenv_file:. - Web-App startet, kann die DB aber nicht erreichen: Die Kurzform
depends_on: [db]wartet nur auf den Container-Start, nicht auf Bereitschaft. Nutze die Langform mitcondition: service_healthyund einen Healthcheck im DB-Service. - Obsolet-Warnung beim Start: Ein Top-Level-
version:löst eine Warnung aus. Entferne die Zeile ersatzlos. - Daten plötzlich weg: Wahrscheinlich wurde
docker compose down -vausgeführt – das löscht Named Volumes unwiderruflich. Für einen reinen Stopp nutzedocker compose stopoderdownohne-v. - Unklar, was Compose tatsächlich sieht:
docker compose configrendert die Datei nach Einsetzen aller.env-Variablen und zeigt, welche Werte real verwendet werden. - Port nicht erreichbar oder umgekehrt zu offen: Prüfe die Bindung.
"8080:80"ist an allen Interfaces offen,"127.0.0.1:8080:80"nur lokal.
Häufige Fragen
compose.yaml oder docker-compose.yml – welcher Dateiname ist richtig?
Der bevorzugte und empfohlene Name ist compose.yaml. Aus Kompatibilität liest Compose v2 auch compose.yml, docker-compose.yaml und docker-compose.yml. Für neue Projekte nimm compose.yaml.
Brauche ich noch das version:-Feld am Anfang der Datei?
Nein. Das Top-Level-version:-Feld ist obsolet. Compose v2 ignoriert es, validiert immer gegen das aktuelle Schema und warnt bei Verwendung. Lass es weg.
Was ist der Unterschied zwischen docker-compose und docker compose?
docker-compose (mit Bindestrich) ist die alte v1 in Python und End-of-Life. docker compose (mit Leerzeichen) ist das aktuelle, offizielle v2-Plugin. Nutze immer die Variante mit Leerzeichen; Tutorials mit Bindestrich können veraltetes Verhalten zeigen.
Löscht docker compose down meine Datenbank?
Nein, nicht von allein. docker compose down entfernt Container und Netzwerke, lässt Named Volumes aber stehen. Erst docker compose down -v (bzw. --volumes) löscht Named und anonyme Volumes – und damit die Datenbankdaten unwiderruflich.
Wie aktualisiere ich meinen Stack, ohne Daten zu verlieren?
Mit docker compose pull gefolgt von docker compose up -d. Compose ersetzt nur die Container mit geändertem Image oder geänderter Konfiguration; die Daten in den Named Volumes bleiben erhalten.
Wie sorge ich dafür, dass die App erst startet, wenn die Datenbank bereit ist?
Über die Langform von depends_on mit condition: service_healthy kombiniert mit einem Healthcheck im Datenbank-Service. Die Kurzform garantiert nur die Startreihenfolge, nicht die Bereitschaft. Weitere Bedingungen sind service_started und service_completed_successfully.
Fazit
Docker Compose macht aus einem Bündel einzelner Container einen reproduzierbaren, dokumentierten Stack. Du beschreibst Services, Images, Ports, Volumes, Variablen, Netzwerke und Abhängigkeiten einmal in der compose.yaml, lagerst Geheimnisse in die .env aus und steuerst alles mit einer Handvoll Befehle. Mit Named Volumes überleben deine Daten jeden down, mit eigenen Netzwerken erreichen sich Container über den Servicenamen, mit Healthchecks und depends_on wird die Startreihenfolge robust, und der Zweischritt aus pull und up -d hält den Stack sauber aktuell. Damit hast du das Fundament für stabiles Self-Hosting – binde Ports an den Loopback, setze einen Reverse Proxy mit TLS davor und schütze deine .env, dann ist der Stack auch produktionsreif.
Weiterführende Anleitungen und Quellen
- Docker installieren: Engine und Compose-Plugin einrichten – die Basis, die du für diese Anleitung brauchst.
- Docker-Netzwerke und Volumes verstehen – tiefer in die Konzepte hinter Volumes und Netzwerken.
- Nginx als Reverse Proxy mit TLS einrichten – den öffentlichen Zugriff sauber vor deinen Stack setzen.
- Alle Docker-Anleitungen in der Kategorie Docker – weitere Container-Themen im Überblick.
Quellen: Compose file reference – services (image, ports, volumes, depends_on, healthcheck, restart), Docker Compose CLI reference (up/down/ps/logs/pull/exec), Set environment variables / variable interpolation (.env) und Version and name top-level elements (version ist obsolet).