Docker Netzwerk erklärt (2026): bridge, host, none und eigene Netze, an einem echten Server gemessen

Docker Netzwerk erklärt (2026): bridge, host, none und eigene Netze, an einem echten Server gemessen

Docker Netzwerk erklärt in einem Satz: Jeder Container bekommt eine eigene, kleine Netzwerkwelt, und der Netzwerktreiber entscheidet, wie diese Welt mit anderen Containern, dem Host und dem Internet verbunden ist. Das klingt einfach. In der Praxis ist es die Stelle, an der die meisten Self-Hosting-Projekte zum ersten Mal hängen bleiben: Die App findet die Datenbank nicht, der Port ist von außen offen, obwohl er es nicht sein sollte, oder der Container kommt plötzlich nicht mehr ins Internet.

Wir betreiben auf unserem eigenen Server seit Jahren Dutzende Container, von kleinen Testshops bis zu produktiven Diensten. Für diesen Artikel haben wir trotzdem nichts aus dem Gedächtnis aufgeschrieben, sondern jede Aussage am 13. November 2026 auf einem echten Linux-Server mit Docker 29.9.3 nachgemessen. Dafür haben wir Wegwerf-Container mit dem Präfix gmnet- gestartet, sie gegeneinander pingen und Namen auflösen lassen und danach alles wieder gelöscht. Wo die Messung von dem abweicht, was man üblicherweise liest, steht die Messung im Text.

Das wichtigste Ergebnis vorweg, weil es fast jeden Einsteiger betrifft:

Im Standardnetz bridge konnte Container A Container B nicht beim Namen finden (ping: bad address 'gmnet-b'), per IP-Adresse aber sofort erreichen (0,096 ms). In einem selbst angelegten Netzwerk funktionierte der Name auf Anhieb.

Wenn Sie nur eine Sache aus diesem Text mitnehmen: Legen Sie für zusammengehörige Container immer ein eigenes Netzwerk an. Warum, zeigen die Messungen unten.

Falls Sie noch einen Schritt früher stehen und wissen wollen, was Container, Images und Volumes überhaupt sind, beginnen Sie mit unserem Einstieg Was ist Docker?. Wer schon mit Compose arbeitet, findet in unserem Docker Compose Beispiel einen kompletten Stack. Dieser Artikel geht eine Ebene tiefer: Was passiert eigentlich unter der Haube, wenn Docker „ein Netzwerk“ anlegt?

Leuchtende, durchscheinende Container schweben in einem dunklen Raum und sind über helle Kabel mit einem zentralen Verteiler verbunden, einige bilden farblich getrennte Gruppen

Docker Netzwerk erklärt: das Grundprinzip

Ein Container ist aus Sicht des Linux-Kernels ein normaler Prozess, der in mehreren sogenannten Namespaces läuft. Einer davon ist der Netzwerk-Namespace. Er gibt dem Prozess eigene Netzwerkschnittstellen, eine eigene Routing-Tabelle und eigene Firewall-Regeln. Der Prozess sieht die Netzwerkkarten des Hosts nicht, sondern nur das, was Docker ihm in diesen Namespace legt.

Was Docker dort hineinlegt, entscheidet der Netzwerktreiber. Auf unserem Server meldet docker info folgende Treiber:

Network: bridge host ipvlan macvlan null overlay

Für 95 % aller Anwendungsfälle auf einem einzelnen Server sind nur drei davon relevant: bridge, host und null (der Treiber hinter dem Netzwerk none). Die anderen drei stellen wir am Ende kurz vor, damit Sie wissen, wann Sie sie brauchen und wann nicht.

Direkt nach der Installation legt Docker drei Netzwerke an, die Sie mit docker network ls sehen:

NETWORK ID     NAME      DRIVER    SCOPE
efea1ab2e702   bridge    bridge    local
9fb6f0a9d365   host      host      local
705caa8f1058   none      null      local

Auf einem Server, auf dem schon Compose-Projekte laufen, kommen weitere dazu. Bei uns stehen zum Beispiel forkcart_default, shopware-test_default und getmind-demo_default in der Liste. Das Schema dahinter ist immer gleich: Compose legt pro Projekt ein Netzwerk namens <ordnername>_default an. Dazu gleich mehr, weil genau dieses automatisch angelegte Netz der Grund ist, warum Compose-Stacks sich beim Namen finden und handgestartete Container nicht.

Die drei Befehle, die Sie immer brauchen

docker network ls                      # welche Netze gibt es?
docker network inspect <name>          # Subnetz, Gateway, verbundene Container
docker inspect -f '{{json .NetworkSettings.Networks}}' <container>   # in welchen Netzen hängt ein Container?

Mit diesen drei Befehlen lassen sich fast alle Netzwerkprobleme eingrenzen, bevor man überhaupt in Logs schaut.

Der bridge-Treiber: das Standardnetz und seine Eigenheiten

Wenn Sie einen Container ohne weitere Angabe starten, landet er im Netzwerk bridge. Der Name kommt vom virtuellen Switch, den Docker auf dem Host anlegt: die Schnittstelle docker0. Auf unserem Server sieht sie so aus:

inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0

docker network inspect bridge verrät die Einstellungen des Standardnetzes:

Subnet: 172.17.0.0/16, Gateway: 172.17.0.1
com.docker.network.bridge.enable_icc: true
com.docker.network.bridge.enable_ip_masquerade: true
com.docker.network.bridge.host_binding_ipv4: 0.0.0.0
com.docker.network.driver.mtu: 1500

Drei dieser Zeilen sind wichtiger, als sie aussehen:

  • enable_icc: true heißt inter-container communication. Alle Container im Standardnetz dürfen miteinander reden, auch die, die nichts miteinander zu tun haben.
  • enable_ip_masquerade: true ist der Grund, warum Container ins Internet kommen. Ausgehende Pakete bekommen die IP des Hosts als Absender (NAT).
  • host_binding_ipv4: 0.0.0.0 heißt: Ein veröffentlichter Port lauscht standardmäßig auf allen Schnittstellen des Hosts, auch der öffentlichen. Das ist die Ursache für den Firewall-Effekt, den wir im Compose-Artikel gemessen haben.

Messung 1: Im Standardnetz gibt es keine Namensauflösung

Wir haben zwei Container ohne Netzwerkangabe gestartet und den einen den anderen anpingen lassen:

docker run -d --name gmnet-a alpine sleep 600
docker run -d --name gmnet-b alpine sleep 600
docker exec gmnet-a ping -c1 gmnet-b

Ergebnis:

ping: bad address 'gmnet-b'

Per IP-Adresse dagegen:

3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.092/0.096/0.099 ms

Die Container können also miteinander reden, sie wissen nur nicht, wie der andere heißt. Der Grund steht in der /etc/resolv.conf des Containers:

nameserver 185.12.64.2
nameserver 185.12.64.1

Das sind schlicht die DNS-Server unseres Hosts. Docker kopiert sie im Standardnetz einfach in den Container. Ein Server, der Containernamen kennt, ist nicht dabei. Die IP-Adresse fest in die Konfiguration zu schreiben, ist keine Lösung, wie Messung 6 unten zeigt: Diese Adressen ändern sich.

Früher hat man das Problem mit --link gelöst. Diese Option gilt seit Jahren als veraltet, und Sie sollten sie in neuen Projekten nicht mehr verwenden. Die richtige Lösung ist ein eigenes Netzwerk.

Eigene Netzwerke: was sich gegenüber dem Standardnetz ändert

Ein eigenes Netzwerk legen Sie mit einem einzigen Befehl an:

docker network create gmnet-user
docker run -d --name gmnet-c --network gmnet-user alpine sleep 600
docker run -d --name gmnet-d --network gmnet-user alpine sleep 600
docker exec gmnet-c ping -c3 gmnet-d

Ergebnis:

3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.081/0.104/0.128 ms

Der Name funktioniert sofort, und die Latenz ist praktisch identisch mit dem Standardnetz. Ein eigenes Netzwerk kostet also keine Leistung. Der Unterschied steckt wieder in der /etc/resolv.conf:

nameserver 127.0.0.11
options edns0 trust-ad ndots:0

Was ist 127.0.0.11?

127.0.0.11 ist der eingebettete DNS-Server von Docker. Er läuft nicht als eigener Container, sondern wird vom Docker-Daemon in den Netzwerk-Namespace jedes Containers eingeklinkt, der in einem eigenen Netzwerk hängt. Er beantwortet zwei Arten von Anfragen:

  1. Namen von Containern (und deren Aliasen) im selben Netzwerk beantwortet er selbst.
  2. Alle anderen Namen, zum Beispiel example.com, reicht er an die DNS-Server des Hosts weiter.

Deshalb kam unser Testcontainer im eigenen Netz problemlos ins Internet (wget example.com mit Rückgabewert 0), obwohl in seiner resolv.conf nur noch 127.0.0.11 steht.

Der Zusatz ndots:0 ist ein kleines Detail mit großer Wirkung: Er sorgt dafür, dass ein kurzer Name wie db direkt als vollständiger Name abgefragt wird und nicht erst mit Suchdomänen kombiniert wird. Das macht Abfragen von Containernamen schnell und vorhersagbar.

Drei gläserne Räume nebeneinander: im ersten sind Würfel über eine leuchtende Brücke verbunden, im zweiten ist ein Würfel direkt mit einer großen Maschinenwand verschmolzen, im dritten steht ein einzelner Würfel isoliert in einer Glasbox

Messung 2: Netzwerke sind voneinander getrennt, bis ins DNS

Als Nächstes wollten wir wissen, ob ein Container im eigenen Netz einen Container im Standardnetz erreicht. Der Container gmnet-b aus Messung 1 hatte die Adresse 172.17.0.5:

docker exec gmnet-c ping -c1 -W2 172.17.0.5
1 packets transmitted, 0 packets received, 100% packet loss

Und per Name:

docker exec gmnet-c nslookup gmnet-a 127.0.0.11
** server can't find gmnet-a: SERVFAIL

Zwei getrennte Ebenen also: Auf der Paketebene blockiert Docker den Verkehr zwischen verschiedenen Bridge-Netzen mit eigenen Firewall-Ketten (auf unserem Server sind das iptables-Regeln in den Ketten DOCKER-ISOLATION-STAGE-1 und -2). Auf der Namensebene kennt der DNS-Server nur die Container aus dem eigenen Netz. Für eine Datenbank heißt das: Ein Container in einem anderen Netz erfährt nicht einmal, dass sie existiert.

Das ist das stärkste Argument für eigene Netze, noch vor der Namensauflösung. Im Standardnetz kann jeder Container jeden anderen erreichen. Wenn Sie auf einem Server einen Shop, ein Wiki und ein Monitoring-Tool betreiben und alle drei im Standardnetz laufen, kann ein kompromittiertes Wiki direkt die Datenbank des Shops ansprechen. Mit getrennten Netzen geht das nicht.

Warum Compose das automatisch richtig macht

Docker Compose legt für jedes Projekt ein eigenes Netzwerk an (<projekt>_default) und hängt alle Dienste dort hinein. Deshalb funktioniert DB_HOST=db in einem Compose-Stack, während dasselbe mit zwei handgestarteten docker run-Containern scheitert. Viele Einsteiger halten das für Compose-Magie. Es ist schlicht ein eigenes Bridge-Netz mit dem DNS-Server auf 127.0.0.11.

Das hat eine Folge, die man kennen sollte: Zwei verschiedene Compose-Projekte auf demselben Server sind standardmäßig voneinander getrennt. Der Reverse Proxy aus Projekt A kann die App aus Projekt B nicht erreichen, solange Sie nicht ausdrücklich ein gemeinsames Netz anlegen. Wie das geht, steht unten im Abschnitt über externe Netzwerke.

Ein Container in mehreren Netzwerken

Ein Container kann gleichzeitig in mehreren Netzen hängen. Genau so baut man saubere Stacks: Der Reverse Proxy hängt im öffentlichen Netz, die Datenbank im internen, und nur die App hängt in beiden.

Messung 3: connect und disconnect zur Laufzeit

Wir haben den Container gmnet-a, der bis dahin nur im Standardnetz lebte, nachträglich in unser eigenes Netz gehängt:

docker network connect gmnet-user gmnet-a
docker exec gmnet-a ping -c1 gmnet-d
round-trip min/avg/max = 0.080/0.080/0.080 ms

docker inspect zeigt danach zwei Adressen:

bridge=172.17.0.4 gmnet-user=172.23.0.4

Interessant war ein Nebenbefund: Die /etc/resolv.conf von gmnet-a zeigte nach dem connect auf 127.0.0.11, nicht mehr auf die Host-DNS-Server. Sobald ein Container in einem eigenen Netz hängt, bekommt er den eingebetteten DNS-Server, auch wenn er zusätzlich noch im Standardnetz steckt.

Und zurück:

docker network disconnect gmnet-user gmnet-a
docker exec gmnet-a ping -c1 gmnet-d
ping: bad address 'gmnet-d'

Das alles passiert ohne Neustart des Containers. Praktisch, wenn Sie einem laufenden Dienst nachträglich Zugriff auf etwas geben wollen, oder ihn im Notfall schnell von einer Datenbank trennen müssen.

In Compose sieht dasselbe Muster so aus:

services:
  proxy:
    image: nginx:1.27-alpine
    ports:
      - "127.0.0.1:8080:80"
    networks: [frontend]
  app:
    image: meine-app
    networks: [frontend, backend]
  db:
    image: postgres:16-alpine
    networks: [backend]
    # absichtlich kein ports:

networks:
  frontend:
  backend:
    internal: true

Die Datenbank hat kein ports:-Mapping und hängt nur im Netz backend. Der Proxy weiß nicht, dass sie existiert. Das Zusammenspiel mit dem Reverse Proxy haben wir ausführlich in nginx als Reverse Proxy beschrieben. Was ein Reverse Proxy grundsätzlich tut, erklärt Was ist ein Reverse Proxy?.

Zwei getrennte durchscheinende Kuppeln auf einer dunklen Ebene, in jeder eine Gruppe verbundener leuchtender Würfel, ein Würfel an der Grenze ist mit beiden Kuppeln verbunden

internal: true, ein Netz ohne Ausgang

Die Zeile internal: true im Beispiel oben verdient einen eigenen Abschnitt, weil sie oft missverstanden wird. Viele glauben, sie verhindere Zugriffe von außen. Das tut sie auch, aber ihre eigentliche Wirkung betrifft die andere Richtung.

Messung 4: Ein internes Netz kommt nicht ins Internet

docker network create --internal gmnet-int
docker run --rm --network gmnet-int alpine sh -c 'wget -q -T3 -O- http://1.1.1.1; ip route'
wget: can't connect to remote host (1.1.1.1): Network unreachable
172.24.0.0/16 dev eth0 scope link  src 172.24.0.2

Die Routing-Tabelle erklärt alles: Es gibt nur eine Route ins eigene Subnetz und keine Default-Route. Der Container kann mit seinen Nachbarn im selben Netz reden, aber er weiß schlicht nicht, wohin mit Paketen an den Rest der Welt.

Für eine Datenbank ist das ideal. Sie muss nie ins Internet, und wenn jemand sie kompromittiert, kann er von dort weder Daten nach draußen schicken noch Schadcode nachladen. Für die App dagegen wäre internal: true falsch, wenn sie zum Beispiel E-Mails verschicken oder eine externe API aufrufen muss. Deshalb hängt die App im Beispiel zusätzlich in einem normalen Netz.

Eine Falle dabei: Wer eine Datenbank in ein internes Netz legt und ihr trotzdem ein ports:-Mapping gibt, wundert sich, dass der Port vom Host aus nicht erreichbar ist. Veröffentlichte Ports funktionieren in internen Netzen nicht, weil Docker für sie keine Weiterleitung einrichtet. Das ist so gewollt.

Der host-Treiber: kein eigenes Netz

Beim Treiber host bekommt der Container keinen eigenen Netzwerk-Namespace. Er benutzt direkt den des Hosts.

Messung 5: Der Container sieht exakt das, was der Host sieht

docker run --rm --network host alpine sh -c 'hostname; ip -o -4 addr | wc -l'
Nyx
9

Auf dem Host selbst:

Nyx
9

Gleicher Hostname, gleiche neun IPv4-Adressen. Der Container lebt netzwerktechnisch auf dem Host. Ein Dienst, der im Container auf Port 3000 lauscht, lauscht damit direkt auf Port 3000 des Servers, auf allen Schnittstellen.

Deshalb ist auch -p sinnlos, und Docker sagt das inzwischen deutlich:

docker run --rm --network host -p 9999:80 alpine true
WARNING: Published ports are discarded when using host network mode

Wann host sinnvoll ist

  • Viele Ports oder dynamische Ports, zum Beispiel bei VoIP-Software oder manchen Spieleservern, bei denen ein Port-Mapping für Hunderte Ports unpraktisch ist.
  • Netzwerk-Tools, die die echten Schnittstellen des Hosts sehen müssen, etwa Monitoring-Agenten oder Tools, die Netzwerkverkehr mitschneiden.
  • Maximale Leistung bei sehr vielen kleinen Verbindungen. Der NAT-Schritt der Bridge fällt weg. In unserem Ping-Test war die Bridge mit rund 0,1 ms aber schon so schnell, dass Sie diesen Unterschied bei einer normalen Webanwendung nicht messen werden.

Wann host gefährlich ist

Mit host verlieren Sie die Netzwerk-Isolation komplett. Der Container kann Dienste ansprechen, die auf dem Host nur an 127.0.0.1 lauschen, zum Beispiel eine lokale Datenbank oder eine Admin-Oberfläche, die Sie extra nicht ins Netz gestellt haben. Für normale Webanwendungen ist host deshalb keine gute Standardwahl. Nehmen Sie ihn nur, wenn Sie einen der Gründe oben haben.

Ein Hinweis noch für Mac- und Windows-Nutzer: Docker Desktop läuft dort in einer virtuellen Maschine. „Host“ ist dann die VM, nicht Ihr Rechner. Unsere Messungen gelten für Linux-Server, auf denen Docker direkt läuft.

Der none-Treiber: komplett ohne Netz

Mit --network none bekommt der Container einen eigenen Netzwerk-Namespace, in dem nichts außer der Loopback-Schnittstelle liegt:

docker run --rm --network none alpine sh -c 'ip -o addr; ping -c1 1.1.1.1'
1: lo    inet 127.0.0.1/8 scope host lo
1: lo    inet6 ::1/128 scope host
ping: sendto: Network unreachable

Keine Netzwerkkarte, keine Route, nichts. Das klingt nutzlos, ist es aber nicht. none ist die richtige Wahl für Batch-Jobs, die nur lokale Dateien verarbeiten: Bilder konvertieren, Backups verschlüsseln, unbekannte Dateien analysieren. Wenn so ein Job einmal fremden Code ausführt, etwa weil eine präparierte Datei eine Lücke im Konverter ausnutzt, kann dieser Code nirgendwohin telefonieren. Wir nutzen das selbst für Aufgaben, bei denen ein Programm Dateien aus unbekannter Quelle anfasst.

Wer die Treiber lieber einmal visuell durchgehen will: Anton Putra zeigt bridge, none, host, ipvlan, macvlan und overlay in einem Video (englisch).

Messung 6: Container-IPs sind nicht stabil

Das ist der Befund, der am häufigsten Ärger macht, weil er erst Tage oder Wochen später auffällt. Wir wollten wissen: Behält ein Container seine IP-Adresse, wenn man ihn neu startet?

Ausgangslage: gmnet-d hatte die Adresse 172.23.0.3. Wir haben ihn gestoppt, währenddessen einen neuen Container gmnet-g ins selbe Netz gestartet und gmnet-d dann wieder hochgefahren:

docker stop gmnet-d
docker run -d --name gmnet-g --network gmnet-user alpine sleep 300
docker start gmnet-d

Ergebnis:

gmnet-d: 172.23.0.7   (vorher 172.23.0.3)
gmnet-g: 172.23.0.3

Der neue Container hat sich die frei gewordene Adresse geschnappt, und gmnet-d bekam eine andere. Wer 172.23.0.3 irgendwo als Datenbank-Host eingetragen hatte, redet jetzt mit dem falschen Container. Im harmlosen Fall gibt es einen Verbindungsfehler. Im unangenehmen Fall lauscht auf der neuen Adresse zufällig ein anderer Dienst auf demselben Port.

Der Name dagegen zeigte sofort auf die neue Adresse:

PING gmnet-d (172.23.0.7): 56 data bytes

Die Regel daraus ist einfach: In Konfigurationen immer Containernamen oder Aliase verwenden, nie IP-Adressen. Der eingebettete DNS-Server hält die Zuordnung aktuell, Sie müssen nichts tun.

Und wenn ich doch eine feste IP brauche?

Manchmal braucht man sie trotzdem, zum Beispiel für eine Firewall-Regel oder ein Programm, das keine Namen kann. Das geht, aber nur in eigenen Netzen mit festgelegtem Subnetz:

docker network create --subnet 10.77.0.0/24 gmnet-sub
docker run --rm --network gmnet-sub --ip 10.77.0.50 alpine ip -o -4 addr show eth0
10.77.0.50/24

Im Standardnetz verweigert Docker das ausdrücklich:

invalid config for network bridge: invalid endpoint settings:
user-specified IP address is supported on user-defined networks only

Noch ein Argument für eigene Netze. In Compose schreiben Sie das so:

services:
  dienst:
    networks:
      festnetz:
        ipv4_address: 10.77.0.50
networks:
  festnetz:
    ipam:
      config:
        - subnet: 10.77.0.0/24

Ein eigenes Subnetz festzulegen hat einen zweiten Vorteil: Es verhindert Überschneidungen. Docker vergibt automatisch Netze aus dem Bereich 172.17.0.0/16 aufwärts (unser Testnetz bekam 172.23.0.0/16). Liegt Ihr Firmen-VPN oder ein Heimnetz zufällig im selben Bereich, sind Teile davon vom Server aus plötzlich nicht mehr erreichbar, weil der Server die Pakete in das Docker-Netz schickt. Wer das schon einmal gesucht hat, legt Subnetze danach nur noch bewusst fest.

Aliase und DNS-Round-Robin

Ein Container kann im Netz zusätzlich zu seinem Namen weitere Namen haben, sogenannte Netzwerk-Aliase. Das ist nützlich, wenn eine Anwendung fest einen bestimmten Hostnamen erwartet, zum Beispiel datenbank, Ihr Container aber anders heißt.

Messung 7: Zwei Container, ein Alias

docker run -d --name gmnet-e --network gmnet-user --network-alias datenbank alpine sleep 300
docker run -d --name gmnet-f --network gmnet-user --network-alias datenbank alpine sleep 300
docker exec gmnet-c nslookup datenbank
Address: 172.23.0.5
Address: 172.23.0.6

Der DNS-Server liefert beide Adressen zurück. In Compose passiert genau das, wenn Sie einen Dienst mit docker compose up --scale app=3 vervielfachen: Der Dienstname löst auf alle drei Container auf.

Ein Detail, das in vielen Anleitungen anders steht: In unseren drei Abfragen hintereinander kamen die Adressen jedes Mal in derselben Reihenfolge zurück. Ob ein Client die Last tatsächlich verteilt, hängt also stark vom Client ab. Viele Programme nehmen einfach die erste Adresse. DNS-Round-Robin ist deshalb kein Ersatz für einen echten Load Balancer. Wer Last wirklich verteilen will, stellt einen Proxy davor, der die Backends selbst ansteuert. Warum das so ist, steht in Was ist ein Load Balancer?.

Ein kleiner leuchtender Leuchtturm inmitten schwebender Würfel, der einzelne Lichtstrahlen zu jedem Würfel schickt, während eine entfernte Würfelgruppe außerhalb einer Kuppel kein Licht erhält

Vom Container zum Host und zurück

Zwei Fragen tauchen in jedem Forum auf: Wie erreiche ich aus einem Container einen Dienst auf dem Host? Und wie erreiche ich vom Host aus einen Container?

Vom Host zum Container

Auf Linux ist das einfach, weil der Host in jedem Bridge-Netz selbst eine Adresse hat (das Gateway). Ein Ping vom Host auf die Container-IP 172.23.0.4 klappte bei uns sofort:

rtt min/avg/max/mdev = 0.110/0.110/0.110/0.000 ms

Für Tests ist das bequem. Für den Dauerbetrieb gilt Messung 6: Die IP kann sich ändern. Dauerhafte Zugriffe vom Host laufen deshalb besser über einen veröffentlichten Port, gebunden an 127.0.0.1.

Vom Container zum Host: host.docker.internal

Unter Docker Desktop gibt es den Namen host.docker.internal automatisch. Auf einem Linux-Server nicht, das haben wir geprüft:

docker run --rm alpine ping -c1 host.docker.internal
ping: bad address 'host.docker.internal'

Mit dem Sonderwert host-gateway lässt er sich aber nachrüsten:

docker run --rm --add-host host.docker.internal:host-gateway alpine grep host.docker /etc/hosts
172.17.0.1	host.docker.internal

Docker trägt die Gateway-Adresse der Bridge in die /etc/hosts des Containers ein. In Compose:

services:
  app:
    extra_hosts:
      - "host.docker.internal:host-gateway"

Die Falle dabei: Der Dienst auf dem Host muss auch auf dieser Adresse lauschen. Eine Datenbank, die nur an 127.0.0.1 gebunden ist, ist über 172.17.0.1 nicht erreichbar, weil 127.0.0.1 im Container der Container selbst ist und nicht der Host. Sie müssen den Host-Dienst also zusätzlich an die Bridge-Adresse binden. Und dann gehört eine Firewall-Regel dazu, damit er nicht versehentlich auch auf der öffentlichen Schnittstelle hängt. Wie man so eine Regel mit ufw baut, zeigt UFW-Firewall einrichten.

Ports veröffentlichen: was -p wirklich tut

Im Bridge-Modus sind Container von außen erst einmal nicht erreichbar. Erst ein veröffentlichter Port (-p oder ports:) öffnet den Weg. Technisch legt Docker dafür eine NAT-Regel an. Auf unserem Server sieht die DOCKER-Kette in der NAT-Tabelle zum Beispiel so aus:

-A DOCKER -d 127.0.0.1/32 ! -i docker0 -p tcp -m tcp --dport 3997 -j DNAT --to-destination 172.17.0.3:80

Übersetzt: Pakete an 127.0.0.1:3997 werden an Port 80 des Containers 172.17.0.3 umgeleitet. Weil wir unsere Ports an 127.0.0.1 binden, steht die lokale Adresse in der Regel. Bei einem nackten -p 3997:80 fehlt diese Einschränkung, und die Regel gilt für alle Adressen des Servers.

Und genau hier liegt der Grund für den wichtigsten Befund aus unserem Compose-Artikel: Diese NAT-Regeln greifen, bevor die normalen Eingangsregeln von ufw überhaupt geprüft werden. Ein nacktes -p 8412:80 war bei unserer Messung von einem fremden Server aus erreichbar, obwohl ufw aktiv war und den Port nie freigegeben hatte. Die vollständige Messung steht im Docker Compose Beispiel. Die Kurzfassung für dieses Thema:

  • -p 127.0.0.1:8080:80 → nur vom Server selbst erreichbar. Davor gehört ein Reverse Proxy.
  • -p 8080:80 → von überall erreichbar, egal was ufw sagt.
  • gar kein -p → nur aus demselben Docker-Netz erreichbar. Für Datenbanken die richtige Wahl.

Die Option expose in Dockerfiles und Compose-Dateien veröffentlicht übrigens nichts. Sie ist reine Dokumentation, welcher Port im Container benutzt wird. Container im selben Netz erreichen jeden Port ohnehin, mit oder ohne expose.

Externe Netzwerke: zwei Compose-Projekte verbinden

Weil jedes Compose-Projekt sein eigenes Netz bekommt, braucht man einen Weg, um zum Beispiel einen zentralen Reverse Proxy mit mehreren Projekten zu verbinden. Der saubere Weg ist ein gemeinsames Netz, das außerhalb der Projekte angelegt wird:

docker network create proxy

Im Projekt des Proxys:

services:
  caddy:
    image: caddy:2
    ports:
      - "80:80"
      - "443:443"
    networks: [proxy]
networks:
  proxy:
    third-party: true

Im App-Projekt:

services:
  app:
    image: meine-app
    networks: [proxy, default]
  db:
    image: postgres:16-alpine
    # nur im default-Netz des Projekts
networks:
  proxy:
    third-party: true

third-party: true sagt Compose: Dieses Netz gehört nicht mir, ich lege es nicht an und lösche es bei docker compose down auch nicht. Die Datenbank bleibt im projekteigenen default-Netz, der Proxy sieht sie nicht. Nur die App ist in beiden Welten. Welchen Proxy Sie dafür nehmen, ist Geschmackssache. Unseren Vergleich finden Sie unter Caddy vs. nginx.

Messung 8: Ein Netz in Benutzung lässt sich nicht löschen

Beim Aufräumen nach den Tests stießen wir auf ein Verhalten, das Ihre Daten schützt, aber auch Skripte zum Stolpern bringt:

docker network rm gmnet-user
Error response from daemon: error while removing network: network gmnet-user has active endpoints
(name:"gmnet-d" ..., name:"gmnet-e" ..., name:"gmnet-c" ..., name:"gmnet-f" ...)

Docker löscht kein Netz, solange noch Container daran hängen, auch keine gestoppten. Deshalb meldet docker compose down gelegentlich, dass ein Netz nicht entfernt werden konnte: Meist hängt dann noch ein Container aus einem anderen Projekt daran. Die Fehlermeldung nennt die Container beim Namen, was die Suche angenehm kurz macht. docker network prune räumt alle Netze ab, an denen nichts mehr hängt. Die drei Standardnetze bridge, host und none fasst es nie an.

macvlan, ipvlan und overlay: wann Sie sie brauchen

Die drei übrigen Treiber lösen Probleme, die auf einem einzelnen Webserver selten auftreten. Sie sollten sie trotzdem kennen, damit Sie nicht zur falschen Lösung greifen.

macvlan gibt jedem Container eine eigene MAC-Adresse und damit eine eigene Adresse direkt im physischen Netz, als wäre er ein eigenes Gerät. Typischer Einsatz ist der Heimserver: Ein Pi-hole oder ein Smart-Home-Dienst soll im Heimnetz unter einer eigenen IP auftauchen. Auf gemieteten Servern funktioniert macvlan meist nicht, weil Hoster pro Server nur bestimmte MAC-Adressen durchlassen. Außerdem kann der Host selbst seine macvlan-Container ohne Zusatzschnittstelle nicht direkt erreichen, eine bekannte Eigenheit des Treibers.

ipvlan ist der Verwandte von macvlan: eigene IP im physischen Netz, aber alle Container teilen sich die MAC-Adresse des Hosts. Das umgeht Netze, die nur eine MAC pro Port erlauben, zum Beispiel manche WLAN-Setups oder Switches mit Port-Security.

overlay verbindet Container über mehrere Server hinweg zu einem gemeinsamen Netz. Das braucht man für Docker Swarm. Wer Container auf mehreren Servern verbinden will, ohne gleich einen Swarm aufzusetzen, fährt oft mit einem VPN zwischen den Servern einfacher. Unseren Vergleich der beiden gängigen Werkzeuge finden Sie unter Tailscale vs. WireGuard.

NetworkChuck baut die Treiber inklusive macvlan und ipvlan live nach. Gut, wenn Sie den Heimserver-Fall ausprobieren wollen (englisch).

Welcher Netzwerktreiber für welchen Fall?

SituationTreiberWarum
Normale Web-App mit Datenbankeigenes bridge-NetzNamensauflösung, Isolation, feste IPs möglich
Datenbank, Cache, Queueeigenes Netz mit internal: truekein Weg ins Internet, keine veröffentlichten Ports
Kurzer Test mit einem ContainerStandard-bridgereicht, solange niemand den Namen braucht
Viele oder dynamische Ports, Netzwerk-Toolshostkein NAT, sieht echte Schnittstellen
Dateien aus unbekannter Quelle verarbeitennonekein Netz, kein Rückkanal
Container mit eigener IP im Heimnetzmacvlan (oder ipvlan)erscheint als eigenes Gerät
Container über mehrere Serveroverlay (Swarm) oder VPNNetz über Rechnergrenzen

Eine leuchtende Weggabelung aus Lichtpfaden auf dunklem Grund, drei Wege führen zu drei unterschiedlich geformten Toren: eine breite Brücke, eine offene Tür und ein verschlossener Tresor

Typische Fehler und wie Sie sie finden

„Could not resolve host: db“

Fast immer hängen die beiden Container nicht im selben eigenen Netz. Prüfen Sie mit docker inspect -f '{{json .NetworkSettings.Networks}}' <container> beide Seiten. Steht bei einem nur bridge, ist das die Ursache (siehe Messung 1). Zweithäufigste Ursache: ein Tippfehler im Dienstnamen. Compose-Dienste heißen im DNS wie der Schlüssel unter services:, nicht wie container_name. Beide Namen funktionieren, aber nur, wenn man den richtigen schreibt.

„Connection refused“, obwohl der Name aufgelöst wird

Dann stimmt das Netz, aber der Dienst im Zielcontainer lauscht nur auf 127.0.0.1 statt auf 0.0.0.0. Innerhalb eines Containers heißt 127.0.0.1 „nur ich selbst“. Viele Entwicklungsserver (Vite, manche Node- und Python-Server) binden standardmäßig nur an localhost. Die Lösung ist eine Option wie --host 0.0.0.0 beim Start des Dienstes, nicht eine Änderung am Docker-Netz.

Der Container kommt nicht ins Internet

Erst prüfen, ob er in einem internal-Netz hängt (Messung 4). Dann ip route im Container: Fehlt die Default-Route, liegt es am Netz. Ist sie da, aber DNS scheitert, hilft ein Blick in /etc/resolv.conf. Eine häufige Ursache auf Servern sind eigene Firewall-Regeln, die die Weiterleitung (FORWARD) blockieren und dabei die Regeln überschreiben, die Docker angelegt hat.

Teile des Firmennetzes sind vom Server aus nicht mehr erreichbar

Ein Docker-Netz überschneidet sich mit einem echten Netz. docker network inspect zeigt die Subnetze. Lösung: Subnetze bei docker network create --subnet bewusst wählen, oder in /etc/docker/daemon.json mit default-address-pools festlegen, aus welchem Bereich Docker automatisch vergibt.

Ein Port ist von außen offen, obwohl die Firewall ihn sperrt

Das ist der NAT-Effekt aus dem Abschnitt über -p. Binden Sie den Port an 127.0.0.1 und stellen Sie einen Reverse Proxy davor. Prüfen Sie das Ergebnis immer von einem anderen Rechner aus, zum Beispiel mit curl von Ihrem Laptop. Ein Test auf dem Server selbst beweist nichts, weil der Server sich selbst immer erreicht.

Grenzen dieser Messungen

Wir haben auf einem einzelnen Linux-Server mit Docker 29.9.3 und iptables-Backend gemessen. Auf Systemen mit Docker Desktop (Mac, Windows) läuft alles in einer VM, und einige Effekte sehen dort anders aus, vor allem beim host-Treiber und bei host.docker.internal. Mit Podman statt Docker verhält sich die Namensauflösung ähnlich, die Firewall-Integration aber nicht. Die Unterschiede beschreibt Docker vs. Podman. Die Latenzwerte (rund 0,1 ms) stammen aus wenigen Pings und taugen als Größenordnung, nicht als Benchmark. Alle Testcontainer und Testnetze haben wir nach der Messung gelöscht. Danach meldete docker ps -a --filter name=gmnet- keinen Treffer mehr.

Fazit

Docker-Netzwerke wirken kompliziert, weil es sechs Treiber und viele Optionen gibt. Für einen normalen Server reduziert sich das Thema auf eine Handvoll Regeln, die wir alle selbst nachgemessen haben:

  1. Nie das Standardnetz für zusammengehörige Container. Dort gibt es keine Namensauflösung, und alle Container sehen sich gegenseitig.
  2. Eigene Netze pro Anwendung. Sie liefern DNS über 127.0.0.11 und trennen Anwendungen bis in die Namensauflösung voneinander.
  3. Namen statt IPs. Container-IPs wechseln beim Neustart, bei uns schon nach einem einzigen stop/start.
  4. Datenbanken ohne Ports und in internal-Netzen. Dann sind sie weder von außen erreichbar noch können sie nach außen telefonieren.
  5. Ports nur an 127.0.0.1 binden, davor ein Reverse Proxy. Ein nacktes -p kann an der Firewall vorbei offen sein.
  6. host und none bewusst einsetzen: host für Spezialfälle mit vielen Ports, none für alles, was fremde Dateien anfasst.

Wer diese sechs Punkte einhält, hat auf seinem Server ein Docker-Netz, das man auch nach einem Jahr noch versteht. Im nächsten Schritt lohnt sich ein Blick auf den Rest der Absicherung. Unser Artikel Linux-Server einrichten und der Artikel über Fail2ban sind gute Anschlussstellen.

Häufige Fragen

Was ist das Standard-Netzwerk in Docker?

Das Netzwerk bridge mit der Host-Schnittstelle docker0, auf unserem Server mit dem Subnetz 172.17.0.0/16 und dem Gateway 172.17.0.1. Jeder Container ohne --network-Angabe landet dort. Container können sich darin per IP erreichen, aber nicht beim Namen.

Was ist der Unterschied zwischen bridge und host?

Bei bridge bekommt der Container ein eigenes, abgeschottetes Netz mit eigener IP. Erreichbar ist er von außen nur über veröffentlichte Ports. Bei host teilt er sich das Netz direkt mit dem Server: gleicher Hostname, gleiche Adressen, jeder Port im Container ist ein Port des Servers. -p wird dabei ignoriert, Docker gibt eine Warnung aus.

Wie kommunizieren Docker-Container miteinander?

Am besten über ein eigenes Netzwerk (docker network create) oder das automatische Netz eines Compose-Projekts. Dort erreicht jeder Container die anderen über ihren Namen, weil der eingebettete DNS-Server unter 127.0.0.11 die Namen auflöst. Ports müssen dafür nicht veröffentlicht werden.

Warum findet mein Container den anderen nicht beim Namen?

Weil beide im Standardnetz bridge hängen. Dort kopiert Docker nur die DNS-Server des Hosts in den Container, die keine Containernamen kennen. Legen Sie ein eigenes Netz an und hängen Sie beide Container hinein.

Was ist 127.0.0.11 in Docker?

Der eingebettete DNS-Server des Docker-Daemons. Er steht in der /etc/resolv.conf jedes Containers, der in einem eigenen Netzwerk hängt. Containernamen aus demselben Netz beantwortet er selbst, alle anderen Anfragen leitet er an die DNS-Server des Hosts weiter.

Kann ein Container in mehreren Netzwerken gleichzeitig sein?

Ja. Mit docker network connect lässt er sich sogar zur Laufzeit zusätzlich verbinden und mit disconnect wieder trennen, ohne Neustart. Er bekommt dann pro Netz eine eigene IP. So baut man Stacks, in denen nur die App sowohl den Proxy als auch die Datenbank sieht.

Wie gebe ich einem Container eine feste IP-Adresse?

Nur in einem eigenen Netz mit festgelegtem Subnetz: docker network create --subnet 10.77.0.0/24 meinnetz, dann docker run --network meinnetz --ip 10.77.0.50 .... Im Standardnetz lehnt Docker das mit „user-specified IP address is supported on user-defined networks only“ ab. In den meisten Fällen ist ein Name aber die bessere Lösung.

Wie erreiche ich vom Container aus einen Dienst auf dem Host?

Unter Linux mit --add-host host.docker.internal:host-gateway (in Compose: extra_hosts). Docker trägt dann die Bridge-Adresse des Hosts, bei uns 172.17.0.1, in die /etc/hosts ein. Der Dienst auf dem Host muss dafür auch auf dieser Adresse lauschen, nicht nur auf 127.0.0.1.

Umgeht Docker meine Firewall?

Veröffentlichte Ports können das, ja. Docker legt NAT-Regeln an, die vor den normalen Eingangsregeln von ufw greifen. Binden Sie Ports deshalb an 127.0.0.1 (-p 127.0.0.1:8080:80) und prüfen Sie von einem anderen Rechner aus, ob sie wirklich zu sind.

Was macht internal: true?

Ein internes Netz hat keine Default-Route. Container darin erreichen sich gegenseitig, kommen aber weder ins Internet noch lassen sich ihre Ports veröffentlichen. Ideal für Datenbanken und Caches, ungeeignet für Dienste, die externe APIs oder Mailserver erreichen müssen.

Was ist der Unterschied zwischen expose und ports?

ports (oder -p) veröffentlicht einen Port auf dem Host. expose veröffentlicht nichts, es dokumentiert nur, welchen Port der Container benutzt. Andere Container im selben Netz erreichen jeden Port, egal ob er mit expose markiert ist oder nicht.

Wann brauche ich macvlan?

Wenn ein Container als eigenes Gerät mit eigener IP im physischen Netz erscheinen soll, typischerweise auf einem Heimserver für Dienste wie Pi-hole. Auf gemieteten Servern funktioniert macvlan meist nicht, weil der Hoster fremde MAC-Adressen blockiert.