Dank Docker for Mac und Docker for Windows muss man nicht mehr Docker Machine aus der Toolbox bemühen, um lokal einen Docker Host mittels Virtual Box zu erzeugen. Jetzt lädt man sich nur noch Docker for Mac/Windows herunter und kann ohne Umwege mit Applikationscontainern arbeiten.
Da Docker weiterhin einen Linux Kernel benötigt, werkelt unter der Haube ein leichtgewichtiges Alpine Linux. Durch die native Integration in die Plattformen kriegt man von dieser Virtualisierungsschicht jedoch recht wenig mit. Die Tage, an den man sich während des Hochfahrens der lokalen Vagrant-Umgebung noch einen Kaffee kochen konnte, sind gezählt.
Der Projektordner
Am Beispiel einer Lumen-Applikation möchte ich den Aufbau des Setups erläutern. Wir werfen zunächst einen Blick in den Projektordner.
etc
nginx
app.conf
php
php.ini
php-fpm.conf
src
docker-compose.yml
In etc
befinden sich die Konfigurationsdateien für unsere wichtigsten Dienste, den Webserver und PHP. Denkbar wäre hier natürlich auch noch eine my.cnf
für die MySQL-Konfiguration zu platzieren. Erfahrungsgemäß benötigt der Datenbankdienst jedoch weniger Anpassungen. In etc/nginx/app.conf
befindet sich die Konfiguration eines Server Blocks (VHost). Die Konfiguration für den PHP-Interpreter liegt in etc/php/php.ini
. Da wir PHP über den Fast Process Manager (FPM) laufen lassen, gibt es dafür eine eigene Pool-Konfigurationsdatei: etc/php/php-fpm.conf
.
Mit diesen Konfigurationsdateien sind wir also später in der Lage, das Verhalten unserer Dienste innerhalb der Docker Container zu steuern. Der Nebeneffekt ist, dass wir diese Konfigurationsdateien im Projektverzeichnis und damit in der Versionierung haben. Den Inhalt der Dateien werde ich bei erster Verwendung erläutern.
Der Ordner src
beinhaltet später unseren Lumen-Code.
In der Datei docker-compose.yml
passiert nun die eigentliche Magie. In ihr werden die verschiedenen Dienste miteinander verknüpft. Zum besseren Verständnis werde ich die Datei Schritt für Schritt erweitern. Wer es kaum noch abwarten kann, scrollt ans Ende des Artikels.
Service Orchestrierung mit Docker Compose
In unserer docker-compose.yml
beginnen wir zunächst mit der Definition unseres Webservers.
version: "2"
services:
webserver:
image: nginx:1.11
ports:
- "80:80"
Die Präambel in der ersten Zeile sorgt dafür, dass wir das Compose File in Version 2 nutzen. Mit Version 1 funktionieren Dinge wie Volumes und Netzwerke nicht, oder nur stark eingeschränkt. Wir nutzen also die neueste Version. Anschließend folgt auch direkt die Definition der services
, die wir selbst benennen können. Der bisher einzige Dienst webserver
nutzt dabei das offizielle nginx-Image aus dem Docker Hub in Version 1.11. Ich empfehle eine konkrete Version anzugeben. Im Endeffekt handelt es sich dabei um eine externe Abhängigkeit. Mit einer anderen Version dieser Abhängigkeit könnte unsere Applikation nicht funktionieren. In Produktivumgebungen ist also von der Verwendung des latest
-Tags abzuraten. Über den Eintrag ports
wird ein Mapping nach dem Schema Host:Container
definiert. Das erlaubt es uns, den Webserver über unser Hostsystem aufzurufen.
Folgender Befehl startet den Container im Vordergrund: docker-compose up
Im Browser erscheint nun beim Aufruf von http://localhost
die Willkommenseite des nginx. Der Terminal lässt uns nun die Logeinträge des Dienstes verfolgen. Der Webserver steht. BTW: Nicht über die Fehlermeldung wundern. Ein Favicon kann nicht gefunden werden. Mit STRG + C beenden wir die Ausführung.
webserver_1 | 2016/09/27 15:53:10 [error] 7#7: *1 open() "/usr/share/nginx/html/favicon.ico" failed (2: No such file or directory), client: 192.168.112.1, server: localhost, request: "GET /favicon.ico HTTP/1.1", host: "localhost", referrer: "http://localhost/"
webserver_1 | 192.168.112.1 - - [27/Sep/2016:16:07:34 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36" „-"
Volumes: Datenaustausch zwischen Hostsystem und Container
Zunächst bringen wir unsere eigenen Projektdateien in den Container und nutzen dazu den Abschnitt volumes
in docker-compose.yml
. Ähnlich der Syntax für das Port-Mapping leiten wir damit, beginnend im aktuellen Projektverzeichnis, den Ordner src
in unseren nginx-Container. Dieser erwartet in der Standardkonfiguration die Dateien im Verzeichnis /usr/share/nginx/html
. Dieser Mount legt sich, wie unter Unix üblich, über die bisherigen Dateien dieses Ordners und damit über die nginx-Willkommensseite.
version: "2"
services:
webserver:
image: nginx:1.11
ports:
- "80:80"
volumes:
- "./src:/usr/share/nginx/html"
Bevor wir nun erneut Docker Compose ausführen, legen wir noch eine Testdatei an. echo "Hallo Welt" > src/index.html
Ein erneuter Aufruf von http://localhost
im Browser zeigt nun „Hallo Welt“. Diese Datei können wir nun bearbeiten. Durch den Mount wird jede Änderung direkt nach dem Aktualisieren im Browser angezeigt.
PHP
Nun geht es an den PHP-Interpreter. Wir erweitern unsere docker-compose.yml
um einen neuen Dienst.
version: "2"
services:
webserver:
image: nginx:1.11
ports:
- "80:80"
volumes:
- "./etc/nginx:/etc/nginx/conf.d"
- "./src:/var/www"
php:
image: servivum/php:7.0-fpm
volumes:
- "./etc/php/fpm/php-fpm.conf:/usr/local/etc/php-fpm.conf"
- "./etc/php/php.ini:/usr/local/etc/php/php.ini"
- "./src:/var/www/"
In diesem Fall greife ich auf ein eigenes PHP-Image zurück, indem Erweiterungen, wie mcrypt, die Lumen benötigt, enthalten sind. Beim offiziellen PHP-Image muss man sich selbst um die Erweiterungen sowie Composerkümmern.
Mit Volumes leite ich die Pool- und Interpreter-Konfiguration in den Container. Hierbei muss man sich zuvor informieren, an welchem Ort im Container die Datei zu liegen hat. Hier hilft der Befehl docker-compose exec php bash
weiter, mit dem man direkt in den PHP-Container springt und eine interaktive Bash bekommt, um sich umzuschauen.
;etc/php/fpm/php-fpm.conf
[global]
error_log = /proc/self/fd/2
daemonize = no
[www]
; if we send this to /proc/self/fd/1, it never appears
access.log = /proc/self/fd/2
user = www-data
group = www-data
listen = [::]:9000
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
clear_env = no
; Ensure worker stdout and stderr are sent to the main error log.
catch_workers_output = yes
#etc/php/php.ini
display_errors = On
Dem ein oder anderen ist es vielleicht schon aufgefallen: Ich habe in der docker-compose.yml
nicht nur einen neuen Dienst hinzugefügt. Für die Interaktion der beiden Container muss natürlich auch noch die nginx-Konfiguration angepasst werden, die ich dem nginx-Container mit auf den Weg gebe.
Achtung: Die Projektdateien werden nun auch in das Verzeichnis /var/www
im nginx-Container mittels der docker-compose.yml
geleitet.
# etc/nginx/app.conf
server {
server_name _;
listen 80;
root /var/www/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
#try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}
include fastcgi_params;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_pass php:9000;
}
}
Hierbei handelt es sich um eine Schmalspurkonfiguration, die lediglich den Server-Block (VHost) unseres Lumen-Projektes beinhaltet. Spannend ist hier aber vor allem die Zeile ganz unten. Der PHP-Interpreter wird nicht per Socket, sondern per TCP-Verbindung hinzugezogen. Hier nimmt uns Docker Compose eine Menge Arbeit ab, indem es den PHP-Container unter dem Hostnamen verfügbar macht, der in der docker-compose.yml
auch als Service-Name vergeben wurde. Darüber hinaus wird ein Docker Network aufgebaut, indem sich bereits beide Container befinden.
Wenn wir das Setup nun erneut hochfahren, fügen wir dem Befehl gleich noch den Flag -d
an, um den Stack im Hintergrund laufen zu lassen: docker-compose up -d
Rufen wir nun http://localhost
auf erhalten wir ein „404 Not Found“, was daran liegt, dass die src/index.html
aufgrund der nginx-Config nun nicht mehr genutzt wird und wir noch keine Lumen-Dateien angelegt haben. Das holen wir nun nach. Wir entfernen zunächst die src/index.html
und führen anschließend diesen Befehl aus:
docker-compose exec php composer create-project --prefer-dist laravel/lumen .
Damit führen wir mittels Docker Compose einen Composer-Befehl zum Initialisieren des Lumen-Projektes aus, was eine praktische Angelegenheit ist, da wir so nicht zunächst in den Container springen müssen, um darin Befehle auszuführen, sondern können dem Container diese von außen mitgeben. Die durch Composer geladenen Dateien werden dank des Mounts in unserem src
-Ordner auf dem Hostsystem abgelegt.
Ein anschließender Aufruf von http://localhost zeigt nun
„Lumen (5.3.0) (Laravel Components 5.3.*)“. Wir haben Lumen also erfolgreich installiert.
MySQL
Eine Datenbank fehlt uns nun natürlich auch noch. Der Docker Hub bietet uns hierfür fertige Images von MySQL oder Percona. Ich entscheide mich für MariaDB und füge diesen Dienst der docker-compose.yml
hinzu.
version: "2"
services:
webserver:
image: nginx:1.11
ports:
- "80:80"
volumes:
- "./etc/nginx:/etc/nginx/conf.d"
- "./src:/var/www"
php:
image: servivum/php:7.0-fpm
volumes:
- "./etc/php/fpm/php-fpm.conf:/usr/local/etc/php-fpm.conf"
- "./etc/php/php.ini:/usr/local/etc/php/php.ini"
- "./src:/var/www/"
mysql:
image: mariadb
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=db
- MYSQL_USER=db
- MYSQL_PASSWORD=db
Mit einem Port-Mapping, wie bereits beim Webserver zu sehen, haben wir die Möglichkeit, mit einem externen MySQL-Client auf unserem Hostsystem, wie z. B. SequelPro, auf die Datenbank zuzugreifen. Wie der Beschreibung des MariaDB-Images zu entnehmen, konfiguriert man den Container mittels Environment-Variablen. Wir setzen also das Root-Passwort auf root
und erstellen eine Datenbank mit dem Namen db
, einen gleichnamigen User, der auf diese Datenbank mit dem Passwort db
zugreifen kann. Als Hostnamen nutzen wir mysql
, da Docker Compose analog zum PHP-Container den Dienstnamen innerhalb des Container-Netzwerks darunter verfügbar macht.
Fertig ist unser LEMP-Stack auf Basis von Docker.
Fazit
Dieses Setup stellt einen guten Startpunkt dar und kann nun beliebig erweitert werden. Denkbar ist der Einsatz einer Test-Datenbank. Hierfür lässt sich schnell ein weiterer MySQL-Container hinzufügen, der die Testdaten in einer separaten Datenbank vorhält. Wir nutzen darüber hinaus z. B. auch noch einen Container, der uns Swagger UI bereitstellt. Damit können wir unserem schlanken Microservice eine API-Dokumentation verpassen, die der Entwickler*in auch lokal bereitsteht.
Bild von Nick Hirche