Docker Compose verstehen: Services, Volumes, Netzwerke
compose.yaml erklärt: Services, Volumes, Netzwerke und Variablen – das Docker-Vokabular, auf dem jedes App-Rezept der Serverküche aufbaut.
Inhaltsverzeichnis
Im Docker-Tutorial hast du das Compose-Plugin
installiert – erklärt haben wir es noch nicht. Das holen wir jetzt nach, denn fast
jedes App-Rezept hier beschreibt eine Anwendung als compose.yaml. Wer diese
Datei liest wie einen Einkaufszettel, kann jedes folgende Tutorial anpassen, statt
nur zu kopieren.
Was bauen wir?
Wir bauen Schritt für Schritt einen kleinen Stack auf und lernen dabei die fünf
Bausteine kennen, aus denen praktisch jede compose.yaml besteht: Services
(die Container), Ports (Erreichbarkeit von außen), Volumes (persistente
Daten), Netzwerke (Container reden miteinander) und Umgebungsvariablen
(Konfiguration). Am Ende verstehst du, warum deine Daten einen down-Befehl
überleben – und wann nicht.
Getestet mit Docker Compose v5.3 (das docker compose-Plugin ohne Bindestrich –
nicht das alte docker-compose v1 mit Bindestrich).
Voraussetzungen
- Ein abgesicherter Server mit installiertem Docker und Compose-Plugin
- Der Benutzer ist in der
docker-Gruppe (dann brauchst du keinsudovordocker)
Schritt für Schritt
Schritt 1: Die erste compose.yaml
Eine compose.yaml beschreibt deklarativ, welche Container laufen sollen – du
sagst was du willst, nicht wie. Lege einen Projektordner an; der Ordnername
wird später zum Präfix aller Container:
mkdir -p ~/compose-demo/site && cd ~/compose-demoLege eine kleine HTML-Seite an, die wir gleich ausliefern:
echo '<h1>Hallo aus der Serverküche</h1>' > site/index.htmlUnd jetzt die zentrale Datei compose.yaml:
services:
web:
image: nginx:1.31
ports:
- "8080:80"
volumes:
- ./site:/usr/share/nginx/html:ro
restart: unless-stoppedZeile für Zeile:
services:– die oberste Ebene. Jeder Eintrag darunter ist ein Container.web:– ein frei wählbarer Service-Name. Merke ihn dir, er wird später auch zum Hostnamen im internen Netzwerk (Schritt 3).image: nginx:1.31– das Container-Image mit festem Tag. Nielatestverwenden:lateständert sich unter dir und macht Fehler unreproduzierbar.ports: - "8080:80"– FormatHOST:CONTAINER. Port 80 im Container wird auf 8080 des Servers gelegt. Links steht immer der Server.volumes: - ./site:...:ro– der lokale Ordnersitewird ins Web-Root gehängt,:ro= read-only. Mehr dazu in Schritt 2.restart: unless-stopped– der Container startet nach einem Reboot oder Absturz automatisch neu, außer du hast ihn selbst gestoppt. Für Server-Dienste der sinnvolle Standard. Die Alternativen:no(nie automatisch – der Default),always(startet selbst nach einem manuellen Stopp wieder, selten gewollt) undon-failure(nur nach einem Absturz mit Fehlercode). Für die allermeisten Dienste istunless-stoppedgenau richtig.
Starte den Stack:
docker compose up -d-d bedeutet detached (im Hintergrund). Beim ersten Mal lädt Docker das Image;
danach siehst du am Ende:
[+] up 2/2
✔ Network compose-demo_default Created 0.0s
✔ Container compose-demo-web-1 Started 0.2sPrüfe den Status:
docker compose psNAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
compose-demo-web-1 nginx:1.31 "/docker-entrypoint.…" web 10 seconds ago Up 9 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcpDer Container heißt compose-demo-web-1 – Projektordner + Service + Nummer. Test:
curl localhost:8080<h1>Hallo aus der Serverküche</h1>Läuft. Logs eines Dienstes siehst du mit docker compose logs web (oder -f zum
Mitlaufen).
Schritt 2: Volumes – wo deine Daten wirklich liegen
Container sind vergänglich: Löschst du einen Container, ist alles weg, was im Container geschrieben wurde. Damit Daten das überleben, gibt es zwei Arten von Volumes:
- Bind-Mount (
./site:/usr/share/nginx/html): ein Ordner von deinem Server wird in den Container gehängt. Ideal für Config-Dateien, die du selbst bearbeitest. - Named Volume (
webdata:/var/lib/...): ein von Docker verwalteter Speicher. Ideal für Datenbank-Daten – performant und sauber getrennt vom Host.
Welches der beiden du wann nimmst – und was das für Rechte, Umzüge und Backups bedeutet – vertieft Docker-Volumes vs. Bind-Mounts.
In Schritt 1 haben wir ein Bind-Mount genutzt. Für datenbankartige Dienste sieht
es so aus – ändere compose.yaml testweise:
services:
web:
image: nginx:1.31
volumes:
- webdata:/usr/share/nginx/html
volumes:
webdata:Named Volumes müssen zusätzlich auf oberster Ebene unter volumes: deklariert
werden. Nach docker compose up -d taucht das Volume auf:
docker volume lsDRIVER VOLUME NAME
local compose-demo_webdataDer entscheidende Punkt kommt jetzt:
docker compose down
docker volume ls[+] down 2/2
✔ Container compose-demo-web-1 Removed 0.2s
✔ Network compose-demo_default Removed 0.2s
DRIVER VOLUME NAME
local compose-demo_webdatadocker compose down entfernt Container und Netzwerk – das Volume bleibt. Genau
deshalb überleben deine Datenbank-Inhalte ein Update. Merke dir das Gegenstück:
down -v löscht Daten
docker compose down -v löscht auch die Named Volumes – also alle persistenten
Daten des Stacks. Tippe das -v nie aus Reflex mit. Für ein reines Neustarten
reicht docker compose down (ohne -v).Schritt 3: Netzwerke – Container reden miteinander
Compose legt automatisch ein Netzwerk pro Projekt an (oben gesehen:
compose-demo_default). Alle Services darin erreichen sich über ihren
Service-Namen als Hostname – kein IP-Gefummel nötig. Das ist der Grund, warum in
App-Tutorials die Anwendung ihre Datenbank einfach unter db erreicht.
Zum Beweis ein zweiter Service, der den ersten anspricht:
services:
web:
image: nginx:1.31
volumes:
- ./site:/usr/share/nginx/html:ro
ping:
image: curlimages/curl:8.21.0
depends_on:
- web
command: ["curl", "-s", "http://web"]depends_on: - web– Compose startetwebvorping. (Achtung: das wartet nur auf den Start, nicht auf „fertig hochgefahren" – dafür gibt es Healthchecks, Schritt 4.)command:– überschreibt den Standardbefehl des Images.pingrufthttp://webauf –webist der Service-Name aus derselben Datei.
Führe nur den ping-Service einmalig aus:
docker compose run --rm ping<h1>Hallo aus der Serverküche</h1>ping hat web allein über den Namen erreicht – ganz ohne Port-Mapping. Merke:
ports: brauchst du nur, um einen Dienst von außen (dem Internet) erreichbar zu
machen. Container untereinander reden über das interne Netzwerk – später lassen
wir deshalb Datenbanken bewusst ohne ports: laufen.
Bisher lebt jedes Netzwerk innerhalb eines Compose-Projekts. Manchmal sollen aber Container aus verschiedenen Projekten miteinander reden – das klassische Beispiel ist ein Reverse Proxy, der vor vielen unabhängigen App-Stacks sitzt. Dafür gibt es das externe Netzwerk: eines, das du einmalig von Hand anlegst und das danach mehrere Compose-Projekte gemeinsam nutzen:
docker network create proxyIn der compose.yaml legst du es dann nicht neu an, sondern verweist mit external: true
auf das bereits bestehende Netzwerk:
services:
web:
image: nginx:1.31
networks:
- proxy
networks:
proxy:
external: trueexternal: true sagt Compose: „Dieses Netzwerk existiert schon – leg es nicht an und
lösch es beim down nicht." Fehlt das Netzwerk, bricht up mit network proxy declared as external, but could not be found ab – dann hast du das docker network create
vergessen. Genau dieses Muster – ein gemeinsames proxy-Netz plus external: true – ist
die Grundlage des Traefik-Tutorials, mit dem jede App
später ihre Domain und ihr HTTPS bekommt.
Was Compose dabei unter der Haube anlegt – den bridge-Treiber, das eingebaute DNS und
Container, die bewusst gar kein Internet bekommen – nimmt
Docker-Netzwerke verstehen
auseinander.
Schritt 4: Konfiguration – Umgebungsvariablen, .env und Healthchecks
Fast jede Anwendung wird über Umgebungsvariablen konfiguriert. Zwei Wege:
services:
app:
image: beispiel/app:1.0
environment:
- TZ=Europe/Berlin
- APP_PORT=3000Geheimnisse (Passwörter, Tokens) gehören nicht in die compose.yaml, sondern in
eine .env-Datei im selben Ordner. Compose liest sie automatisch:
echo "DB_PASSWORD=EIN_LANGES_ZUFALLSPASSWORT" > .envservices:
db:
image: postgres:18
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}`.env` nie ins Backup-Repo pushen
.env enthält Klartext-Geheimnisse. Nimm sie in eine .gitignore auf, falls du
deine Compose-Dateien versionierst, und sichere sie getrennt (verschlüsselt).So prüfst du, ob Compose deine Datei versteht und die .env-Werte richtig
einsetzt – ohne etwas zu starten:
docker compose configconfig löst alle Variablen auf und gibt die fertige, normalisierte Konfiguration
aus:
name: compose-demo
services:
db:
environment:
POSTGRES_PASSWORD: EIN_LANGES_ZUFALLSPASSWORT
image: postgres:18
networks:
default: nullSteht dort dein echter Wert statt ${DB_PASSWORD}, greift die .env. Brauchst
du nur einen schnellen Syntax-Check ohne die ganze Ausgabe, nimm
docker compose config --quiet – kommt nichts zurück (Exit-Code 0), ist die Datei
gültig. Genau das ist auch dein erster Griff bei YAML-Fehlern (siehe unten).
Ein Healthcheck sagt Docker, wann ein Dienst wirklich bereit ist – die Basis dafür, dass abhängige Dienste erst dann starten:
services:
db:
image: postgres:18
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
app:
image: beispiel/app:1.0
depends_on:
db:
condition: service_healthyMit condition: service_healthy startet app erst, wenn der Healthcheck von db
grün ist – das häufigste „warum verbindet sich meine App nicht zur Datenbank?"
verschwindet damit. Die vier Healthcheck-Felder bedeuten: test ist der Befehl,
der im Container läuft (Exit-Code 0 = gesund), interval der Abstand zwischen
den Prüfungen, timeout wie lange eine Prüfung dauern darf, und retries
wie viele Fehlversuche in Folge nötig sind, bevor der Container als unhealthy gilt.
Den aktuellen Zustand zeigt die STATUS-Spalte von docker compose ps als
(healthy) bzw. (unhealthy).
Schritt 5: Der Betriebs-Werkzeugkasten
Diese Befehle brauchst du täglich – immer im Projektordner ausführen:
docker compose up -d # starten / Änderungen anwenden
docker compose ps # Status der Services
docker compose logs -f web # Logs live mitlesen (Strg+C beendet nur das Ansehen)
docker compose exec web sh # Shell im laufenden Container
docker compose restart web # einen einzelnen Dienst neu starten
docker compose stop # anhalten, ohne Container/Netzwerk zu entfernen
docker compose pull # neue Image-Versionen holen
docker compose down # Stack stoppen und entfernen (Volumes bleiben)Fast alle Befehle lassen sich auf einen Service einschränken, indem du seinen
Namen anhängst (docker compose logs -f web, docker compose restart web) – ohne
Namen gelten sie für den ganzen Stack. Der Unterschied zwischen stop und down:
stop hält die Container nur an (mit start geht’s weiter), down entfernt sie
samt Netzwerk (die Named Volumes bleiben in beiden Fällen).
Ein Update läuft fast immer nach demselben Muster: Tag in der compose.yaml
hochsetzen → docker compose pull → docker compose up -d. Compose ersetzt nur die
Container, deren Image sich geändert hat.
Schritt 6: Alles zusammen – ein realistischer App-Stack
So sieht das Muster aus, das dir in den App-Tutorials immer wieder begegnet: eine
Anwendung plus ihre Datenbank. Diese Datei bündelt alles aus den Schritten 1–4 –
lies sie einmal komplett, dann hast du 90 % jeder späteren compose.yaml
verstanden:
services:
app:
image: beispiel/app:1.4 # feste Version, kein latest
ports:
- "8080:3000" # nur die App ist von außen erreichbar
environment:
- TZ=Europe/Berlin
- DATABASE_URL=postgres://app:${DB_PASSWORD}@db:5432/app
volumes:
- appdata:/data # persistente App-Daten (Named Volume)
depends_on:
db:
condition: service_healthy # startet erst, wenn db bereit ist
restart: unless-stopped
db:
image: postgres:18
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=app
volumes:
- dbdata:/var/lib/postgresql/data # die eigentlichen Datenbank-Dateien
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
# kein ports: – die Datenbank ist NUR intern über den Namen "db" erreichbar
volumes:
appdata:
dbdata:Drei Design-Entscheidungen, die du dir merken solltest:
- Nur
apphatports:. Die Datenbank braucht keinen offenen Host-Port – die App erreicht sie intern über den Hostnamendb(dieDATABASE_URLzeigt genau dorthin). Ein nicht veröffentlichter Port ist ein Port, den niemand aus dem Internet angreifen kann. - Zwei getrennte Named Volumes. App-Daten und Datenbank-Dateien liegen sauber getrennt – das macht spätere Backups und Restores nachvollziehbar.
- Passwort nur als
${DB_PASSWORD}. Der echte Wert steht in der.env, nicht in dieser Datei. Dieselbecompose.yamlkann so gefahrlos geteilt werden.
Genau dieses Grundgerüst – App nach außen, Datenbank nur intern, Daten in Named
Volumes, Secrets in der .env – wiederholt sich in Nextcloud, Vaultwarden,
Paperless und den meisten anderen Rezepten.
Wenn es nicht funktioniert
yaml: line 7: did not find expected key (oder ähnliche YAML-Fehler). YAML ist
einrückungssensibel – ausschließlich Leerzeichen, niemals Tabs, und pro Ebene
konsistent (üblich: 2 Leerzeichen). Prüfe die Datei ohne sie zu starten: docker compose config löst alles auf und meckert genau die falsche Zeile an.
Error ... address already in use beim up. Der Host-Port (links in 8080:80)
ist schon belegt. Finde den Beleger mit sudo ss -tlnp | grep 8080 oder wähle einen
anderen Host-Port. Zwei Container dürfen sich denselben Host-Port nicht teilen.
Eine App findet ihre Datenbank nicht (could not translate host name). Als
Hostname muss der Service-Name stehen (z. B. db), nicht localhost. Innerhalb
eines Containers ist localhost der Container selbst, nicht der Nachbar-Service. Und:
Beide Services müssen im selben Compose-Projekt (derselben Datei) liegen.
Nach docker compose down sind alle Daten weg. Entweder lag das Volume nicht als
Named Volume unter volumes: vor (dann war es nur der vergängliche
Container-Speicher), oder es wurde down -v verwendet. Persistente Dienste immer mit
deklariertem Named Volume fahren.
docker-compose: command not found. Das ist das alte Compose v1 (mit Bindestrich).
Aktuell ist docker compose (mit Leerzeichen, Plugin). Falls es fehlt: sudo apt install docker-compose-plugin (siehe
Docker-Tutorial).
Wartung & Backups
- Image-Tags pflegen: Feste Tags (
nginx:1.31) bedeuten, dass du Updates bewusst durch Hochsetzen des Tags einspielst. Das ist gewollt – so entscheidest du, wann ein Update kommt, statt überrascht zu werden. Rechne mit monatlichem Draufschauen auf die Release-Notes deiner Dienste. - Was gehört ins Backup? Nicht die Container – die sind aus der
compose.yamljederzeit neu baubar. Sichern musst du die Named Volumes (bzw. Bind-Mount- Ordner), diecompose.yamlund die.env. Ein durchdachtes Off-Site-Backup dieser Daten bauen wir mit verschlüsselten Restic-Backups. - Aufräumen:
docker compose downbeim Abbau eines Stacks; ungenutzte Images räumst du mitdocker image pruneweg. Named Volumes werden nie automatisch gelöscht – das ist Absicht.
Zuletzt aktualisiert: 07.08.2026
Feedback per E-Mail: feedback@serverkueche.de
Wie geht's weiter?

Docker-Netzwerke verstehen: bridge, internes DNS & das proxy-Netz
Warum deklariert jedes App-Rezept ein proxy-Netz? Dieser Guide erklärt Docker-Netzwerke, das interne DNS und …

Docker-Volumes vs. Bind-Mounts: Wo deine Daten wirklich liegen
Named Volumes oder Bind-Mount? Dieser Grundlagen-Guide erklärt den Unterschied, zeigt beide an echten Beispielen und …

Den ganzen Docker-Stack sicher aktuell halten
Update-Disziplin für deinen Docker-Stack: Versionen pinnen, mit Diun über Updates informiert werden, sicher einspielen, …

Monitoring mit Grafana & Prometheus: dein Server in Live-Dashboards
Ein vollständiger Monitoring-Stack aus Prometheus, node-exporter, cAdvisor und Grafana hinter Traefik – mit …

Traefik einrichten: Reverse Proxy mit automatischem HTTPS
Traefik als Reverse Proxy vor deinen Containern, mit automatischen Let's-Encrypt-Zertifikaten: Jede App bekommt Domain …
Das könnte dir auch schmecken

Homepage: das Dashboard für alle deine selbstgehosteten Dienste
Ein aufgeräumtes Start-Dashboard für deinen Server: Homepage verlinkt alle Dienste, zeigt Systemlast und live den …

Die eigene Website mit Hugo hosten – wie die Serverküche selbst
Statische Website mit Hugo bauen und im Docker-Container hinter Traefik ausliefern – schnell, sicher, ohne Datenbank. …

Linux-Benutzer, Gruppen & Dateirechte verstehen
Wer darf was auf deinem Server? Dieser Grundlagen-Guide erklärt Benutzer, Gruppen und Dateirechte an echten Befehlen – …