1. Einführung
Als die Anwendung noch der einzige Container war, schien fast jede Adressierung „logisch“: Sie haben den Port weitergeleitet und http://localhost:8080 aufgerufen. Doch sobald ein zweiter Container hinzukommt, macht das Gehirn aus Gewohnheit weiter mit „alles läuft auf meinem Laptop“. Genau hier schreibt ein Anfänger meistens jdbc:postgresql://localhost:5432/... und wundert sich aufrichtig, warum die Datenbank „nicht sichtbar“ ist, obwohl der PostgreSQL-Container scheinbar läuft.
Der Hauptgedanke dieser Lektion ist einfach: localhost ist nicht „mein Computer allgemein“, sondern „die aktuelle Maschine/der aktuelle Container, in dem ich mich befinde“. In einer Compose-Umgebung befinden Sie sich innerhalb des Containers app, während PostgreSQL im benachbarten Container postgres lebt. Daher sollte die Adresse nicht die Loopback localhost sein, sondern der Servicename.
2. Zwei verschiedene „Welten“: Host-Maschine und Container
Es ist sehr bequem (und sehr gefährlich) zu denken, ein Container sei „eine kleine VM“, und daher sei localhost quasi „überall localhost“. Leider nein. Für unsere praktische Ebene reicht es zu akzeptieren: Die Host-Maschine und jeder Container haben ihren eigenen Netzwerk-„Raum“, und innerhalb dieses Raums bedeutet das Wort localhost immer „ich selbst“, nicht „die gesamte Wohnung“.
Es gibt also zwei verschiedene Kontexte, die ständig verwechselt werden:
- Host-Maschine (Ihr Laptop / Ihre Workstation). Hier ist localhost Ihr physischer Computer.
- Container app. Hier ist localhost der Container app.
- Container postgres. Hier ist localhost der Container postgres.
Compose sorgt dafür, dass Container über das Netzwerk kommunizieren können, aber das ändert nichts an der Tatsache, dass localhost innerhalb jedes Containers ein „Egoist“ bleibt und auf sich selbst verweist.
Um das Bild zu festigen, kann man sich das folgende Schema vorstellen:
flowchart LR %% Host -> container: Zugriff vom Laptop auf den veröffentlichten Port %% App -> Pg: internes Compose-Netzwerk (hier ist localhost fast immer ein Fehler) Host["Host-Maschine
localhost"] -->|Port veröffentlicht| App["container: app
localhost innerhalb von app"] Host -->|Port veröffentlicht| Pg["container: postgres
localhost innerhalb von postgres"] App -->|internes Compose-Netzwerk| Pg
Der Pfeil Host -> App ist Ihr Browser/curl auf der Maschine. Der Pfeil App -> Pg ist das, was die Anwendung innerhalb des Compose-Netzwerks tut. Und bei diesem zweiten Pfeil ist localhost fast immer falsch.
3. Die Bedeutung von localhost innerhalb eines Containers
Wenn Sie localhost in einer Netzwerkadresse verwenden, wird dies normalerweise in 127.0.0.1 (Loopback) aufgelöst. Im alltäglichen Sinne bedeutet das „verbinde mich mit mir selbst“. Auf der Host-Maschine bedeutet es „verbinde mich mit einem Prozess auf meinem Laptop“. In einem Container bedeutet es „verbinde mich mit einem Prozess innerhalb desselben Containers“. Der Container ist kein Gedankenleser; er kann nicht erraten, dass Sie den „benachbarten Container“ gemeint haben.
Schauen wir uns ein typisches falsches Fragment von compose.yaml an (genau so machen es Leute aus Gewohnheit):
services:
app:
environment:
# ❌ localhost innerhalb des Containers app bedeutet "dieser Container app", nicht postgres
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/${POSTGRES_DB}
Warum ist das schlecht? Weil Spring Boot innerhalb des Containers app ehrlich versucht, PostgreSQL innerhalb des Containers app auf Port 5432 zu finden. Aber innerhalb von app läuft ein Java-Prozess, keine Datenbank. Das Ergebnis sieht meistens aus wie „connection refused“ (und manchmal noch lustiger, wenn innerhalb des Containers zufällig etwas auf diesem Port lauscht).
Wenn Sie PostgreSQL auf Ihrer Host-Maschine (nicht in Compose) ausgeführt haben und die Anwendung lokal gestartet haben – dann war jdbc:postgresql://localhost:5432/... die richtige Lösung. Aber in einer Compose-Umgebung ist PostgreSQL kein Prozess auf der Host-Maschine mehr, sondern ein separater Container.
4. Service-Name als Host: integriertes DNS in Docker Compose
Damit Container einander nicht „nach Bild“ (IP-Adressen) suchen müssen, bietet Docker Compose eine sehr praktische Sache: Die Servicenamen werden zu DNS-Namen innerhalb des Compose-Netzwerks. Das heißt, wenn es in compose.yaml einen Service postgres gibt, kann vom Container app aus auf den Host postgres zugegriffen werden – und Docker leitet den Verkehr automatisch in den richtigen Container.
Minimal sieht das so aus: Wir teilen der Anwendung mit, dass der Host der Datenbank postgres und nicht localhost ist.
services:
app:
environment:
# ✅ Innerhalb des Compose-Netzwerks verwenden wir den Servicenamen (postgres) und den Container-Port (5432)
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB}
postgres:
image: postgres
Hier sind zwei Ideen wichtig.
Erstens: postgres ist keine „magische Domain“, sondern genau der Name des Services im YAML. Compose richtet ein Netzwerk für das Projekt ein und fügt einen DNS hinzu, der weiß: „Wenn jemand nach postgres fragt, leite ihn zum Container des Services postgres.“
Zweitens: Dieser Ansatz ist skalierbar. Heute haben Sie postgres, morgen kommt redis hinzu, dann rabbitmq. Sie werden nicht die IP umschreiben oder localhost „hartcodieren“, sondern einfach die Services beim Namen nennen, wie normale Teilnehmer der Umgebung.
Wenn man eine Analogie möchte: localhost ist der Ruf „Hey, ich!“, den nur der Rufende hört. Ein Service-Name hingegen ist die Anrede mit Namen: „Peter, komm her.“ Im Compose-Netzwerk wird „Peter“ ohne Rätselraten gefunden.
5. Ports: Container und Host – unterschiedliche Dinge
Bei Ports gibt es bei Anfängern genau zwei häufige Verwechslungen: Erstens denken die Leute, die Veröffentlichung eines Ports sei nötig, damit Container einander „sehen“; zweitens setzen sie in die Intercontainer-URL den Host-Port ein, obwohl innerhalb von Compose der Container-Port gilt. Beide Punkte führen zu unerwarteten Fehlern, weil alles fast richtig aussieht, bis auf eine Zahl.
Hier ist ein typisches Fragment, in dem PostgreSQL auf der Host-Maschine veröffentlicht wird:
services:
postgres:
ports:
- "55432:5432" # host:container (nach außen 55432, innerhalb des Netzwerks bleibt es 5432)
Das bedeutet Folgendes: Auf Ihrer Host-Maschine ist PostgreSQL unter localhost:55432 erreichbar. Aber innerhalb des Compose-Netzwerks lauscht PostgreSQL weiterhin auf 5432. Daher muss sich der Container app mit postgres:5432 verbinden, nicht mit postgres:55432 und schon gar nicht mit localhost:55432.
Kurz gesagt: ports: ist eine „Tür nach außen“ zum Host, kein „Kabel zwischen Containern“.
6. Adressierung: Woher und wohin verbinden wir uns?
Um diese Verwirrung nicht jedes Mal neu zu erleben, ist es hilfreich, einmal eine Tabelle „wer verbindet sich wohin“ speziell für unsere Umgebung im Kopf zu haben. Es ist wie im Büro: ein Eingang für Mitarbeiter, ein anderer für Besucher, und wenn man sie verwechselt, schaut der Sicherheitsdienst misstrauisch. Compose verhält sich ähnlich – es ist nicht böse, es befolgt nur buchstäblich die Anweisungen.
Hier ist eine praktische Gedächtnisstütze für app + postgres:
| Wer stellt die Anfrage | Wohin | Adresse (Host) | Port | Warum |
|---|---|---|---|---|
| Sie auf der Host-Maschine (Browser/curl) | zur Anwendung | localhost | 8080 (oder Ihr Port) | API der Anwendung prüfen |
| Anwendung im Container app | zu PostgreSQL | postgres | 5432 | Arbeitsverbindung zur DB innerhalb von Compose |
| Sie auf der Host-Maschine (psql, DBeaver) | zu PostgreSQL (falls nötig) | localhost | 55432 (falls so veröffentlicht) | Von außen mit der DB verbinden |
Hier geht es nicht darum, die Zahlen zu merken (sie können geändert werden), sondern das Prinzip zu verinnerlichen: Zwischen Containern funktioniert der Service-Name + Container-Port. Der Host-Port taucht nur auf, wenn Sie ihn explizit nach außen veröffentlichen.
7. Schnellcheck: Suchen Sie nach überflüssigem localhost
Wenn Sie eine Umgebung zusammenstellen, möchte man seinen Augen glauben: Die Container laufen („Running“), also ist alles gut. Aber Netzwerk und Konfiguration sind tückisch: Ein Container kann „Running“ sein, während die Anwendung darin bereits an einem fehlgeschlagenen Verbindungsversuch zur Datenbank gestorben ist, oder umgekehrt – die Anwendung lebt, läuft aber im In-Memory-Modus, weil Sie versehentlich die falsche Adresse angegeben haben. Daher ist ein kurzer Check sinnvoll: „Habe ich die Adressen nicht verwechselt?“
Der einfachste Weg ist, buchstäblich mit den Augen in compose.yaml die Verbindungszeichenfolge zu suchen und sicherzustellen, dass dort kein localhost steht, wo es nicht hingehört:
services:
app:
environment:
# Wenn Sie hier localhost sehen, sind Sie mit ziemlicher Sicherheit auf dem falschen Realitätszweig
# Richtiger Anhaltspunkt: Servicename (postgres) + Container-Port (5432)
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB}
Wenn Sie etwas mehr „Realitätstest“ über die CLI wünschen, können Sie Compose bitten, die Umgebung innerhalb des Containers app anzuzeigen:
# Befehl innerhalb des Containers app ausführen
docker compose exec app env | grep SPRING_DATASOURCE_URL
# Vergleichen: Host sollte postgres sein, Port 5432 (Container-Port)
# SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/catalog
Dieser Befehl ist gut, weil er kein Verständnis von Spring, JPA oder Magie erfordert. Er bestätigt einfach: Im Container ist tatsächlich der Wert angekommen, den Sie erwarten, und er ist nicht aufgrund eines Tippfehlers, falscher Anführungszeichen oder einer falschen Variablensubstitution „verrutscht“.
8. Typische Fehler bei der Adressierung innerhalb von Compose
Fehler Nr. 1: localhost in SPRING_DATASOURCE_URL „aus Gewohnheit“.
Dies ist das häufigste Szenario, weil es bei einem lokalen Start ohne Docker normalerweise so funktioniert. Aber innerhalb des Containers app ist localhost „ich selbst“, also der Container app. PostgreSQL lebt in einem anderen Container, daher sollte die Adresse der Service-Name sein, z. B. postgres.
Fehler Nr. 2: Verwechslung von Host-Port und Container-Port.
Wenn Sie ports: "55432:5432" schreiben, neigt man leicht zu der Annahme, „PostgreSQL ist jetzt auf 55432“. Für die Host-Maschine stimmt das, für andere Container nicht. Der Container app muss sich mit postgres:5432 verbinden. Wenn Sie versehentlich postgres:55432 verwenden, erhalten Sie eine Verbindungsfehlermeldung, obwohl von außen (vom Host) alles lebendig aussieht.
Fehler Nr. 3: Der Glaube, dass die Veröffentlichung des Ports die Intercontainer-Kommunikation „repariert“.
Die Veröffentlichung eines Ports (ports) ist dafür da, dass Sie von der Host-Maschine aus auf den Service zugreifen können. Für die Kommunikation app -> postgres ist sie überhaupt nicht erforderlich. Container im selben Compose-Netzwerk sehen einander auch ohne Veröffentlichung. Daher ist „lassen Sie uns den Port öffnen, dann wird es schon funktionieren“ oft die Behandlung der falschen Krankheit.
Fehler Nr. 4: Der Versuch, ein Netzwerkproblem durch Änderung des Java-Codes zu beheben.
Wenn Spring Boot etwas Schreckliches wie Connection refused ins Log schreibt, greift man instinktiv zu: „Dann ändere ich mal die URL im Code, mache ein if (docker) ...“. In unserem Kurs gilt das als Anti-Pattern: Die Umgebung wird durch Konfiguration und Compose geändert, nicht durch bedingte Verzweigungen im Code. In den meisten Fällen haben Sie einfach den falschen Host oder Port angegeben.
Fehler Nr. 5: Verlass auf container_name als den „wahren Namen“ des Containers.
Manchmal möchte man container_name: my-postgres festlegen und als Host verwenden. Technisch kann das funktionieren, aber Sie verlieren den Hauptvorteil von Compose: den Servicenamen als Teil des deklarativen Modells. Es ist viel robuster, den Servicenamen (postgres) zu verwenden, da er logisch, lesbar ist und Ihre Umgebung nicht „manuell“ und zerbrechlich macht.
GO TO FULL VERSION