Hübsch verpackt

Docker am praktischen Beispiel – mit WordPress
Kommentare

Technologischer Fortschritt, der im Web stattfindet, hat ein sehr eindeutiges Alleinstellungsmerkmal: Alles Neue muss schneller sein und besser skalieren. Und das macht die Sache für uns gerade erst spannend, denn die kontinuierlich sinkenden Kosten – sowohl für Hardware als auch Know-how im Open-Source-Umfeld – machen es uns leichter, uns an Lösungen zu wagen, die früher noch zu kostspielig waren.

Noch vor nicht all zu langer Zeit galt es als Standard, völlig voneinander unterschiedliche Services auf gleichem Server laufen zu lassen: Mailserver, Datenbank, Webserver, Runtime Environment, Storage etc. Parallelen zu unseren Desktoprechnern waren nicht zu übersehen.

Das Projekt finden Sie als Test-Container auch auf GitHub.

Heute allerdings – hier rede ich von der PHP-Welt – ist es so gut wie ausnahmslos so, dass PHP und die Datenbank in der Produktivumgebung auf den verschiedenen Servern laufen. Lokal bleibt es aber eher wie gehabt: Hier wird weiterhin alles zusammen auf der gleichen Maschine laufen. Der Grund ist meistens erst einmal der, dass unsere Rechner eine zusätzliche VM nicht immer ohne Weiteres verkraften. Und da lokal sowieso meistens keine großartig vielen Daten gebraucht werden, wird meist darauf verzichtet, eine oder mehrere zusätzliche VMs darauf zu schalten.

Alles hat seinen Preis

Das hat natürlich seinen Preis. Wenn die Topologie der lokalen Entwicklungsumgebung nicht mit der von Production oder Staging übereinstimmt, führt das dazu, dass die Applikation, einmal online gestellt, plötzlich anders funktioniert; wenn überhaupt. Die Lösung: Container. Sie nehmen den gesamten Overhead, den ein Virtual Machine Hypervisor wie VirtualBox mit sich bringt, komplett weg. Man bleibt weiter bei gleichem Kernel, legt lediglich einen neuen User Space darauf und besitzt so eine neue, komplett isolierte Umgebung. Das Problem war bis jetzt die Verwaltung der Container. Das erforderte spezielle Kenntnisse, die Applikationsentwickler nicht unbedingt haben sollten bzw. benötigten. Doch Docker hat diese Lage ordentlich umgekrempelt: Das Containermanagement ist jetzt so einfach geworden wie nie.

Genau das wollen wir uns genauer am Beispiel eines lokalen WordPress-Set-ups ansehen. Wir trennen PHP und MySQL voneinander und bleiben einem Konzept des guten Softwaredesigns treu: dem „Separation of Concerns“. Dabei lassen wir auch den Webserver (NGINX) als eigenständige Network Instance separat von der Skriptausführungsumgebung laufen.

Bevor wir beginnen, müssen wir uns zunächst überlegen, welche Abhängigkeiten wir verwalten müssen. Dabei lassen wir die Securityaspekte erst mal außen vor, uns geht es in erster Linie ums Konzept: die drei Container für NGINX, PHP und MySQL für eine WordPress-Applikation.

Es wird gleich klar, dass MySQL von den anderen Containern eigentlich nichts wissen muss. Die einzige Aufgabe ist es, den Clients Daten zur Verfügung zu stellen. Dabei muss PHP sehr wohl vom MySQL-User Bescheid wissen, wobei NGINX keine direkte Abhängigkeit zu MySQL besitzt.

Zwischen PHP und NGINX ist es schon etwas komplizierter. Hier müssen beide festlegen, wie sie miteinander kommunizieren wollen (per Unix Socket oder TCP). Außerdem müssen sie entscheiden, welchen Dokument-Root sie beide teilen. Obwohl PHP und NGINX eine gegenseitige Abhängigkeit darstellen, werden wir in diesem Fall PHP als primären Node bestimmen und NGINX darauf ausrichten lassen, um als Meditor-Schicht zwischen Browser und der PHP-Applikation zu dienen (Abb. 1).

Abb.1: Der Aufbau unseres Experiments

MySQL

Wollen wir zunächst mit dem MySQL-Container beginnen. Wir deklarieren ein Dockerfile (Listing 1) und führen anschließend einen Build-Befehl aus: docker build -t wordpress/mysql.

FROM ubuntu:14.04
MAINTAINER Seva Dolgopolov

# Get MySQL
RUN apt-get update
RUN apt-get install -y mysql-server-5.5

# Remove pre-installed database
RUN rm -rf /var/lib/mysql/*

# Open MySQL to a world
RUN sed -i -e"s/^bind-addresss*=s*127.0.0.1/bind-address = 0.0.0.0/" /etc/mysql/my.cnf

# Script to pass MySQL
ADD startup.sh /
RUN chmod 755 /*.sh

#define access data
ENV DB_USER docker_user
ENV DB_PASSWORD 1234
ENV DB_NAME wordpress
ENV VOLUME_HOME "/var/lib/mysql"

# expose MySQL port to a network
EXPOSE 3306

# open to mount
VOLUME ["/var/lib/mysql", "/var/log/mysql"]

# start container
CMD ["/bin/bash", "/startup.sh"]

In der Docker-Welt deklariert das Dockerfile, was dem Container gehören soll.

In unserem MySQL-Container-Build ist beinahe alles erlaubt – mit einer Ausnahme: Wir entfernen die von MySQL bereitgestellten Datenbanken und überlassen es unserem Skript startup.sh zu entscheiden, ob unser MySQL-Container die initialen Datenbanken erhält, oder ob er sie von anderer Stelle bereitgestellt bekommt.

Dieser Umstand ist dem geschuldet, dass wir auch nach dem Stoppen beziehungsweise Löschen unseres Containers weiterhin an die Daten kommen können bzw. müssen. Dafür verwenden wir die VOLUME-Instruktion, die bestimmte Filesystem-Segmente für das Mounten an andere Container reserviert.

Nachdem der Build erstellt ist, können wir den MySQL-Container starten und ihm explizit vermitteln, wo sich auf dem Host Mounts zu den entsprechenden Stellen im Container befinden:

$ docker run -d -v /data/mysql:/var/lib/mysql -v /data/log:/var/log/mysql -p 3306:3306 --name wordpress_mysql  wordpress/mysql

Die Option -v weist hier auf die Mounts-Schnittstellen hin. Das erste Argument ist der absolute Pfad auf dem Host, und das zweite Argument nach dem Doppelpunkt ist ein absoluter Pfad im Container. Mit der Option -p deklarieren wir, wie die Ports weitergeleitet werden. In unserem Beispiel bleibt es bei der gleichen Portsignatur.

An der Stelle ist unsere Datenbank komplett einsatzbereit. Nach unserer vorher definierten Abhängigkeitskette folgt jetzt der PHP-Container.

PHP

Wir legen ein Dockerfile an (Listing 2) und erstellen einen Build auf dieser Grundlage: docker build -t wordpress/php.

 FROM ubuntu:14.04

MAINTAINER Seva Dolgopolov

# get php and mysql client
RUN apt-get update
RUN apt-get -yqq install php5-fpm php5-mysql
RUN apt-get -yqq install mysql-client-5.5

# create app document root
RUN mkdir -p /var/www/app

# add php fpm runtime config
ADD www.conf /etc/php5/fpm/pool.d/

# open to mount
VOLUME ["/var/www/app", "/var/run/"]

# start php
ENTRYPOINT ["php5-fpm", "-F"]

Einfacher geht’s nicht. Im Grunde installieren wie die nötigen Pakete, verpassen PHP die notwendige Konfiguration und starten es anschließend. Was allerdings doch interessant ist: Wir lassen den Pfad /var/run, in dem die PHP-Socket-Datei abgelegt wird, offen. Bevor wir den PHP-Container allerdings initialisieren, laden wir uns die aktuellste WordPress-Version unter [1] herunter und entpacken sie unter /var/www/wordpress/. Jetzt sind wir bereit, den PHP-Container zu starten:

 $ docker run -d --link wordpress_mysql:db --name wordpress_php -v /var/www/wordpress:/var/www/app  wordpress/php

In diesem run-Befehl an den Docker Daemon

  • verlinken wir den PHP-Container mit dem MySQL-Container durch die Option –link und legen den Alias db fest, der innerhalb des PHP-Containers als gültiger Hostname für den MySQL-Container dienen wird.
  • übergeben dem Document Root des Containers die WordPress-Codebasis, die auf dem Host unter dem Pfad /var/www/wordpress kurz zuvor von uns abgelegt wurde.

 An dieser Stelle sind wir so gut wie fertig. Jetzt müssen wir nur noch die App über den Browser ansprechen können – und hier folgt unser letztes Element der Kette: der NGINX-Container.

NGINX

Wir erstellen das Dockerfile wie in Listing 3 zu sehen.

FROM ubuntu:14.04

MAINTAINER Seva Dolgopolov

# get nginx
RUN apt-get update
RUN apt-get install -y nginx

# add and activate host configuration
ADD app.conf /etc/nginx/sites-available/
RUN rm /etc/nginx/sites-enabled/default &&  ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled

# let it be open on port 80
EXPOSE 80

# start nginx
ENTRYPOINT ["nginx", "-g", "daemon off;"]

Die Datei app.conf ist die Konfiguration des Hosts, der die PHP-Dateien per FastCGI dem PHP-Container über Unix Sockets bereitstellt. Ein weiterer wichtiger Aspekt ist hier das NGINX-Setting daemon off;, das beim Starten gesetzt wird. Um den Docker-Container mit NGINX am Laufen zu halten, muss ein NGINX-Prozess laufen. Wenn wir NGINX starten, wird der Prozess, der das Hochfahren von NGINX verantwortet, nach dem Hochfahren vom neuen Prozess übernommen (geforkt) und sich selbst in den idle-Zustand verabschieden – was auch für Container den Feierabend bedeutet. Somit muss dieses Weiterreichen an dem Daemon unterbunden werden.

Nach dem Erstellen des Builds mit docker build -t wordpress/nginx lassen wir den nginx-Container laufen:

$ docker run -d -p 80:80 --volumes-from wordpress_php --name wordpress_nginx  wordpress/nginx

Hier erbt der NGINX-Container alle für das Mounten der vom PHP-Container bereitgestellten Pfade und reicht den Port 80 an den Host weiter. Und jetzt wird es spannend. Denn der Docker Daemon läuft gleich auf dem Host. Jetzt genügt es, http://127.0.0.1 im Browser aufzurufen. Diese Option wird aber weniger genutzt. Es ist eher üblich, eine separate VM, die man mit Vagrant zuvor vorbereitet hatte, dafür bereitzustellen – zumindest wenn wir lokal entwickeln. In diesem Fall sollten Sie die IP Ihrer VM ansprechen.

Im Ergebnis sieht man den klassischen Installationassistenten von WordPress, in dem wir kurz vor dem Abschluss der Installation im DB-Konfigurationsformular landen. Hier tragen wir die für den MySQL-Container definierten Environment-Variablen ein und unsere Docker-Container-Set-up ist fertig.

Abb. 2: Die Konfiguration der DB in WordPress

Jetzt haben wir eine Lösung, die recht gut skalieren kann und dabei performant bleibt. Wir können jederzeit einen weiteren Webserver dazu schalten, der z. B. auf Port 8080 lauscht:

$ docker run -d -p 8080:80 --volumes-from wordpress_php --name wordpress_nginx  wordpress/nginx

Oder einen weiteren, der sich bei einem PHP-Container bedient, der eine neuere WordPress-Version übergeben bekommen hat, der aber weiterhin die gleiche Datenbank anspricht – und das alles, ohne einmal den Code der Applikation anfassen zu müssen.

Fazit

Der beschriebene Ansatz ist unter anderem auch ein guter Einblick in einen möglichen Paradigmenwechsel, ein Ausblick darauf, wohin sich die Architektur einer Webapplikation in der näheren Zukunft entwickeln könnte. Nämlich in Richtung der Zusammensetzung von schnelllebigen, invariablen, leicht austauschbaren Services, die man auch unter Umständen weiter teilen wird, was zum Teil schon am Beispiel von Docker Hub Realität wurde. Eins ist sicher, es wird nur spannender!

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -