NGinx mit PHP-FPM, MySQL und Xdebug mit Docker auf Mac OS einrichten

Ich habe mich eine ganze weile erfolgreich vor Docker als lokale Entwicklungsumgebung gedrückt. Der Grund: Ich nutze eine kommerzielle Parallels Lizenz, über die ich Ubuntu virtualisiert laufen lasse und bin damit bisher ganz gut gefahren. Bisher. Paralles hat nämlich immer wieder Problem gemacht. So konnte ich nach manchen Updates von Parallels oder Ubuntu die Parallels Tools nicht mehr nutzen und musste diese umständlich neu installieren. Da die Doku dazu auch nur unvollständig vorliegt, war das regelmäßig eine ziemliche Zeitverschwendung. Die Parallels Tools sind aber notwendig, um von Ubuntu aus auf die Dateien des Gastsystems, Mac OS X, zuzugreifen.

Beim letzten Update auf Ubuntu 18.04 ließen sich die Parallels Tools gar nicht mehr installieren. Die Ursache dafür ist wohl irgendeine Inkompatibilität eines abhängigen Paketes mit dem neuen Linux-Kernel. Wie auch immer: Ich war die Sorgen leid und auf der Suche nach einer Alternative. Weshalb ich Docker stieß. Der Vorteil: Docker ist weitaus performanter und portabler. Der Nachteil: Um damit eine funktionierende Entwicklungsumgebung zu schaffen, kommt man um die Shell nicht herum. Das mitgelieferte UI Kitematic liegt noch als Beta vor und lässt einige Funktionen einfach vermissen. Es gibt also einige kleinere Fallstricke, die es zu überwinde galt. Was mir gelungen ist. Wie, das werde ich nun genauer erläutern. Viel Spass.

[caption id=“attachment_2166” align=“aligncenter” width=“300”]Kitematic - schön aber (noch) weitestgehend nutzlos Kitematic - schön aber (noch) weitestgehend nutzlos[/caption]

Erste Schritte

Grundsätzlich kann man bei der  Installation der Docker-Anwendung nicht viel falsch machen: Account anlegen, Docker herunterladen, installieren, anmelden - fertig. Auf die zugrunde liegende Technologie will ich hier nicht weiter eingehen, das machen andere weitaus besser (z.B. ist diese Anleitung sehr zu empfehlen).

Nur soviel soll gesagt sein: Du lädst ein Image herunter, dass du dann starten kannst. Dadurch erhältst du einen Container, der bestimmte Dienste bereitstellt. Dieser Container ist kein vollständiges Betriebssystem mit all seinem Ballast. So kannst du z.B. nicht ohne weiteres per SSH darauf zugreifen. Das funktioniert nur, wenn der entsprechende SSH-Dienst auch im Image vorgesehen ist. Das schöne an Docker ist aber, dass du das Image mit beliebigen Funktionalitäten über ein sogenanntes Dockerfile relativ unkompliziert nachrüsten kannst. Das erfordert zwar eine gewisse Umgewöhnung im Arbeitsablauf ab, bringt aber auch viele Vorteile mit sich.

Wer will kann sich mit der Kitematic UI durch die vorhandenen Docker-Images wühlen und auch direkt herunterladen. Diese stehen dann natürlich auch auf der Kommandozeile zur Verfügung. Mit docker image ls zeigst du alle verfügbaren Images an. Analog dazu listet docker container ls alle erstellten Container auf. Außerdem gibt es noch ein paar andere, für den Anfang ganz brauchbare Befehle:

# alle lokal verfügbaren Images auflisten docker image ls

alle gestarteten bzw. erzeugten Container auflisten

docker container ls

die Kommandozeile für den Container mit dem Namen “mysql” die Kommandozeile starten

docker exec -it mysql /bin/bash

für den Container mit dem Namen “mysql” das Setup anzeigen

docker inspect mysql

Für eine lokale Entwicklungsumgebung benötigt man zunächst einen HTTP- und einen MySQL-Server. Natürlich gehört zu jeder guten Entwicklungsumgebung auch ein Debugger - für PHP wäre das wohl xdebug. Ich nutze hier die beiden Images: nginx-php-fpm von Ric Harvey und das sehr aktuelle und offizielle MySQL-Image, die ich erstmal ganz unkompliziert über Kitematic herunterlade. Das nginx-Image werde ich schließlich mit einem Dockerfile anpassen um auch xdebug nutzen zu können.

[caption id=“attachment_2167” align=“aligncenter” width=“300”]Die Auswahl neuer Images über Kitematic ist sehr bequem Die Auswahl neuer Images über Kitematic ist sehr bequem[/caption]

Ab hier verlasse ich Kitematic allerdings wieder und werde Docker nur noch über die Kommandozeile und ein paar selbst geschriebene Scripte nutzen. Los gehts…

Den MySQL-Container starten

… es mit dem MySQL-Container. Da der nginx-Container auf MySQL zugreift, muss der MySQL-Container auch zuerst dasein. Der Aufruf dafür sieht folgendermaßen aus:

docker run \ –name mysql \ –publish 3306:3306 \ –volume /Users/nicky/Development/MySQL:/var/lib/mysql \ –env MYSQL_ALLOW_EMPTY_PASSWORD=yes \ –env MYSQL_ROOT_HOST=% \ –detach \ –default-authentication-plugin=mysql_native_password \ mysql

Mit –name vergebe ich einen festen und vor allem lesbaren Namen. Ohne diesen Parameter würde Docker eine Id anlegen, die den Zugriff später aber unnötig kompliziert macht. Der Parameter --publish legt fest, welcher Port “von draußen” auf einen Port im Docker-Container gemappt wird. Da sich die Dateien für die Datenbank physikalisch natürlich nicht im Docker-Container, sondern im Dateisystem vom Host befinden, muss ich dem Container mitteilen, wie er darauf zugreifen kann. Das passiert mit dem Parameter --volume. Damit kann ich nich nur Ordner im Container verfügbar machen, sondern auch Dateien. Das ist ganz praktisch, um z.B. Configurations-Dateien von außerhalb beim Start des Containers mitzuliefern.

Der Parameter --env dient dazu, Umgebungsvariablen zu setzen. Diese sind natürlich abhängig vom verwendeten Container. Für den MySQL-Container möchte ich hier zwei Parameter übergeben: Ich will eine Root-Benutzer ohne Passwort anlegen (MYSQL_ALLOW_EMPTY_PASSWORD) (das mag unsicher erscheinen, da ich hier aber lokal nur mit Testdaten arbeite, ist das erstmal einfach nur pragmatisch). Außerdem soll sich jeder Client verbinden können, also setzte ich MYSQL_ROOT_HOST auf %.

Eine Besonderheit ist der Parameter default-authentication-plugin. Wenn du ein Root-Passwort vergibst, solltest du zusätzlich diesen Parameter setzen. Zur Erklärung: Es handelt sich hier um ein MySQL 8-Image. Dort wird als Authentifizierungs-Methode caching_sha2_password verwendet, was sich mit Docker leider nicht verträgt und mit dieser Fehlermeldung quittiert wird:

Unable to load authentication plugin ‘caching_sha2_password

Und schließlich gibt es noch den Parameter –detach, der einfach dafür sorgt, dass der Container im Hintergrund gestartet wird.

Das war es fast. Was jetzt noch fehlt, ist der Name des Images, dass die Grundlage für deinen neuen Container bilden soll: mysql. Wenn das Image lokal nicht vorhanden ist, lädt Docker es hilfsbereiterweise einfach herunter.

Es empfiehlt sich, den Aufruf in ein Shell-Script zu packen und diese Zeilen voranzustellen. Beim Aufruf wird also ein vorhandener Container erst gestoppt und gelöscht und dann neu gestartet:

#!/bin/bash docker stop mysql docker rm mysql docker run \ –name mysql \ -p 3306:3306 \ -v /Users/nicky/Development/MySQL:/var/lib/mysql \ -e MYSQL_ALLOW_EMPTY_PASSWORD=yes \ -e MYSQL_ROOT_HOST=% \ -d \ –default-authentication-plugin=mysql_native_password \ mysql

Et voilà: Ein MySQL-Server im Docker-Container auf Knopfdruck!

Den HTTP-Container starten

Am Vorgehen ändert sich nicht viel. Auch für den nginx-Server erstelle ich mir ein kleines Script. Dieses befindet sich aber in einem Unterordner meiner Wordpress-Installation. Mit dem Platzhalter ${PWD} im Parameter --volume kann ich Docker das aktuelle Verzeichnis mitgeben. Dadurch kann  ich das Script auch innerhalb andere Wordpress-Installationen verwenden (und muss lediglich den Namen des Containers anpassen). Hier sorge ich also dafür, dass Docker das aktuelle Verzeichnis als Root-Verzeichnis für den HTTP-Server nutzt. Außerdem liefere ich eine eigene ini-Datei für PHP mit. Wichtig ist außerdem die Verknüpfung zu meinem zuvor erstellen MySQL-Container über den Parameter --link. Der Rest wird analog des ersten Containers vorgegeben: Port, Name, Image, usw.

#!/bin/bash docker stop nickyreinert-de docker rm nickyreinert-de docker run \ –link mysql \ –name nickyreinert-de \ –volume ${PWD}/dev/wordpress.ini:/usr/local/etc/php/conf.d/uploads.ini \ –volume ${PWD}:/var/www/html \ –publish 80:80 \ –detach \ richarvey/nginx-php-fpm

xdebug installieren

Leider liefert das nginx-Image kein xdebug mit. Bzw: Zurecht - der Sinn des ganzen Konzeptes ist es ja, schlanke Container nutzen zu können. Eine All-In-One-Lösung entspräche letztlich ja wieder einer kompletten virtuellen Maschine. Für mich jedenfalls heißt das, dass ich das nginx-Image nun irgendwie mit xdebug füttern muss. Das passiert mit docker build. Dazu benötige ich zunächst ein Dockerfile, dass auch genau so heißt und folgendermaßen aufgebaut ist - Erklärung folgt darunter:

FROM richarvey/nginx-php-fpm

RUN apk add –no-cache –virtual .phpize-deps $PHPIZE_DEPS RUN apk add –no-cache nano

RUN pecl install xdebug RUN echo ‘zend_extension = /usr/local/lib/php/extensions/no-debug-non-zts-20170718/xdebug.so’ » /usr/local/etc/php/php.ini RUN touch /usr/local/etc/php/conf.d/xdebug.ini; \ echo xdebug.remote_enable=1 » /usr/local/etc/php/conf.d/xdebug.ini; \ echo xdebug.remote_autostart=1 » /usr/local/etc/php/conf.d/xdebug.ini; \ echo xdebug.remote_connect_back=0 » /usr/local/etc/php/conf.d/xdebug.ini; \ echo xdebug.remote_host=192.168.0.11 » /usr/local/etc/php/conf.d/xdebug.ini; \ echo xdebug.remote_port=9000 » /usr/local/etc/php/conf.d/xdebug.ini; \ echo xdebug.remote_log=/tmp/php-xdebug.log » /usr/local/etc/php/conf.d/xdebug.ini;

Zunächst einmal lege ich mit FROM fest, welches Images als Grundlage genutzt werden soll - in meinem Fall also nginx-php-fpm von richarvey. Mit RUN übergebe ich dann, zum Image passende, Befehle. So könnte ich xdebug zum Beispiel einfach mit pecl install xdebug installieren lassen. Du wirst dann aber recht schnell feststellen, dass das nicht ohne weiteres funktioniert. PHP ist in diesem Docker-Image natürlich nur mit den wichtigsten Paketen eingerichtet, phpize gehört so z.B. nicht dazu und der Aufruf würde mit folgender Fehlermeldung quittiert werden:

Cannot find autoconf. Please check your autoconf installation and the # $PHP_AUTOCONF environment variable. Then, rerun this script.

Die Lösung ist, phpize im Voraus mit allen notwendigen Abhängigkeiten zu installieren. Dazu dient der Aufruf

RUN apk add –no-cache –virtual .phpize-deps $PHPIZE_DEPS

Das das nicht immer so umfangreich vonstatten gehen muss, soll die Installation des Texteditors nano zeigen: Man kann gewünschte Pakete auch einfach mit apk add zum Docker-Image hinzufügen. Der apk-Parameter --virtual besagt, dass die genannten Pakete zu einem “virtuellen Paket” hinzugefügt werden, dass sich dann mit apk del leicht wieder entfernen lässt. Danach schließlich kann xdebug mit pecl install installiert werden. Die letzten beiden RUN-Aufrufe sorgen dafür, dass eine Standard-Konfiguration für xdebug eingerichtet wird. Hier wird es tatsächlich noch etwas tricky, wenn es um den remote_host geht. Docker lässt den Container später in einem eigenen Netzwerk laufen. Der Docker-Host, also in meinem Fall OS X,erhält dafür eine eigene IP-Adresse, wie z.B. 172.17.0.1. Das Problem: Das ist nicht die IP-Adresse, unter der OS X bzw. der Debug-Client (z.B. Visual Studio Code) erreichbar ist. Jeder Debug-Versuch wurde (in meinem Fall) in /tmp/php-xdebug.log mit folgender Fehlermeldung quittiert:

W: Creating socket for ‘127.0.0.1:9000’, poll success, but error: Operation in progress (29).

Der Remote-Host ist also die tatsächliche IP-Adresse deines Hosts, unabhängig von Docker. Hier demnach die IP-Adresse 192.168.0.11.

Zum Abschluss muss das Dockerfile nur noch verarbeitet werden. Das passiert mit folgendem Aufruf:

docker build –tag nginx-php-fpm-xdebug .

Mit dem Parameter --tag gibst du dem modifizierten Image einen eigenen Namen. Der letzte Parameter - ein Punkt - zeigt docker, in welcher Datei sich die Build-Anweisung befindet. Docker sucht standardmäßig nach einer Datei mit dem Namen Dockerfile. Deshalb verweise ich mit dem Punkt einfach nur auf den aktuellen Ordner, in dem sich diese Datei auch befindet.

Docker wird nun die Anweisungen aus dem Dockerfile verarbeiten und ein neues Image erzeugen. Der erste Aufruf wird etwas länger dauern. Wenn du an dem Dockerfile nur geringfügige Änderungen vornimmst und den build-Prozesse erneut startest, ist Docker so clever und nimmt nur die notwendigen Änderungen vor - alles andere befindet sich bereits in einer Art “Zwischenspeicher”.

Denke nun daran, den oben bereits zusammengebauten Container-Aufruf des HTTP-Servers den Image-Name anzupassen: nginx-php-fpm-xdebug!

Visual Studio Code

Der Vollständigkeit halber möchte ich nun noch die Schritte dokumentieren, die bei Microsofts Visual Studio Code (VSC) notwendig sind. Hier installiert man zunächst das Paket PHP Debug. Im Debug-Bereich fügt man dann eine neue Konfiguration hinzu, die VSC mitteilt, unter welcher IP-Adresse xdebug erreichbar ist - nämlich 127.0.0.1 und dem üblichen Port: 9000:

{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 “version”: “0.2.0”, “configurations”: [ { “name”: “Listen for XDebug”, “type”: “php”, “request”: “launch”, “port”: 9000, “host”: “127.0.0.1” } ] }

Fertig. Mit einem Klick auf den grünen Playbutton wird VSC sich nun mit xdebug verbinden.

[caption id=“attachment_2199” align=“aligncenter” width=“300”]Visual Studio Code - den Debugger starten Visual Studio Code - den Debugger starten[/caption]

Fazit

Docker hat mich voll überzeugt. Ich bereue es, dass ich nicht schon früher umgestiegen bin. Es läuft sauber und wenn man das Konzept erstmal verstanden hat, ist es auch sehr intuitiv zu bedienen und lässt vor allem keine Wünsche auf. Im Nachhinein betrachtet hat mich die komplette Einrichtung der virtuellen Maschine mit Ubuntu unter Parallels auch weitaus mehr Zeit - und Nerven! - gekostet. Was jetzt noch fehlt, ist das etwas dynamischere Verwalten mehrerer lokaler Websiten mit Docker. Dazu komme ich später - stay tuned.

Referenzen

Mein Dank gilt Caleb Sotelo, von wo ich einen Großteil der Scripte habe. Ein Teil des Dockerfiles stammt von philipphauer.de. Danke!