Docker Tutorial Deutsch – Serie Teil 2: Docker Compose einrichten und verstehen

Nach Teil 1 hatte ich Docker am Laufen. Ich konnte Container starten, stoppen, Logs lesen. Ich fühlte mich gut.
Dann wollte ich anfangen, wirklich etwas Sinnvolles zu betreiben. Ich wollte eine kleine Webapplikation hochziehen – einen einfachen Dienst mit einer Datenbank dahinter. Also tippte ich mir einen Container-Startbefehl nach dem anderen in die Kommandozeile. Der erste Container. Der zweite. Den musste ich mit dem ersten verbinden. Dann noch ein Volume für die Datenbank. Dann ein eigenes Netzwerk, damit beide Container miteinander reden können.
Der Befehl, den ich am Ende hatte, war länger als manche E-Mails, die ich schreibe.
docker run -d --name meine-db --network app-net -e POSTGRES_PASSWORD=geheim -v db-daten:/var/lib/postgresql/data postgres:16
Und das war nur der erste von zwei Befehlen. Beim nächsten Neustart musste ich das alles wieder eintippen. Ich hab mir das natürlich in eine Textdatei kopiert, aber das war keine Lösung – das war Bastelei.
Das ist der Moment, in dem mir jemand gesagt hat: „Dafür gibt es Docker Compose.“
Warum Docker Compose das Spielfeld verändert
Ich erkläre es so, wie ich es gerne erklärt bekommen hätte.
Docker Compose ist ein Werkzeug, mit dem du alle deine Container, deren Einstellungen, Netzwerke und Volumes in einer einzigen Textdatei beschreibst. Diese Datei heißt docker-compose.yml. Und wenn du alles gestartet haben möchtest, tippst du exakt einen Befehl:
docker compose up -d
Das war’s.
Stell dir vor, du ziehst zum dritten Mal in neue Hardware um. Normalerweise müsstest du dir merken: Welche Container hatte ich? Welche Ports? Welche Umgebungsvariablen? Mit Compose packst du deine gesamte Infrastruktur in eine Datei. Die kopierst du auf den neuen Rechner, tippst docker compose up -d, und nach zwei Minuten läuft alles wieder. Exakt so wie vorher.
Das ist für mich der eigentliche Grund, warum Compose so mächtig ist – nicht die Bequemlichkeit beim ersten Start, sondern die Reproduzierbarkeit. Dein Setup ist dokumentiert. Es ist versionierbar. Es ist teilbar.
Was du aus Teil 1 brauchst
Falls du direkt hier eingestiegen bist: Willkommen! Diese Serie baut aufeinander auf. In Teil 1 haben wir Docker Desktop installiert, gelernt was Images, Container und Volumes sind, und den ersten Container per Hand gestartet.
Für diesen Teil brauchst du:
- Docker Desktop installiert und lauffähig
- Ein grundlegendes Verständnis von Container, Image, Port und Volume
- Eine Kommandozeile (PowerShell oder Terminal reicht völlig)
Docker Compose ist seit Docker Desktop 3.x direkt integriert – du musst nichts extra installieren. Wenn docker compose version in deiner Konsole eine Versionsnummer zurückgibt, bist du startklar.
Die Anatomy einer docker-compose.yml – Stück für Stück
Eine docker-compose.yml Datei ist im YAML-Format geschrieben. YAML (ausgesprochen wie „Yäml“) ist eine Auszeichnungssprache für Konfigurationsdateien. Sie ist bewusst menschenlesbar gehalten – keine spitzen Klammern wie in XML, keine geschweiften Klammern wie in JSON. Nur Einrückungen mit Leerzeichen.
Wichtig: YAML reagiert empfindlich auf Einrückungen. Tabs sind verboten – du musst immer Leerzeichen verwenden. Das ist einer der häufigsten Fehlerquellen. Merk dir das.
Schauen wir uns eine einfache Datei an:
services:
webserver:
image: nginx:latest
container_name: mein-webserver
ports:
- "8080:80"
volumes:
- ./webseite:/usr/share/nginx/html
restart: unless-stopped
Lass mich das Zeile für Zeile erklären:
services: – Der Hauptbereich. Hier definierst du alle Container, die du starten möchtest. Jeder Service ist ein Container.
webserver: – Das ist der Name dieses Services. Du kannst ihn frei wählen.
image: nginx:latest – Welches Docker Image verwendet werden soll. latest bedeutet: die aktuellste Version. In Produktionsumgebungen würde ich stattdessen immer eine konkrete Versionsnummer angeben (z.B. nginx:1.27), damit Updates nicht unbemerkt Dinge kaputt machen.
container_name: – Der Name, unter dem der Container in docker ps auftaucht.
ports: – Port-Mapping. "8080:80" heißt: Port 8080 auf deinem Rechner zeigt auf Port 80 im Container. Immer nach dem Muster: Host:Container.
volumes: – Hier verbindest du Verzeichnisse. ./webseite ist ein Ordner auf deinem Rechner (relativ zur Position der Compose-Datei). /usr/share/nginx/html ist der Zielort im Container. Nginx liest seine HTML-Dateien von dort.
restart: unless-stopped – Startet den Container automatisch neu, wenn er abstürzt oder der PC neugestartet wird – solange du ihn nicht manuell gestoppt hast. Für Homelab-Dienste fast immer die richtige Wahl.
Schritt für Schritt: Dein erstes Compose-Projekt
Genug Theorie. Wir bauen jetzt etwas Echtes.
Ich zeige dir, wie du einen Nginx-Webserver mit einer kleinen HTML-Seite über Docker Compose startest. Klingt simpel – ist es auch. Aber genau so fühlt man sich mit dem Werkzeug vertraut.
Ordnerstruktur anlegen
Erstelle einen neuen Ordner, zum Beispiel C:\docker-projekte\webserver. Darin legst du zwei Dinge an:
- Eine Datei namens
docker-compose.yml - Einen Unterordner namens
webseite
Im Ordner webseite erstellst du eine einfache index.html:
<!DOCTYPE html>
<html>
<head><title>Mein Docker Server</title></head>
<body>
<h1>Es funktioniert!</h1>
<p>Dieser Webserver läuft in einem Docker Container.</p>
</body>
</html>
Die docker-compose.yml schreiben
Öffne docker-compose.yml und füge folgenden Inhalt ein:
services:
webserver:
image: nginx:latest
container_name: mein-webserver
ports:
- "8080:80"
volumes:
- ./webseite:/usr/share/nginx/html:ro
restart: unless-stopped
Das :ro am Ende des Volume-Pfads steht für „read-only“. Der Container kann die Dateien lesen, aber nicht verändern. Für statische Webseiten eine gute Praxis.
Starten und testen
Öffne PowerShell, navigiere in den Projektordner:
cd C:\docker-projekte\webserver
Dann der magische Befehl:
docker compose up -d
Docker lädt das Nginx-Image (falls noch nicht vorhanden), erstellt den Container, verbindet die Volumes und startet alles. Öffne deinen Browser und gehe auf http://localhost:8080. Deine HTML-Seite sollte erscheinen.
Zum Stoppen:
docker compose down
Das stoppt alle Container in dieser Compose-Datei und räumt auf. Die Volumes bleiben dabei erhalten – deine Daten gehen also nicht verloren.
Ein realistisches Beispiel: App mit Datenbank
Ein Webserver alleine ist natürlich noch kein Homelab. Lass uns das Beispiel praxisnäher machen: Eine kleine Webanwendung, die eine Datenbank nutzt.
Ich verwende hier Adminer – ein webbasiertes Datenbankverwaltungstool – zusammen mit einer PostgreSQL-Datenbank. Beides in Docker Containern, miteinander verbunden, ohne dass du PostgreSQL auf deinem Windows-System installieren musst.
Erstelle einen neuen Ordner C:\docker-projekte\datenbank und lege folgende docker-compose.yml an:
services:
datenbank:
image: postgres:16
container_name: meine-postgres
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: sicherespasswort
POSTGRES_DB: meinedaten
volumes:
- postgres-daten:/var/lib/postgresql/data
restart: unless-stopped
adminer:
image: adminer:latest
container_name: mein-adminer
ports:
- "8081:8080"
depends_on:
- datenbank
restart: unless-stopped
volumes:
postgres-daten:
Hier ist einiges Neues dazugekommen – gehen wir es durch.
environment: – Umgebungsvariablen. Das ist, wie du einem Container Konfiguration mitgibst. PostgreSQL liest beim ersten Start diese Variablen und erstellt Benutzer und Datenbank entsprechend. Kein manuelles Setup nötig.
volumes: postgres-daten:/var/lib/postgresql/data – Dieses Mal verwenden wir kein lokales Verzeichnis, sondern ein Named Volume. Docker verwaltet es selbst. Der Name postgres-daten erscheint auch unten in der volumes:-Sektion auf oberster Ebene – das ist notwendig, um es zu registrieren.
depends_on: – Adminer soll erst starten, wenn die Datenbank bereit ist. depends_on definiert diese Abhängigkeit. Wichtiger Hinweis: Das sorgt nur dafür, dass der Datenbank-Container gestartet wird, bevor Adminer anläuft – nicht, dass die Datenbank vollständig bereit ist. Bei kritischen Diensten braucht man manchmal zusätzliche Health-Check-Konfiguration. Für unsere Zwecke reicht es.
volumes: (auf oberster Ebene) – Hier werden Named Volumes deklariert. Ohne diese Deklaration akzeptiert Compose das Volume nicht.
Starte alles mit docker compose up -d und öffne http://localhost:8081. Du siehst die Adminer-Oberfläche. Als Server gibst du datenbank ein (den Service-Namen aus der Compose-Datei) – Docker Compose erstellt automatisch ein internes Netzwerk, in dem sich die Container gegenseitig über ihren Service-Namen erreichen.
Das ist einer der unschätzbaren Vorteile von Compose: Automatisches Netzwerk. Du musst kein docker network create tippen, kein manuelles Verknüpfen. Alle Services in einer Compose-Datei sprechen von Haus aus miteinander.
Umgebungsvariablen sauber verwalten: die .env Datei
Passwörter direkt in der docker-compose.yml zu schreiben ist praktisch – aber keine gute Idee, sobald du deine Datei irgendwo teilst oder in ein Git-Repository hochlädst.
Die Lösung ist eine .env-Datei. Docker Compose liest sie automatisch, wenn sie im selben Verzeichnis wie die Compose-Datei liegt.
Erstelle eine Datei namens .env:
POSTGRES_USER=admin
POSTGRES_PASSWORD=sicherespasswort
POSTGRES_DB=meinedaten
In der docker-compose.yml referenzierst du sie mit ${VARIABLENNAME}:
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
Wenn du das Projekt nun mit Git versionierst, trägst du .env in die .gitignore ein – und deine Passwörter bleiben privat, während die Compose-Datei problemlos geteilt werden kann.
Das klingt nach Overhead, aber der Aufwand ist minimal und du wirst es nicht bereuen. Ich hab es einmal vergessen und einen Moment lang geschwitzt, als ich merkte, dass mein Repository mit Klartext-Passwörtern öffentlich war. Nur eine Minute, aber es reicht für ein ganzes Leben.
Dockerfile und Compose kombinieren – eigene Images einbauen
Bisher haben wir nur fertige Images aus Docker Hub verwendet. Aber Docker Compose kann auch dein eigenes Image bauen – direkt aus einem Dockerfile.
Das ist relevant, sobald du eigene Anwendungen containerisieren möchtest. Erinnere dich an das Dockerfile aus Teil 1:
mein-projekt/
├── docker-compose.yml
├── Dockerfile
└── app.py
In der docker-compose.yml nutzt du statt image: das build:-Schlüsselwort:
services:
meine-app:
build:
context: .
dockerfile: Dockerfile
container_name: meine-python-app
ports:
- "5000:5000"
restart: unless-stopped
context: . sagt Compose, wo der Build-Kontext liegt – also welche Dateien beim Bauen zur Verfügung stehen. Der Punkt steht für das aktuelle Verzeichnis.
Mit docker compose up --build -d wird das Image neu gebaut und gestartet. Das --build-Flag erzwingt einen Neubau – sinnvoll, wenn du Änderungen am Code gemacht hast.
Compose-Dateien für mehrere Umgebungen: override Dateien
Ein Trick, den ich erst nach einigen Monaten entdeckt habe: Du kannst Compose-Dateien überlagern. Das ist nützlich, wenn du eine Basis-Konfiguration hast, aber für Entwicklung und Betrieb leicht unterschiedliche Einstellungen brauchst.
Die Standard-Datei docker-compose.yml enthält alles Gemeinsame. Eine zweite Datei docker-compose.override.yml wird von Compose automatisch zusätzlich geladen, wenn sie existiert.
Für Entwicklung willst du vielleicht einen Debug-Port öffnen:
# docker-compose.override.yml
services:
meine-app:
ports:
- "5001:5001"
environment:
DEBUG: "true"
Für den produktiven Betrieb erstellst du docker-compose.prod.yml und lädst sie explizit:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Das ist fortgeschrittenes Terrain – für den Anfang reicht eine einzige Datei völlig. Aber gut zu wissen, dass es diese Möglichkeit gibt, wenn das Projekt wächst.
Alle wichtigen Compose-Befehle im Überblick
Das hier ist mein persönlicher Spickzettel, der nach einem Jahr immer noch an meinem Monitor hängt:
# Alle Services starten (im Hintergrund)
docker compose up -d
# Services neu bauen und starten
docker compose up --build -d
# Alle Services stoppen (Container bleiben erhalten)
docker compose stop
# Alle Services stoppen und Container löschen
docker compose down
# Container UND Volumes löschen (Vorsicht: Datenverlust!)
docker compose down -v
# Status aller Services anzeigen
docker compose ps
# Logs aller Services anzeigen
docker compose logs
# Logs eines bestimmten Services anzeigen
docker compose logs webserver
# Logs in Echtzeit verfolgen
docker compose logs -f webserver
# Befehl in einem laufenden Container ausführen
docker compose exec webserver bash
# Einen Service neu starten
docker compose restart webserver
# Images aktualisieren (neue Version laden)
docker compose pull
docker compose up -d
Die Kombination aus docker compose logs -f und docker compose ps löst gefühlt 80 Prozent aller Probleme. Wenn etwas nicht funktioniert: Zuerst ps um zu sehen ob der Container läuft, dann logs um zu sehen warum er es vielleicht nicht tut.
Troubleshooting: Meine ehrlichsten Fehler
Ich war einige Male kurz davor, alles hinzuschmeißen. Damit du das nicht musst, hier meine teuersten Lektionen:
Fehler 1: Tabs statt Leerzeichen in der YAML-Datei YAML hasst Tabs. Wirklich. Wenn du einen merkwürdigen Fehler wie yaml.scanner.ScannerError bekommst, öffne deine Datei in VS Code und aktiviere unten in der Statusleiste die Anzeige von Leerzeichen. Tabs sehen aus wie Pfeile, Leerzeichen als Punkte. Ersetze jeden Tab durch zwei oder vier Leerzeichen.
Fehler 2: Port bereits belegt Bind for 0.0.0.0:8080 failed: port is already allocated. Das bedeutet, ein anderer Container oder Prozess nutzt Port 8080 bereits. Lösung: Im Compose-File einen anderen Host-Port wählen, z.B. 8082:80. Der Container-Port (rechts vom Doppelpunkt) bleibt gleich.
Fehler 3: Volumes falsch verstanden Ich dachte lange, docker compose down löscht meine Daten. Tut es nicht. Container werden gelöscht – aber Volumes bleiben. Erst docker compose down -v löscht auch Volumes. Ich hab das einmal versehentlich mit dem -v-Flag gemacht und eine halbe Stunde Konfigurationsarbeit verloren. Lektion gelernt.
Fehler 4: Container reden nicht miteinander Du hast zwei Services definiert, aber der eine findet den anderen nicht. Häufigste Ursache: Du verwendest localhost als Hostname, obwohl du den Service-Namen aus der Compose-Datei verwenden musst. Im internen Compose-Netzwerk erreichst du einen Service über seinen Namen – nicht über localhost oder eine IP-Adresse.
Fehler 5: Änderungen werden nicht übernommen Du hast etwas in der docker-compose.yml geändert, aber nach docker compose up -d passiert scheinbar nichts. Das liegt daran, dass Compose laufende Container nicht automatisch neu erstellt, wenn sich nur die Konfiguration ändert. Erzwinge einen Neustart mit:
docker compose up -d --force-recreate
Fehler 6: Das .env-File wird nicht gelesen Die .env-Datei muss im gleichen Verzeichnis liegen wie die docker-compose.yml. Nicht im Unterordner. Nicht woanders. Exakt da.
Projektstruktur: So halte ich mein Homelab organisiert
Das hat mich am meisten Zeit gespart: eine saubere Verzeichnisstruktur von Anfang an.
Ich arbeite so:
C:\docker-projekte\
├── jellyfin\
│ ├── docker-compose.yml
│ └── .env
├── homeassistant\
│ ├── docker-compose.yml
│ └── .env
├── datenbank\
│ ├── docker-compose.yml
│ └── .env
└── webserver\
├── docker-compose.yml
└── webseite\
└── index.html
Jedes Projekt bekommt seinen eigenen Ordner, seine eigene Compose-Datei und seine eigene .env. Das hält alles sauber und getrennt. Wenn ich ein Projekt entfernen möchte, lösche ich einfach den Ordner – nach einem docker compose down natürlich.
Und wenn ich auf neue Hardware umziehe? Ich kopiere den gesamten docker-projekte-Ordner, installiere Docker Desktop, und führe in jedem Unterordner einmal docker compose up -d aus.
Portainer: Die grafische Schaltzentrale
Ich verwalte mein Homelab hauptsächlich über die Kommandozeile – aber ich bin ehrlich: Portainer macht das Leben deutlich angenehmer.
Portainer ist ein webbasiertes Container-Management-Tool, das du selbst als Docker Container betreibst. Du siehst alle Container auf einen Blick, kannst Logs lesen, Container starten und stoppen, Volumes inspizieren und sogar Compose-Stacks direkt über die Web-Oberfläche verwalten.
Und das Beste: Du startest es natürlich über Docker Compose.
Erstelle C:\docker-projekte\portainer\docker-compose.yml:
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
ports:
- "9000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer-daten:/data
restart: unless-stopped
volumes:
portainer-daten:
Das /var/run/docker.sock Volume ist besonders: Es gibt Portainer Zugriff auf den Docker-Socket – also die Schnittstelle, über die Docker kommuniziert. Dadurch kann Portainer alle anderen Container verwalten. Auf Windows mit Docker Desktop funktioniert das unter WSL 2 out of the box.
Nach docker compose up -d erreichst du Portainer unter http://localhost:9000. Beim ersten Aufruf richtest du einen Admin-Benutzer ein.
Fazit: Compose ist kein Add-on, es ist der Standard
Rückblickend würde ich sagen: Wer Docker ohne Compose lernt, lernt es halb. Der eigentliche Workflow fängt bei Compose an.
Du hast heute gelernt:
- Was Docker Compose ist und warum es unverzichtbar ist
- Wie eine
docker-compose.ymlaufgebaut ist - Wie du Services, Ports, Volumes und Umgebungsvariablen definierst
- Wie du mehrere Container miteinander verbindest
- Wie du eigene Dockerfiles in Compose einbindest
- Wie du dein Homelab sauber strukturierst
- Wie du mit Portainer eine grafische Oberfläche bekommst
Das ist eine solide Grundlage für alles, was folgt.
In Teil 3 dieser Serie wird es richtig praktisch: Wir setzen Home Assistant und Jellyfin als Docker Container auf – zwei der beliebtesten Selfhosting-Anwendungen überhaupt. Home Assistant für die Heimautomatisierung, Jellyfin als persönliches Mediacenter. Alles über Compose, alles reproduzierbar, alles wartbar.
Vorheriger Artikel: Teil 1 – Docker Grundlagen und Installation: Dein erster Container unter Windows
Nächster Artikel: [Teil 3 – Home Assistant & Jellyfin im Docker Container: Selfhosting für dein Heimnetzwerk] (in Kürze)
