Loading...
Wie funktioniert Docker Networking überhaupt?! 😵‍💫

Wie funktioniert Docker Networking überhaupt?! 😵‍💫

Jonas Scholz - Co-Founder von sliplane.ioJonas Scholz
9 min

Docker erstellt virtuelle Netzwerke, die isolierten Containern die Kommunikation ermöglichen, weist IP-Adressen und Domainnamen für die interne Kommunikation zu, nutzt DNS für die Namensauflösung und macht Ports durch Network Address Translation für den Host-Rechner zugänglich, sodass eine Verbindung vom Host zu Containern möglich ist, während die Netzwerkisolierung für Sicherheit sorgt.

Hast Du Dich jemals gefragt, was eigentlich passiert, nachdem Du docker-compose up ausgeführt hast? Sind IP-Tables, DNS und virtuelle Netzwerke für Dich nur vage Schlagworte? Dann ist heute Dein Glückstag! Lies weiter und lerne etwas Neues, das Dir hilft, die Docker-Magie besser zu verstehen ✨🧑‍🎓

lol

Warnung: Dies ist ein Anfänger-Beitrag. Wenn Du bereits ein gutes Verständnis von Netzwerkaspekten hast, ist dieser Beitrag vielleicht nicht das Richtige für Dich :)

Das Setup

Wenn Du bereits irgendwas mit Docker Compose gemacht hast, wird Dir das sehr vertraut vorkommen:

services:
  web:
    build: .
    ports:
      - "8000:8000"
  db:
    image: postgres

Das ist quasi das Hello-World von Docker. Ein lokal definierter Webservice, der auf Port 8000 zugänglich ist, und eine interne Datenbank. Du weißt wahrscheinlich auch, dass durch 8000:8000 der interne Port 8000 des Containers auch auf Deinem Host-Port 8000 verfügbar wird. Dadurch kannst Du auf den Webservice zugreifen, indem Du Deinen Browser öffnest und zu http://localhost:8000 navigierst. Während die Postgres-Datenbank für Dich irgendwie nicht erreichbar ist, ist sie vom Web-Container aus erreichbar. Also beginnen wir mit dem ersten von vielen WAS?! und schauen etwas genauer hin. Was passiert eigentlich, wenn wir docker-compose up ausführen?

PS: Du kannst auch mitmachen, indem Du dieses Github-Repository klonst und die Befehle ausführst. Das sollte für MacOS und Linux funktionieren, für Windows kann ich nichts garantieren 😬

Schritt 1. docker-compose up

Wenn Du docker-compose up ausführst, passieren 3 Dinge:

  1. Ein virtuelles Netzwerk namens dockernetworking_default wird erstellt (oder wie auch immer Dein Arbeitsverzeichnis heißt + "_default")
  2. Sowohl der web- als auch der db-Container werden gestartet und dem Netzwerk hinzugefügt
  3. web macht zusätzlich den Port 8000 auf Deinem Host verfügbar

Das klingt einfach genug, aber ich frage trotzdem wieder dasselbe: WAS?!

Was bedeutet es, ein Netzwerk zu erstellen? Wie fügen wir Container zu einem Netzwerk hinzu? Und warum müssen wir überhaupt Ports für den Host verfügbar machen, es läuft doch alles auf demselben Computer, oder?!

Schritt 2. Ein "Netzwerk" erstellen

Wenn Du docker-compose up ausführst, wird ein sogenanntes "Bridge-Netzwerk" erstellt, das nur auf Deinem lokalen Rechner isoliert ist, was bedeutet, dass keine anderen Hosts darauf zugreifen können (die Alternative wäre ein Overlay-Netzwerk).

(docker-compose up macht das Äquivalent zu docker network create -d bridge my-bridge-network)

Jeder Container im Netzwerk erhält eine eindeutige IP-Adresse und einen Namen, unter dem er erreichbar ist.

Schauen wir uns genauer an, was passiert. Nach dem Ausführen von docker-compose up, führe docker network ls aus. Das listet alle Deine Docker-Netzwerke auf, eine Zeile sollte so aussehen:

docker network ls

Du kannst es dann "inspizieren" (die Konfiguration ausgeben) mit docker inspect docker-networking_default | jq (jq ist nur ein nettes Addon für JSON-Syntax-Highlighting)

Das wird dann dieses Monster von einem JSON-String ausgeben:

[
  {
    "Name": "docker-networking_default",
    "Id": "d968443acaf789d7d2e9ae18a82371f663baa3019288b18a13c5f38c432aa5df",
    "Created": "2024-12-15T17:25:27.011248Z",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": false,
    "IPAM": {
      "Driver": "default",
      "Options": null,
      "Config": [
        {
          "Subnet": "172.21.0.0/16",
          "Gateway": "172.21.0.1"
        }
      ]
    },
    "Internal": false,
    "Attachable": false,
    "Ingress": false,
    "ConfigFrom": {
      "Network": ""
    },
    "ConfigOnly": false,
    "Containers": {
      "7dc4807614c7a4660b9bd34f23dc91610afcdffad8b39af288b9160c3ce34e6e": {
        "Name": "docker-networking-web-1",
        "EndpointID": "25250386497a2d3093ac133ab0892595128c5734eedb8e039744deccfa1c5c06",
        "MacAddress": "02:42:ac:15:00:03",
        "IPv4Address": "172.21.0.3/16",
        "IPv6Address": ""
      },
      "ac105fd2a292cda4136aedd0d00c88635f2a140ba3e2359c7e0267514ed9c8b5": {
        "Name": "docker-networking-db-1",
        "EndpointID": "e26c41754ad5016b7fd9ea0777cbbaa27546209cc8c610db7648e5ed33189538",
        "MacAddress": "02:42:ac:15:00:02",
        "IPv4Address": "172.21.0.2/16",
        "IPv6Address": ""
      }
    },
    "Options": {},
    "Labels": {
      "com.docker.compose.network": "default",
      "com.docker.compose.project": "docker-networking",
      "com.docker.compose.version": "2.23.0"
    }
  }
]

Hier gibt es viele Informationen, aber ich möchte mich auf zwei wichtige Zeilen konzentrieren:

{
  "Subnet": "172.21.0.0/16",
  "Gateway": "172.21.0.1"
}

Das bedeutet, dass alle IP-Adressen Deiner Container im 172.21.0.0/16-Subnetz liegen werden, oder anders ausgedrückt zwischen 172.21.0.1 - 172.21.255.254. Wobei die erste unter 172.21.0.1 für Dein Gateway reserviert ist. Du kannst das bestätigen, indem Du die Shell Deines Web-Containers mit docker exec -ti docker-networking-web-1 sh öffnest und hostname -I ausführst. Das wird 172.21.0.3 oder 172.21.0.2 ausgeben! Übrigens, das Gateway ist (wie der Name schon sagt) das Teil, das das virtuelle Docker-Netzwerk mit dem Netzwerk Deines Hosts verbindet.

Aus dem gleichen Grund, warum wir im Internet Domainnamen statt IP-Adressen verwenden, erhält jeder Container auch einen Domainnamen, der anstelle der IP-Adresse verwendet werden kann (die sich auch ändern kann!)

Wenn Du zum Beispiel in Deinen Web-Container mit docker exec -ti docker-networking-web-1 sh gehst, kannst Du ping db ausführen:

ping db

Du solltest sehen, dass der Domainname db zu 172.21.0.2 aufgelöst wird! Ziemlich cool, oder? Aber WAS?! Woher weiß Dein Container das?!

3. DNS in Docker

Was ich Dir vorher nicht gesagt habe: Nicht nur die Gateway-IP ist reserviert, 172.21.0.1 ist auch für den internen DNS-Server von Docker reserviert.

Wenn Du noch einmal in Deinen Web-Container mit docker exec -ti docker-networking-web-1 sh gehst, kannst Du dig db ausführen, um zu sehen, wie der Hostname db von Deinem DNS aufgelöst wird.

# dig db

; <<>> DiG 9.18.18-0ubuntu0.22.04.1-Ubuntu <<>> db
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 37963
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;db. IN A

;; ANSWER SECTION:
db. 600 IN A 172.21.0.2

;; Query time: 1 msec
;; SERVER: 127.0.0.11#53(127.0.0.11) (UDP)
;; WHEN: Fri Dec 15 18:32:12 UTC 2023
;; MSG SIZE rcvd: 38

Ehrlich gesagt ist das ziemlich unleserlich. Aber wir interessieren uns nur für zwei Zeilen. Erstens sehen wir, dass es einen A-Record für db gibt, der auf 172.21.0.2 zeigt, was (wer hätte das gedacht!) auch die IP-Adresse unseres Datenbank-Containers ist.

db. 600 IN A 172.21.0.2

Darunter sehen wir, dass der von uns verwendete DNS-Server auf 127.0.0.11#53 liegt, was der Standard-DNS-Server ist.

;; SERVER: 127.0.0.11#53(127.0.0.11) (UDP)

4. Ports freigeben

Hast Du Dich jemals gefragt, warum Du Deinen Container-Port auf Deinen Host-Port mappen musst? Die abstrakte und einfache Antwort ist, dass das Netzwerk Deiner Container von dem Deines Hosts isoliert ist. Das geschieht aus Sicherheitsgründen, vermeidet aber auch Nebeneffekte bei ungewöhnlichen Setups. Um von Deinem Host aus eine Verbindung zu einem Docker-Container herzustellen, muss es also eine Art Übersetzungsschicht dazwischen geben. Zum Glück übernimmt der Docker-Daemon das für Dich! Wenn Du Deine Container mit docker-compose up startest und einige Ports gemappt hast, kannst Du diesen Befehl verwenden: lsof -i -P -n | grep LISTEN, um herauszufinden, welche Anwendungen auf welchen Ports lauschen. Irgendwo wirst Du den Host-Port finden, den Du angegeben hast. Dieser Prozess leitet den Traffic zum "versteckten" Docker-Netzwerk um. Ziemlich cool, oder? Wenn Du mehr wissen willst, empfehle ich Dir, mehr über virtuelle Netzwerke und Netzwerkisolierung in Linux zu lesen!

5. Container für das Internet freigeben

Docker-Networking endet normalerweise auf Deinem Rechner. Wenn Du Deine Apps dem Internet zugänglich machen willst, kannst Du entweder einen Reverse-Proxy wie Caddy oder ein Reverse-Tunneling-Tool wie ngrok oder Livecycle verwenden. Tools wie Caddy sind großartig für production-reife Apps, während etwas wie Livecycle eher für Prototyping/Zusammenarbeit gedacht ist. Auch wenn das natürlich nicht notwendig ist, wirst Du irgendwann Deine App mit der Welt teilen müssen :)

Ich habe bereits darüber geschrieben, wie ich Livecycle für Hackathons verwende, aber die Kurzfassung ist:

  1. Installiere die Livecycle Docker Desktop-Erweiterung
  2. Klicke auf "share" im Docker Desktop Livecycle Tab
  3. Deine lokal gehosteten Docker-Compose-Apps werden nun zu einem öffentlich verfügbaren Webserver mit einer zufälligen Domain getunnelt.
  4. Wenn Du Deine Compose-App herunterfährst, wird auch der Tunnel geschlossen!

Livecycle extension

Fazit

Es gibt so viele Dinge, die ich gerne abdecken und auch viel tiefer in technische OS-Level-Details eintauchen würde. Aber um das Ganze kurz und einigermaßen unterhaltsam zu halten, höre ich hier auf. Wenn Du Fragen hast oder Vorschläge, was ich als nächstes behandeln soll, lass es mich gerne wissen! Wie immer hoffe ich, dass Du etwas Neues gelernt hast :)

Tschüss, Jonas

Willkommen in der Container-Cloud

Sliplane macht es einfach, Container in der Cloud zu deployen und bei Bedarf zu skalieren. Probier es jetzt aus!