Ein praktischer Leitfaden für benutzerdefinierte Docker-Netzwerke und Container-Kommunikation

Dieser Leitfaden bietet eine praktische Erkundung benutzerdefinierter Docker-Bridge-Netzwerke und ihrer Rolle bei der Container-Kommunikation. Erfahren Sie, wie Sie Container mit der Docker-CLI und Docker Compose erstellen, verwalten und verbinden. Entdecken Sie, wie benutzerdefinierte Netzwerke automatische DNS-Auflösung ermöglichen, die Isolation verbessern und die Kommunikation zwischen Diensten vereinfachen, was zu robusteren und skalierbareren containerisierten Anwendungen führt.

Ein praktischer Leitfaden für benutzerdefinierte Docker-Netzwerke und Container-Kommunikation

Benutzerdefinierte Docker-Netzwerke gehören zu den Funktionen, die sich optional anfühlen, bis Sie mehr als einen Container ausführen. Die Standard-Bridge eignet sich für einen schnellen Test, aber eine benutzerdefinierte Bridge bietet Ihnen vorhersagbare Dienstnamen, sauberere Isolation und einfachere Fehlerbehebung. Für eine kleine App mit einem Web-Container, einem API-Container und einer Datenbank ist der Unterschied sofort spürbar: Die API kann sich mit db:5432 verbinden, anstatt der IP hinterherzujagen, die Docker heute zugewiesen hat.

Dieser Leitfaden konzentriert sich auf benutzerdefinierte Bridge-Netzwerke auf einem einzelnen Docker-Host. Overlay-Netzwerke, Kubernetes-Netzwerke und Swarm-Service-Discovery lösen ähnliche Probleme in Multi-Host-Setups, aber das Bridge-Netzwerk ist immer noch das tägliche Werkzeug für die lokale Entwicklung, kleine Bereitstellungen und Docker Compose-Projekte.

Warum die Standard-Bridge umständlich wird

Docker erstellt automatisch ein Netzwerk namens bridge. Wenn Sie Container ohne Angabe eines Netzwerks ausführen, landen sie normalerweise dort. Es funktioniert für einfache Fälle, ist aber für Multi-Container-Anwendungen nicht angenehm.

In einem benutzerdefinierten Bridge-Netzwerk bietet Docker integriertes DNS für Containernamen und Compose-Dienstnamen. In der Standard-Bridge ist die namensbasierte Erkennung eingeschränkt und Legacy-Linking ist kein Muster, um das Sie herum bauen sollten. Das praktische Ergebnis ist, dass benutzerdefinierte Netzwerke es Ihnen ermöglichen, Anwendungen mit stabilen Hostnamen zu konfigurieren:

DATABASE_HOST=db
REDIS_HOST=redis
API_BASE_URL=http://api:3000

Das ist einfacher zu lesen, einfacher zwischen Maschinen zu verschieben und weniger fragil als Container-IP-Adressen.

Benutzerdefinierte Netzwerke schaffen auch eine klarere Grenze. Container, die mit demselben Netzwerk verbunden sind, können miteinander kommunizieren. Container in verschiedenen Netzwerken können dies nicht, es sei denn, Sie verbinden einen Container mit beiden oder veröffentlichen Ports über den Host. Dies ist kein vollständiges Sicherheitsmodell, aber es ist eine nützliche Trennungsebene.

Erstellen Sie ein Netzwerk mit der Docker-CLI

Erstellen Sie eine benutzerdefinierte Bridge:

docker network create app-net

Starten Sie zwei Container darauf:

docker run -d --name db --network app-net -e POSTGRES_PASSWORD=devpass postgres:16
docker run -d --name adminer --network app-net -p 8080:8080 adminer

Vom adminer-Container aus lautet der Datenbank-Hostname db. Sie müssen seine IP nicht kennen.

Inspizieren Sie das Netzwerk:

docker network inspect app-net

Sie sehen den Treiber, das Subnetz, das Gateway und die verbundenen Container. Bei der Fehlerbehebung beantwortet dieser Befehl eine grundlegende Frage: Sind die beiden Container tatsächlich im selben Netzwerk?

Sie können einen vorhandenen Container anhängen:

docker network connect app-net some-container

Und ihn trennen:

docker network disconnect app-net some-container

Docker wird ein Netzwerk nicht entfernen, während noch Container verbunden sind. Trennen Sie zuerst die Verbindung oder entfernen Sie die Container:

docker network rm app-net

Veröffentlichte Ports unterscheiden sich von Container-zu-Container-Ports

Ein häufiges Missverständnis: Container im selben Docker-Netzwerk benötigen keine veröffentlichten Host-Ports, um miteinander zu kommunizieren. Veröffentlichte Ports sind für Datenverkehr gedacht, der vom Host oder von außerhalb des Hosts kommt.

Wenn ein API-Container auf Port 3000 lauscht und ein Web-Container im selben Netzwerk ist, kann der Web-Container aufrufen:

http://api:3000

Sie benötigen -p 3000:3000 nur, wenn Sie die API von Ihrem Laptop-Browser oder einem anderen Host über den Docker-Host erreichen möchten.

Das bedeutet, dass Ihre Datenbank in einem produktionsähnlichen Setup normalerweise keinen Host-Port veröffentlichen sollte, es sei denn, etwas außerhalb von Docker benötigt direkten Zugriff. Lassen Sie die API stattdessen db:5432 über das private Docker-Netzwerk erreichen.

Verwenden Sie Compose für normale Multi-Service-Apps

Docker Compose erstellt ein Standardnetzwerk für das Projekt, auch wenn Sie keins definieren. Dienste können sich gegenseitig über den Dienstnamen erreichen:

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    depends_on:
      - api

  api:
    image: my-api:latest
    environment:
      DATABASE_HOST: db

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass

In dieser Datei kann api db über den Hostnamen db erreichen. web kann api über den Hostnamen api erreichen, vorausgesetzt, die Anwendungskonfiguration zeigt dorthin.

Sie können auch benannte Netzwerke definieren, wenn Sie eine klarere Absicht oder Trennung wünschen:

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    networks:
      - frontend

  api:
    image: my-api:latest
    environment:
      DATABASE_HOST: db
    networks:
      - frontend
      - backend

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

Hier kann web nicht direkt mit db kommunizieren, da sie kein Netzwerk teilen. api ist die Brücke zwischen den beiden Anwendungsschichten. Dies ist eine nützliche Form für reale Dienste: Stellen Sie nur den Edge-Dienst dem Host zur Verfügung, halten Sie die Datenbank privat und verbinden Sie jeden Dienst nur dort, wo er kommunizieren muss.

depends_on ist keine Bereitschaft

Compose depends_on steuert die Startreihenfolge in der üblichen Compose-Nutzung, garantiert aber nicht, dass die Datenbank bereit ist, Verbindungen anzunehmen. Ihre API kann nach dem Start des db-Containerprozesses starten und trotzdem fehlschlagen, weil PostgreSQL initialisiert wird.

Behandeln Sie die Bereitschaft in der Anwendung mit Wiederholungen oder verwenden Sie einen Health Check und eine Compose-Konfiguration, die die Service-Gesundheit für Ihre Compose-Version und Ihren Workflow respektiert. Selbst dann ist die anwendungsebene Wiederholungslogik immer noch die zuverlässigste Gewohnheit, da Datenbanken nach dem ersten Start neu starten können.

Eine praktische API-Konfiguration verwendet DATABASE_HOST=db und wiederholt die Verbindung für einen kurzen Zeitraum, bevor sie mit einem klaren Fehler beendet wird.

Benutzerdefinierte Subnetze sind nützlich, aber übertreiben Sie es nicht

Sie können ein Subnetz wählen:

docker network create --subnet 172.28.0.0/16 app-net

In Compose:

networks:
  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

Dies hilft, wenn das automatische Subnetz von Docker mit einem VPN, einem Büronetzwerk oder einer anderen Route auf dem Host überlappt. Es wird für die meisten Projekte nicht benötigt. Das Hartcodieren von Container-IPs sollte selten sein; Dienstnamen sind normalerweise der bessere Vertrag.

Fehlerbehebung bei der Netzwerkkommunikation

Wenn ein Container einen anderen nicht erreichen kann, überprüfen Sie in dieser Reihenfolge:

  1. Laufen beide Container?
  2. Sind sie mit demselben Netzwerk verbunden?
  3. Verwendet der Client den Dienst-/Containernamen, nicht localhost?
  4. Lauscht der Server auf dem erwarteten Port und Interface?
  5. Wird der Port nur veröffentlicht, wenn Host-Zugriff benötigt wird?

Der localhost-Fehler ist besonders häufig. Innerhalb eines Containers bedeutet localhost denselben Container, nicht den Docker-Host und nicht einen anderen Dienst. Wenn die API versucht, sich mit localhost:5432 zu verbinden, sucht sie nach PostgreSQL im API-Container. Verwenden Sie db:5432, wenn der Datenbankdienst db heißt.

Inspizieren Sie Netzwerke:

docker network inspect app-net

Führen Sie einen temporären Diagnosecontainer im selben Netzwerk aus:

docker run --rm -it --network app-net alpine sh

Installieren oder verwenden Sie darin nach Bedarf verfügbare Tools:

getent hosts db
nc -vz db 5432

Minimale Images haben möglicherweise nc, curl oder DNS-Tools nicht installiert. Ein kurzlebiger Debug-Container ist oft sauberer, als Fehlerbehebungspakete zu Ihrem Anwendungsimage hinzuzufügen.

Ein sinnvolles Standardmuster

Verwenden Sie für die meisten Single-Host-Apps Compose und lassen Sie es das Projektnetzwerk erstellen. Fügen Sie explizite Netzwerke hinzu, wenn Sie eine Trennung benötigen, wie z. B. frontend und backend. Verwenden Sie Dienstnamen für internen Datenverkehr. Veröffentlichen Sie nur die Ports, die Menschen, Reverse-Proxys oder externe Systeme erreichen müssen.

Das ergibt ein Setup, das leicht zu erklären ist:

  • Browser erreicht localhost:8080, weil web einen Port veröffentlicht.
  • web erreicht api über das Docker-Netzwerk.
  • api erreicht db über das Backend-Netzwerk.
  • db hat keinen Host-Port, es sei denn, es gibt einen echten betrieblichen Grund.

Benutzerdefinierte Docker-Netzwerke sind nicht nur eine nette Funktion. Sie sind der Unterschied zwischen Containern, die zufällig auf derselben Maschine laufen, und Diensten, die ein klares Kommunikationsmodell haben.

Netzwerkaliase können Migrationen erleichtern

Manchmal erwartet eine Anwendung einen Hostnamen, den Sie nicht als Compose-Dienstnamen verwenden möchten. Sie können einen Alias in einem Netzwerk hinzufügen:

services:
  postgres:
    image: postgres:16
    networks:
      backend:
        aliases:
          - database

networks:
  backend:
    driver: bridge

Container im backend-Netzwerk können den Dienst jetzt als postgres oder database erreichen. Dies ist praktisch, wenn Sie eine ältere App migrieren, die bereits DATABASE_HOST=database verwendet, aber ich würde Aliase nicht überall verwenden. Dienstnamen sind einfacher, wenn Sie die Anwendungskonfiguration kontrollieren.

Host-Zugriff ist ein separates Problem

Ein Container, der mit einem anderen Container spricht, unterscheidet sich von einem Container, der mit dem Docker-Host spricht. Auf Docker Desktop ist host.docker.internal allgemein verfügbar. Unter Linux hängt die Unterstützung von der Docker-Version und -Konfiguration ab; viele Teams fügen es explizit hinzu, wenn nötig:

docker run --add-host=host.docker.internal:host-gateway ...

Verwenden Sie dies sparsam. Wenn ein Container stark von Diensten abhängt, die direkt auf dem Host laufen, kann Ihr Setup in CI oder auf dem Rechner eines anderen Entwicklers schwerer reproduzierbar sein. Für Datenbanken und Caches ist es normalerweise sauberer, die Abhängigkeit als einen anderen Dienst im selben Docker-Netzwerk auszuführen.

Interne Ports sollten mit dem Prozess übereinstimmen, nicht mit dem Dockerfile-Kommentar

Docker-Netzwerke kümmern sich nicht darum, was eine Dockerfile EXPOSE-Zeile sagt, es sei denn, ein Tool verwendet sie als Metadaten. Die Anwendung muss tatsächlich auf dem Port lauschen, den Sie aufrufen. Wenn eine Node-App auf 3000 lauscht, sollten andere Container api:3000 verwenden, auch wenn jemand versehentlich EXPOSE 8080 geschrieben hat.

Überprüfen Sie auch die Bind-Adresse. Ein Dienst, der innerhalb seines Containers auf 127.0.0.1 lauscht, ist möglicherweise von anderen Containern aus nicht erreichbar. Für Container-zu-Container-Datenverkehr muss der Prozess normalerweise auf 0.0.0.0 oder dem Netzwerk-Interface des Containers lauschen.

Halten Sie das Netzwerkdesign langweilig

Es ist verlockend, viele Netzwerke zu erstellen, weil die Funktion existiert. Beginnen Sie mit den Kommunikationspfaden, die Sie tatsächlich benötigen. Eine kleine App benötigt möglicherweise nur das Standard-Compose-Netzwerk. Eine realistischere Web-App benötigt möglicherweise frontend und backend. Darüber hinaus sollte jedes neue Netzwerk einen Grund haben, den jemand während eines Vorfalls erklären kann.

Gutes Netzwerkdesign erleichtert die Fehlerbehebung. Wenn web db nicht erreichen kann und Sie wissen, dass sie absichtlich kein Netzwerk teilen, ist die Antwort architektonisch und nicht mysteriös. Wenn jeder Dienst mit jedem Netzwerk verbunden ist, dokumentiert das Netzwerk nichts mehr.

Ein echter Review-Durchlauf vor dem Ausliefern

Bevor Sie ein Skript oder Container-Setup als abgeschlossen bezeichnen, lesen Sie es einmal so, als wären Sie die nächste Person, die es um 2 Uhr morgens debuggen muss. Das ändert, was Ihnen auffällt. Eine Eingabeaufforderung, die beim Schreiben des Skripts sinnvoll war, kann in einem CI-Protokoll mehrdeutig sein. Ein Docker-Dienstname, der offensichtlich schien, passt möglicherweise nicht zum Variablennamen in der Anwendung. Ein Bash-Standardwert mag für die Entwicklung sicher und für die Produktion gefährlich sein.

Ich mache gerne einen kurzen Trockentest mit bewusst ungünstigen Werten. Verwenden Sie einen Pfad mit Leerzeichen. Verwenden Sie einen leeren optionalen Wert. Versuchen Sie einen Dateinamen, der mit einem Bindestrich beginnt. Führen Sie das Skript aus einem anderen Arbeitsverzeichnis aus. Starten Sie den Container ohne eine erwartete Umgebungsvariable. Diese Tests sind nicht ausgefallen, aber sie fangen die Annahmen, die normalerweise zuerst brechen.

Überprüfen Sie auch die Fehlermeldung. Wenn die einzige Ausgabe fehlgeschlagen ist, ist der Rat des Artikels nicht in die Implementierung eingeflossen. Ein nützlicher Fehler sagt, welcher Wert verwendet wurde, welche Prüfung fehlgeschlagen ist und was der Bediener ändern kann. Das bedeutet nicht, jede Umgebungsvariable auszugeben oder Geheimnisse zu drucken. Es bedeutet, dort spezifisch zu sein, wo Spezifität hilft: der Konfigurationspfad, der fehlende Befehlsname, der Netzwerkname, der Diensthostname oder der Port, den der Prozess zu binden versuchte.

Die letzte Gewohnheit ist, Beispiele nah an der Art und Weise zu halten, wie das System tatsächlich ausgeführt wird. Wenn die Produktion Compose verwendet, testen Sie mit Compose. Wenn ein Skript von systemd gestartet wird, testen Sie es mit systemd oder einer ähnlich minimalen Umgebung. Wenn ein Befehl zum Kopieren und Einfügen sicher sein soll, fügen Sie die Anführungszeichen, ---Trennzeichen und Validierung im Beispiel selbst hinzu. Leser kopieren funktionierende Muster häufiger, als sie Warnungen kopieren.

Dieser Review-Durchlauf ist keine Bürokratie. Es ist, wie kleine Automatisierung langweilig bleibt. Langweilig ist, was Sie von Shell-Eingabeaufforderungen, Konfigurationsladern, Variablenerweiterung, Container-Diagnose und Docker-Netzwerken wollen. Je weniger überraschend das Verhalten ist, desto einfacher ist es für den nächsten Bediener, ihm zu vertrauen.

Für Docker-Netzwerke dokumentieren Sie den beabsichtigten Datenverkehrspfad neben der Compose-Datei oder in der Service-README. Eine kurze Notiz wie web -> api:3000 -> db:5432 verhindert viel Verwirrung. Es erleichtert auch Reviews: Wenn jemand den Datenbank-Port veröffentlicht oder web mit dem Backend-Netzwerk verbindet, muss sich die Änderung gegenüber dem beabsichtigten Pfad rechtfertigen.

Wenn die App wächst, überprüfen Sie die Netzwerkkarte erneut. Alte Aliase, ungenutzte veröffentlichte Ports und Dienste, die mit Netzwerken verbunden sind, die sie nicht mehr benötigen, sind leise Quellen operationeller Risiken.