In meinen ersten Artikeln über Docker bin ich kurz auf die kleinen Stolperfallen bei der Installation unter Ubuntu 15.04 eingegangen und habe in einem zweiten Artikel erläutert, wie Docker mit geänderten Daten umgeht. In diesem Artikel gehe ich ein wenig auf die Netz-Besonderheiten von Docker ein und zeige anhand einer einfachen Web-Applikation wie mächtig das Docker Container Linking ist. Den vorläufigen Abschluss meiner kleinen Artikelserie bildet eine Anleitung zu Remote Debugging via XDebug im Docker Container.
Auch hier der Hinweis vorweg: Ich lasse in allen Beispielen das Kommando “sudo” weg. Das dient zum Einen der Lesbarkeit, zum Anderen kann man bei der Benutzung von boot2docker sudo nicht verwenden.
Willkommen in Neuland!
Wie so häufig möchte ich ein bisschen tiefer in die Materie einsteigen als unbedingt nötig wäre um Docker einfach nur zu benutzen. Daher werde ich ein paar Sätze zum Docker-Netz sagen. Wer es noch genauer wissen möchte kann es in der offiziellen Dokumentation nachlesen.
Docker verwendet eine Reihe von Tricks um die Sichtbarkeit von Anwendungen innerhalb der Container kontrollierbar zu machen. Die Container werden in eigenes Subnetze auf dem Host gesperrt. Darin haben alle Container eine Adresse aus einem privaten Adressebereich nach RFC 1918, Abschnitt 3. Diese Adressen werden, ähnlich wie es fast alle Router am heimischen DSL-Anschluss tun, gegenüber der großen weiten Welt verborgen. Dabei verwendet Docker nicht die bekannteren IP-Bereiche 10/8 (z.B. 10.0.4.240; die Adresse meines wlan0 hier in der Firma) und 192.168/16 (z.B. 192.168.1.2; typisch für Heimnetze) sondern 172.16/12. Mein docker0 hat z.B. die Adresse 172.17.42.1. Obwohl Docker theoretisch die Adressen von 172.16.0.0 bis 172.31.255.255 verwenden könnte beschränkt es sich auf den 16-bittigen Bereich 172.17/16 von 172.17.0.0 bis 172.17.255.255. Die Adressen der Container innerhalb des Netzes werden dann einfach ab 172.17.0.1 hochgezählt und haben 172.17.42.1 als Gateway.
Auf dem Host selbst sorgen Routing-Regeln dafür, dass aller Verkehr zu einer der „Docker-Adressen“ über docker0 laufen. 172.17.0.43:3306 (da läuft gerade ein DB-Container) ist also von meinem Host aus erreichbar und für alle anderen Rechner um mich herum unsichtbar. Damit kommt erstmal alles raus und nichts rein… Es gibt natürlich eine Reihe von Möglichkeiten, die Sicht- und Erreichbarkeit von Containern zu verändern.
Nackt und bloß auf dem Hos… t
Wer es schnell und schmutzig mag kann das Netz eines Containers mit der Option –net=host direkt auf dem Host laufen lassen. Die obige Datenbank wäre dann über 10.0.4.240:3306 von überall aus dem Firmennetz erreichbar und hätte umgekehrt vollen Zugriff auf meinen lokalen Netz-Stack. Nicht schlimm? Doch! Darüber sind dann nämlich auch lokale Dienste wie D-Bus erreichbar und ein shutdown -h –no-wall now innerhalb des Containers kann dann unerwartete Auswirkungen haben: der Host wird runtergefahren. Jetzt, ohne Warnung.
You should use this option with caution. https://docs.docker.com/articles/networking/#container-networking
Egal wohin, Hauptsache erreichbar
Etwas weniger drastisch dafür aber weniger vorhersehbar ist das Ergebnis wenn man einen Container mit dem Parameter -P startet. Das Netz des Containers wird wie oben beschrieben über das docker0 Interface geführt. Alle durch den Container exponierten Ports werden auf zufällige Ports in der ephemeral port range auf dem Host gemappt. Je nach Konfiguration des Kernels liegt diese typischerweise im Bereich von 32768 bis 61000.
> docker run --rm -ti --name web -P php:apache > docker port web 80/tcp -> 0.0.0.0:49153
Der Port 80 meines Containers wurde hier auf den Port 49153 meines Host gemappt und wäre so von außerhalb zu erreichen. Natürlich kann ich vom Host aus weiter direkt auf den Container zugreifen.
> docker inspect --format '{{ .NetworkSettings.IPAddress }}' web 172.17.0.45
Auf dem Host ist localhost:49153 also identisch mit 172.17.0.45:80. Aus Sicht des Containers gibt es gar keinen wahrnehmbaren Unterschied weil beide Zugriffe für ihn von 172.17.42.1 kommen. Aber irgendwie ist es lästig immer wieder nachschauen zu müssen auf welchem Port oder welcher IP ein Container erreichbar ist.
I choose you Port!
Noch mehr Kontrolle über das Port Mapping gewährt der Parameter -p (oder –publish). Hierüber kann bestimmt werden welcher Port auf welchem Interface des Hosts auf einen Port im Container führt.
> docker run --rm -ti --name web -p 127.0.0.1:8080:80 php:apache > docker inspect --format '{{ .NetworkSettings.IPAddress }}' web 172.17.0.46 > docker port web 80/tcp -> 127.0.0.1:8080 > docker kill web > docker run --rm -ti --name web -p 8080:80 php:apache > docker inspect --format '{{ .NetworkSettings.IPAddress }}' web 172.17.0.47 > docker port web 80/tcp -> 0.0.0.0:8080
Im ersten Fall wäre der Container nur lokal über Port 8080 erreichbar, im zweiten von jedem Rechner aus, der Zugriff auf meinen Port 8080 hat. Das klingt so langsam nach produktivem Einsatz, oder? Es ist natürlich möglich, den Parameter -p mehrfach zu verwenden um mehrere Port Mappings zu definieren.
Container unter sich
Docker Container haben noch eine weitere Möglichkeit miteinander zu kommunizieren: Container Linking. Hierbei handelt es sich nach meiner Meinung um eine der coolsten Funktionen von Docker überhaupt. Wer meinen Artikel über Data Volumes gelesen hat erinnert sich vielleicht noch an den Container ‚db3‘ in dem eine MySQL-Datenbank läuft. Zur Erinnerung: Die Container für Daten und DB wurden erzeugt mit
docker create -v /var/lib/mysql --name mysql_data tianon/true true docker run --volumes-from mysql_data --name db3 -e MYSQL_ROOT_PASSWORD=rootpwd -e MYSQL_DATABASE=test1 -e MYSQL_USER=testuser -e MYSQL_PASSWORD=testpwd -d mysql:latest
Um jetzt aus meinem Application Container ‚web‘ auf die Datenbank in ‚db3‘ zuzugreifen könnte ich natürlich dem web-Container die IP und den Port der Datenbank explizit mitteilen. Da sich die Container untereinander alle erreichen können wäre der Zugriff dann gar kein Problem. Aber es ist schon wieder umständlich! Viel einfacher wird es mit
docker run --rm -ti --name web -p 8080:80 -v /home/cgd/Projects/Docker/Example:/var/www/html:ro --link db3:db phpmysqli
phpmysqli?
‚phpmysqli‘ ist ein Image von mir das php:apache um mysqli- und mbstring-Unterstützung erweitert. Um es zu verwenden braucht man nur eine Datei namens ‚Dockerfile‘ in einem sonst leeren Verzeichnis:
FROM php:apache RUN docker-php-ext-install mysqli mbstring
In dem Verzeichnis docker build -t phpmysqli . aufrufen und schon erzeugt Docker basierend auf php:apache ein neues Image namens phpmysqli.
Weiter im Text!
In das Verzeichnis auf dem Host (/home/cgd/Projects/Docker/Example) habe ich eine einfache PHP-Datei gelegt:
<?php echo $_SERVER['REMOTE_ADDR'] ; echo "<br>"; $mysqli = new mysqli('db', $_ENV['DB_ENV_MYSQL_USER'], $_ENV['DB_ENV_MYSQL_PASSWORD'], $_ENV['DB_ENV_MYSQL_DATABASE']); if ($mysqli->connect_errno) { echo "Failed to connect to MySQL: " . $mysqli->connect_error; } else { $result = $mysqli->query('select * from testo'); echo "Result contains {$result->num_rows} rows"; }
In der Datenbank befinden sich die passende Tabelle ‚testo‘ mit drei Einträgen:
CREATE TABLE testo (id INT(32) UNSIGNED NOT NULL AUTO_INCREMENT, somedata VARCHAR(255) DEFAULT '', PRIMARY KEY (id)); INSERT INTO testo (somedata) VALUES ('bla'),('blo'),('blue');
Und wie es der Zufall will…
> wget -q -O - localhost:8080/test.php 172.17.42.1<br>Result contains 3 rows
Wie kann das funktionieren? Durch den –link Parameter erzeugt Docker im Zielcontainer eine Reihe von Umgebungsvariablen über die man den Port und weiteres in Erfahrung bringen kann sowie einen Eintrag in /etc/hosts für ‚db‘.
Werfen wir doch einen kurzen Blick in den Container:
> docker exec -ti web bash root@50fe110e07b5:/var/www/html# ls test.php root@50fe110e07b5:/var/www/html# env DB_ENV_MYSQL_USER=testuser DB_ENV_MYSQL_PASSWORD=testpwd HOSTNAME=50fe110e07b5 DB_NAME=/web/db TERM=xterm PHP_INI_DIR=/usr/local/etc/php DB_PORT=tcp://172.17.0.53:3306 DB_PORT_3306_TCP_PORT=3306 DB_PORT_3306_TCP_PROTO=tcp DB_ENV_MYSQL_ROOT_PASSWORD=rootpwd PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin GPG_KEYS=6E4F6AB321FDC07F2C332E3AC2BF0BC433CFC8B3 0BD78B5F97500D450838F95DFE857D9A90D90EC1 PWD=/var/www/html DB_PORT_3306_TCP_ADDR=172.17.0.53 SHLVL=1 HOME=/root DB_ENV_MYSQL_DATABASE=test1 DB_PORT_3306_TCP=tcp://172.17.0.53:3306 DB_ENV_MYSQL_VERSION=5.6.24 PHP_EXTRA_BUILD_DEPS=apache2-dev DB_ENV_MYSQL_MAJOR=5.6 PHP_VERSION=5.6.9 PHP_EXTRA_CONFIGURE_ARGS=--with-apxs2 _=/usr/bin/env root@50fe110e07b5:/var/www/html# cat /etc/hosts 172.17.0.54 50fe110e07b5 127.0.0.1 localhost ::1 localhost ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters 172.17.0.53 db
Es gibt für jede Umgebungsvariable in db (dem Link-Alias von db3) einen entsprechende Variable in web mit dem Präfix ‚DB_ENV_‘. Da der Container db3 mit den Parametern MYSQL_DATABASE usw. erzeugt wurde und mit diesen automatisch eine passende Datenbank angelegt hat kann der web-Container die daraus abgeleiteten Variablen für den Zugriff auf die Datenbank in db3 verwenden.
Jetzt steht dem Entwickeln einer komplexen Web-Anwendung aus mehreren Containern nichts mehr im Wege. Viel Spaß beim Nachbauen!
Das Titelbild stammt von https://www.flickr.com/photos/xmodulo/14098888813 und steht unter der CC BY 2.0 Lizenz.