1. compose.yaml als Beschreibung der Umgebung
Wenn Docker für Sie bisher nur eine Ansammlung von Befehlen war, dann fügt Compose genau eine entscheidende Idee hinzu: „Die Umgebung soll in einer Datei leben, die Befehle kurz bleiben.“ Es ist wichtig, nicht zu verwechseln: Compose ersetzt nicht Docker Engine und erzeugt kein „anderes Docker“. Es bietet Ihnen lediglich eine bequeme Möglichkeit, Container und ihre Parameter zu beschreiben – ganz ohne sie im Kopf behalten oder zwischen Chats kopieren zu müssen.
Was macht Docker Compose eigentlich genau? Es liest YAML und verwandelt es in genau dieselben Aktionen, die Sie von Hand ausführen würden: image bauen (falls nötig), Netzwerk erstellen (falls nötig), Volume erstellen (falls nötig), Container mit den gewünschten Ports, Variablen und Mounts starten. Mit anderen Worten: Compose ist kein „zweites System“, sondern eine „ordentliche Hülle über das, was Sie bereits von Hand gemacht haben“.
Stellen Sie es sich am besten so vor:
flowchart TD A["compose.yaml (Beschreibung)"] --> B["docker compose up"] B --> C["Docker Engine: image bauen (optional)"] B --> D["Docker Engine: Netzwerk erstellen (optional)"] B --> E["Docker Engine: Volumes erstellen (optional)"] B --> F["Docker Engine: Container starten"] F --> G["Sie überprüfen die API /actuator/health und die Logs"]
Im Rahmen des heutigen Tages halten wir die Umgebung bewusst einfach: ein Service app, ohne externe Datenbank und ohne „Abhängigkeitszoo“. Das hilft, die Mechanik von Compose an einem reinen Beispiel zu verstehen, statt darin zu ertrinken, was genau nicht startet: die Anwendung oder die Datenbank.
2. Kanonische Namen und der Befehl Compose
Wenn Sie im Internet nach Beispielen suchen, teilt sich die Welt in zwei Lager: Manche zeigen die Datei docker-compose.yml, andere compose.yaml. Und manchmal begegnet Ihnen der Befehl docker-compose statt docker compose. Das liegt nicht daran, dass die Leute Chaos lieben (obwohl es manchmal so scheint), sondern daran, dass Compose historisch evolviert ist, und heute haben wir einen kanonischen modernen Weg.
In diesem Kurs legen wir den Baseline fest: die Datei heißt compose.yaml, der Befehl ist docker compose (mit Leerzeichen). Dies entspricht dem modernen Compose v2, das als Teil des Docker CLI ausgeliefert wird. Für Ihre zukünftige „erwachsene“ Erfahrung ist das vorteilhaft: weniger Verwirrung, weniger „ich habe eine andere Version“, weniger „warum wurde der Befehl nicht gefunden“.
Praktisch bedeutet das drei einfache Regeln.
Erste Regel: Die Datei liegt im Wurzelverzeichnis des Repositorys, neben Dockerfile, .dockerignore, README.md. Verstecken Sie sie nicht in docker/compose/compose.yaml, es sei denn, Sie bieten einen separaten Kurs zur Dateisuche an.
Zweite Regel: Sie starten Compose aus dem Projektverzeichnis (oder geben explizit die Datei an). Das häufigste Szenario in einem Lernprojekt wird so aussehen:
cd docker-java-catalog-service # Wechsel ins Projektverzeichnis (wo compose.yaml liegt)
docker compose up # Umgebung gemäß Beschreibung aus compose.yaml hochfahren
Dritte Regel: Wir verwandeln den Kurs nicht in Archäologie, aber der Kontext ist nützlich. Der alte Name docker-compose.yml und der Befehl docker-compose sind der Legacy-Pfad, den Sie in fremden Repositorys antreffen können. Es zu lesen zu können ist nützlich. Aber als primären Teaching-Path verwenden wir ihn nicht, um keine Varianten zu vermehren.
3. YAML in Compose: Einrückungen sind Ihre neuen „Klammern“
Die Compose-Datei ist YAML. Und YAML ist ein Format, das freundlich aussieht … bis man ein Leerzeichen zu viel macht. Wenn Sie in Java vergessen, eine geschweifte Klammer zu schließen, sagt der Compiler: „Freund, du hast den Block nicht geschlossen.“ In YAML übernehmen Einrückungen die Rolle der „Klammern“. Und ja, das ist dieser seltene Fall, in dem Leerzeichen wirklich wichtig sind und nicht nur „damit es schön aussieht“.
Lassen Sie uns eine minimale YAML-Grammatik vereinbaren, die speziell für Compose benötigt wird.
Schlüssel-Wert-Paar – das ist eine „Map“ (Wörterbuch). Beispiel:
services: # Abschnitt mit den Services der Umgebung
app: # Name des Services (logischer Name in Compose)
build: . # Image aus dem aktuellen Verzeichnis bauen (Dockerfile standardmäßig)
Hier ist services der Schlüssel der obersten Ebene, und der Wert ist ein verschachteltes Objekt. services hat den Schlüssel app, und app hat den Schlüssel build.
Liste – das ist ein „Array“ mit -. Beispiel mit Ports:
ports: # Liste der Portweiterleitungen
- "8080:8080" # host:container (links der Port auf Ihrem Rechner, rechts im Container)
ports ist eine Liste, in der jedes Element ein String ist. Warum String in Anführungszeichen? Weil es so zuverlässiger und lesbarer ist. YAML ist zwar intelligent, aber wir messen uns nicht mit ihm im IQ – wir wollen, dass die Datei auf allen Maschinen und bei allen Studierenden gleich gelesen wird.
Und hier ein wichtiger praktischer Tipp: Wenn die Datei aufgrund von Einrückungen bricht, erhalten Sie normalerweise eine Fehlermeldung wie mapping values are not allowed here oder etwas ähnlich „Poetisches“. In solchen Momenten müssen Sie nicht alle Sünden der letzten zwei Wochen bereuen. Sie müssen die Datei von oben nach unten durchlesen und prüfen, ob die Verschachtelungsebenen logisch sind.
Minimales Gerüst einer compose.yaml
Die Compose-Datei wird fast immer vom Abschnitt services aus gelesen. Das ist der Einstiegspunkt. Man kann sagen, dass services wie das public static void main der Umgebungswelt ist: Hier beginnt das „Leben“.
Die minimal mögliche Compose-Datei, die überhaupt einen Sinn ergibt, sieht so aus:
services: # Wir beginnen immer mit der Beschreibung der Services
app: # Name des Services (Sie werden in Befehlen darauf verweisen: logs, exec usw.)
build: . # Quelle für den Image-Bau (Kontext)
Ja, fertig. Drei Zeilen – und schon ergibt es einen Sinn: „Es gibt einen Service app, er wird aus dem aktuellen Verzeichnis gebaut.“ Und jetzt wird docker compose up --build das tun, was Sie von Hand gemacht hätten: docker build ... und docker run ...
Meistens werden Sie in einem realen Projekt ports hinzufügen, weil der Service sonst zwar startet, aber „nur sich selbst sieht“ (dem Container). Es ergibt sich dieses grundlegende Minimum:
services:
app:
build: .
ports:
- "8080:8080" # Port nach außen veröffentlichen: localhost:8080 -> container:8080
Jetzt gibt es eine „Brücke nach außen“: Der Host-Port 8080 wird in den Container auf 8080 weitergeleitet.
Beachten Sie, dass die Compose-Datei nicht der Ort ist, an dem Sie „eine neue Art des Anwendungsstarts erfinden“. Wenn Sie bereits einen korrekten ENTRYPOINT im Dockerfile haben, muss Compose ihn nicht wiederholen. Compose soll sagen: welches image gestartet werden soll und welche Laufzeitparameter es bekommt.
4. build und image: nicht verwechseln
Eine der häufigsten frühen Verwirrungen: „Soll ich build oder image schreiben?“. Das ist keine Geschmackssache. Es sind zwei verschiedene Szenarien.
build bedeutet: „Baue das image aus den Quellen“. Compose verwendet den Build-Kontext und das Dockerfile (standardmäßig Dockerfile im angegebenen Verzeichnis). Einfaches Beispiel:
services:
app:
build: . # Baue das Image lokal aus dem Dockerfile und dem Projektkontext
Wir sagen also: „Nimm das Dockerfile aus dem aktuellen Projekt und baue.“
image bedeutet: „Nimm ein bereits fertiges Image“. Entweder haben Sie es zuvor gebaut (docker build -t ...) oder es liegt in einer Registry. Beispiel:
services:
app:
image: docker-java-catalog-service:latest # Nimm fertiges Image mit Name:Tag
Und das ist wichtig: image „baut“ nicht, es „verwendet“. Wenn das image also weder lokal existiert noch abgerufen werden kann, findet kein Start statt.
Es gibt noch einen dritten, sehr praktischen Fall: build und image zusammen zu verwenden – zum Beispiel build: . plus image: docker-java-catalog-service. Dann ist build für die Bauquelle zuständig und image für den Namen des Ergebnisses. Für die lokale Entwicklung ist das praktisch: Compose baut den Service selbst und speichert das Ergebnis gleichzeitig unter einem vorhersagbaren Namen, der nicht vom Projektverzeichnisnamen abhängt.
Im Lernprojekt ist es am Anfang meist bequem, mit build: . zu beginnen, weil das weniger Schritte bedeutet: Sie müssen sich nicht merken, unter welchem Tag Sie das Image gebaut haben. Compose baut und startet selbst. Es ist jedoch nützlich, beide Modi zu verstehen, denn im echten Leben werden Sie beide verwenden: build für die lokale Entwicklung, image wenn Sie ein vorgebautes, festes Artefakt starten möchten.
Wenn Sie build etwas expliziter (und weniger magisch) gestalten möchten, können Sie es so schreiben:
services:
app:
build:
context: . # Wo der Baukontext liegt (was wir dem Docker-Daemon senden)
dockerfile: Dockerfile # Explizit den Namen des Dockerfile angeben
Das sieht länger aus, liest sich aber wie ein „Vertrag“. Am Anfang des Kurses ist das nicht zwingend erforderlich – manchmal ist Kürze nützlicher als ideologische Strenge.
5. Service-Einstellungen: Ports, Env, Volumes
Wenn Sie von docker run auf Compose umsteigen, ist der angenehmste Moment, dass Sie die Startparameter als Struktur sehen. Wo früher ein langer One-Liner stand, gibt es jetzt YAML, in dem man auf einen Blick erkennt: „Ports hier, Variablen hier, Volumes hier“.
Damit sich das Gehirn schneller daran gewöhnt, können Sie eine kleine Entsprechungstabelle im Kopf behalten. Das ist kein „vollständiges Compose-Handbuch“, sondern genau die Brücke von den bereits bekannten Flags von docker run zu den YAML-Schlüsseln:
| Was Sie in docker run gemacht haben | Wie es in compose.yaml aussieht | Bedeutung auf Deutsch |
|---|---|---|
| -p 8080:8080 | ports: ["8080:8080"] | „Öffne Port auf dem Host und leite in den Container weiter“ |
| -e SPRING_PROFILES_ACTIVE=standalone | environment: { SPRING_PROFILES_ACTIVE: standalone } | „Setze Umgebungsvariable innerhalb des Containers“ |
| -v ./data/exports:/data/exports | volumes: ["./data/exports:/data/exports"] | „Mounte Host-Verzeichnis in den Container“ |
Ein einfacher Service mit einem Port und einer Env-Variable sieht so aus:
services:
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: standalone
Das reicht bereits, um die Struktur zu erkennen: ports ist für den externen Zugang zum Container zuständig, environment für die Umgebungsvariablen im Inneren. Wenn der Service Bind-Mounts oder zusätzliche Parameter benötigt, werden diese einfach in den benachbarten Abschnitten volumes und environment hinzugefügt – das Grundgerüst der Datei bleibt gleich.
Hier werden zwei wichtige Punkte deutlich.
Erstens sind die „Umgebungseinstellungen“ nach Sinn getrennt: Ports separat, Variablen separat, Volumes separat. Sie suchen nicht mit den Augen nach \ und fragen sich, wo in der Zeile -e und wo -v ist.
Zweitens verlangt Compose nicht, dass Sie sich die Reihenfolge der Flags merken. In docker run können Sie -p vor -e oder danach schreiben – Docker versteht es. Aber ein Mensch liest das schwer. YAML dagegen ist von Natur aus strukturell: Sie gruppieren nach Abschnitten, nicht nach Reihenfolge.
6. Oberste Ebene: networks und volumes
Auch wenn wir heute nur einen Service haben, sehen Sie bereits, dass Compose Entitäten auf „Umgebungsebene“ hat, nicht auf „Containerebene“. Das ist ein wichtiger Gedanke, denn Compose ist nicht „das Starten eines einzelnen Containers“, sondern „die Beschreibung eines Setups“.
In der Datei gibt es Abschnitte auf oberster Ebene, die häufig vorkommen:
services – wer an der Umgebung beteiligt ist (Container);
networks – wie sie netzwerktechnisch verbunden sind;
volumes – wie Zustand/Daten über benannte Volumes gespeichert werden.
Heute reicht es, den strukturellen Moment zu verstehen: Diese Abschnitte werden auf derselben Verschachtelungsebene wie services geschrieben, nicht innerhalb von services.app. Also so (richtig):
services:
app:
build: .
volumes:
exports-data: # Deklarieren ein named volume auf oberster Ebene (als Umgebungsentität)
Und so (falsch, und YAML/Compose werden nicht begeistert sein):
services:
app:
build: .
volumes:
exports-data: # Fehler: hier wird eine Liste von Verbindungen erwartet, keine Volume-Deklaration
Innerhalb des Services verbinden Sie ein Volume, aber deklarieren es außen. Warum das so? Weil die Deklaration bedeutet: „Wir führen eine Umgebungsentität ein“, und die Verbindung bedeutet: „Wir verwenden die Umgebungsentität in einem bestimmten Service“. Diese Trennung ähnelt Java: Eine Klasse wird separat deklariert und dann an konkreten Stellen verwendet.
Noch einmal betont: Wir vertiefen uns heute nicht in das Netzwerkmodell und DNS. Wir stellen nur fest: Wenn Sie networks: und volumes: auf der obersten Ebene sehen – das ist normal, so soll es sein. Es ist keine „überflüssige Bürokratie“, sondern eine Struktur, die später das Projekt vor YAML-Chaos bewahrt.
7. Minimaler Arbeitszyklus: up, logs, down
Die Datei allein startet nichts. Sie ist wie build.gradle.kts: Solange Sie keinen Befehl aufrufen, ist es nur Text. Daher ist es wichtig, den minimalen „Arbeitszyklus“ zu kennen, der die manuellen Schritte docker build + docker run ersetzt.
Das grundlegendste Szenario sieht so aus:
docker compose up --build # Umgebung hochfahren und bei Bedarf Image neu bauen
Das Flag --build ist nützlich, wenn Sie build: . verwenden und sicherstellen möchten, dass das Image neu gebaut wird, falls Sie Code oder das Dockerfile geändert haben. Zu Beginn des Lernens macht das das Verhalten vorhersagbarer: weniger Chancen, „versehentlich das alte Image zu starten“.
Wenn der Stack hochgefahren ist, möchten Sie fast sofort die Logs sehen:
docker compose logs app # Zeigt Logs des Service app (der Name stammt aus compose.yaml)
app ist der Name des Services (nicht des Containers). Das ist der Reiz von Compose: Sie denken in „Umgebungsdiensten“, nicht in „zufälligen Containern“.
Wenn Sie die Umgebung anhalten müssen, verwenden Sie:
docker compose down # Stoppen und Löschen der erstellten Container/Netzwerke (standardmäßig)
Und hier entsteht ein sehr angenehmes Gefühl: Sie suchen nicht, wie genau der Container hieß, erinnern sich nicht an docker stop ... und docker rm .... Compose weiß selbst, was es erstellt hat, und räumt es ordentlich auf.
Wenn Sie es lieber „sauber im Terminalfenster“ haben möchten, können Sie es im Hintergrund starten (detached):
docker compose up -d --build # Start im Hintergrund, damit das Terminal sofort frei wird
Aber zu Beginn des Lernens ist es meist bequemer, ohne -d zu arbeiten, um die Logs direkt in der Konsole zu sehen und Aktion mit Symptom schneller zu verbinden.
8. Typische Fehler beim Erstellen und Lesen einer compose.yaml
Fehler Nr. 1: „Ich habe YAML geschrieben, aber Compose schimpft mit unverständlichen Wörtern.“
Meistens sind es Einrückungen. YAML verzeiht keine „leicht verschobene Ebene“, denn die Verschachtelungstiefe ist der Sinn. Die effektivste Heilungsmethode ist nicht zu raten, sondern die Datei zu öffnen und zu lesen: services → app → Service-Schlüssel. Wenn ein Schlüssel plötzlich auf einer anderen Ebene auftaucht, sehen Sie das. Und ja, manchmal ist es buchstäblich ein einzelnes Leerzeichen, das Sie 20 Minuten und ein paar philosophische Fragen an das Universum kostet.
Fehler Nr. 2: Verwechslung von build und image – „Ich dachte, es würde sich automatisch bauen.“
Wenn Sie image: docker-java-catalog-service geschrieben haben, ist Compose nicht verpflichtet, dieses image zu bauen. Es ist verpflichtet, zu versuchen, es zu verwenden. Ist das Image nicht vorhanden, erhalten Sie einen Fehler. Wenn Sie einen Bau aus dem aktuellen Repository wünschen, verwenden Sie build: .. Und umgekehrt: Wenn Sie strikt ein bestimmtes fertiges Artefakt starten möchten (insbesondere im Team), wird image ehrlicher und vorhersagbarer.
Fehler Nr. 3: Erwartung, dass Compose „alles für mich versteht“, auch wenn ich keine Ports angegeben habe.
Compose ist kein Gedankenleser. Wenn Sie ports nicht angegeben haben, kann die Anwendung innerhalb des Containers perfekt laufen, aber von außen werden Sie sie nicht über localhost:8080 erreichen. Das ist kein Defekt von Spring Boot, sondern einfach das Fehlen einer Portveröffentlichung. Compose veröffentlicht Ports nicht „standardmäßig“, weil dies unsicher und unerwartet wäre.
Fehler Nr. 4: Versuch, die erste compose.yaml zu groß zu machen.
Man möchte sofort Abschnitte „für später“ hinzufügen: networks, volumes, restart-Policies, healthchecks, profiles, depends_on, und fünf weitere Dinge, die Sie in einem fremden Repository gesehen haben. In der Praxis tötet das die Lernbarkeit. Eine richtige erste Datei liest sich auf einen Blick und beantwortet die Frage: „Wie starte ich unseren Service in dem Modus, den wir genau jetzt brauchen?“. Alles andere ist unnötiges Rauschen, das später schwer von wirklich Wichtigem zu unterscheiden ist.
Fehler Nr. 5: Vermischen von „Compose-Einstellungen“ und „Anwendungseinstellungen“ in einem Brei.
Compose verwaltet, wie der Container gestartet wird: Ports, Umgebungsvariablen, Mounts. Spring Boot verwaltet, wie sich die Anwendung innerhalb des Containers verhält: Profil, Port, Datenquelle usw. In der Compose-Datei können Sie Werte über environment in den Container hineingeben. Wenn Sie jedoch versuchen, „Spring Boot mit Compose-Schlüsseln zu konfigurieren“ (oder umgekehrt), verlieren Sie schnell den Überblick, wo genau die Ursache des Verhaltens liegt.
GO TO FULL VERSION