Nachdem ich in den letzten Artikeln das Deployment einer Laravel-Anwendung mit Nginx-Proxy und Nginx-Proxy-Companion auf einem Host beschrieben hatte, sollte nun WordPress auf demselben Host im Docker Swarm Mode platziert werden. Dabei gelten dieselben Einschränkungen, d.h. der Betrieb findet nur auf dem einzelnen Host und nicht im gesamten Swarm-Cluster statt.
Die Vorarbeiten waren geleistet, der Nginx-Proxy-Container stellt den Reverse-Proxy dar, der Nginx-Proxy-Companion sorgt für die Verwaltung der Let’s-Encrypt-SSL-Zertifikate. Der folgende Artikel beschreibt die spezifischen Einstellungen für den Betrieb von WordPress und gibt Hinweise, wie sich einige Hürden umschiffen lassen.
Die Komponenten
Zunächst einmal stellt sich die Frage nach den benötigten Komponenten für WordPress. Analog zur Laravel-Anwendung werden minimal benötigt:
- eine Datenbank – hier MariaDB
- PHP, dabei kommt erneut das php-fpm-swrm-Image zum Einsatz und
- Nginx, ebenfalls wird auf das nginx-swrm-Image zurück gegriffen.
Weitere mögliche Komponenten wie Redis oder Memcache können zu einem späteren Zeitpunkt ergänzt werden.
Verzeichnisstruktur für verschiedene Services
Um für zukünftige bzw. weitere Services gerüstet zu sein, habe ich zunächst eine Verzeichnisstruktur angelegt. Dabei sollen alle Dateien, die für eine einzelne Anwendung benötigt werden, an einer Stelle versammelt sein. Die Struktur sieht für die WordPress-Anwendung wie folgt aus:
services |--geschkename |--html |--mariadb |--data |--nginx |--sites-enabled |--weitere...
Ausgehend vom Verzeichnis „services“ befinden sich in „geschkename“ alle notwendigen Dateien bzw. Verzeichnisse. Darunter „html“ für die Aufnahme der WordPress-Dateien, „mariadb/data“ für die Datenbank und „nginx/sites-enabled“ für die Nginx-Konfigurationsdatei. Im Hauptverzeichnis „geschkename“ befindet sich die docker-compose-Datei, die den Docker Stack definiert.
Einstellungen der Komponenten
MariaDB-Datenbank
MariaDB bietet nicht nur die Möglichkeit, ein root-Passwort beim Start des Containers zu definieren, sondern es kann auch direkt eine Datenbank mitsamt Datenbank-User und -Passwort angelegt werden. Damit diese Angaben, insbesondere das Passwort, nicht als Umgebungsvariable übergeben werden müssen, kommt erneut die Docker Secret-Funktion zum Einsatz. Wie sich beim ersten Start heraus stellte, muss der Parameter für das root-Passwort auf jeden Fall übergeben werden, es sei denn, dies wird explizit abgestellt, d.h. entweder ein leeres root-Passwort erlaubt, oder ein zufälliges gewählt. Insgesamt ergeben sich damit die folgenden vier Docker Secret Parameter:
geschke@waren:~/service$ echo 'GANZGEHEIMESPASSWORT' | docker secret create geschkename_mysql_password - jws0ijhot612uci6j02ggidbu geschke@waren:~/service$ echo 'geschkename_db' | docker secret create geschkename_mysql_database - m17jsqo7pd4u7jpzcusaxa6xo geschke@waren:~/service$ echo 'geschkename_user' | docker secret create geschkename_mysql_user - tdmadiwoo230hwlcuyv1d8yeb geschke@waren:~/services/geschkename$ echo 'ULTRAGEHEIMESPASSWORT' | docker secret create geschkename_mysql_root_password - vxfcwf0xukqxc6peym29mpzf5
Hat es funktioniert?
geschke@waren:~/services/geschkename$ docker secret ls ID NAME DRIVER CREATED UPDATED m17jsqo7pd4u7jpzcusaxa6xo geschkename_mysql_database 24 hours ago 24 hours ago jws0ijhot612uci6j02ggidbu geschkename_mysql_password 24 hours ago 24 hours ago vxfcwf0xukqxc6peym29mpzf5 geschkename_mysql_root_password 24 hours ago 24 hours ago tdmadiwoo230hwlcuyv1d8yeb geschkename_mysql_user 24 hours ago 24 hours ago
Das sieht insgesamt doch sehr gut aus. Die jeweiligen Identifier der Docker Secrets finden sich im docker-compose-File wieder.
Nginx-Webserver
Die Konfiguration des Nginx-Webservers entspricht weitestgehend der bisher verwendeten. Die Datei „nginx/sites-enabled/default“ sieht wie folgt aus:
# Default server configuration # server { listen 80 default_server; listen [::]:80 default_server; # SSL configuration # # listen 443 ssl default_server; # listen [::]:443 ssl default_server; # # Note: You should disable gzip for SSL traffic. # See: https://bugs.debian.org/773332 # # Read up on ssl_ciphers to ensure a secure configuration. # See: https://bugs.debian.org/765782 # # Self signed certs generated by the ssl-cert package # Don't use them in a production server! # # include snippets/snakeoil.conf; root /var/www/html; # Add index.php to the list if you are using PHP index index.php index.html index.htm index.nginx-debian.html; server_name _; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. # taken from further WordPress with Nginx configuration try_files $uri $uri/ /index.php?q=$uri&$args; } # pass PHP scripts to FastCGI server # location ~ \.php$ { include snippets/fastcgi-php.conf; # With php-cgi (or other tcp sockets): fastcgi_pass phpbackend_geschkename:9000; } }
Hierbei ist zu beachten – und daher sind die Stellen hier absichtlich kommentiert beibehalten, dass der „innere“ Nginx nur auf Port 80 lauscht, insofern kein SSL kennt. Sämtliche SSL-Kommunikation findet nur auf zwischen Browser und Nginx-Proxy statt, intern hingegen erfolgt die Kommunikation unverschlüsselt. Zwar ist dies eine übliche Konfiguration eines Reverse-Proxys, doch bei WordPress führte es zunächst zu einigen Problemen, dazu später mehr.
Um die grundlegende Funktionsfähigkeit zu testen, hatte ich in das „html
„-Verzeichnis zunächst nur eine Test-Datei kopiert, aber es spricht auch nichts dagegen, WordPress direkt dort hinein zu packen. Dazu kann einfach eine aktuelle WordPress-Version herunter geladen und in das Verzeichnis entpackt werden. Wichtig dabei ist, dass es kein weiteres Unterverzeichnis „wordpress
“ gibt, d.h. das Document-Root ist das Verzeichnis namens „html
„. Andernfalls wären die Verzeichnisse im docker-compose-File entsprechend anzupassen.
Der Docker-Stack
Das docker-compose-File zur Service-Beschreibung sieht wie folgt aus:
version: '3.3' services: nginx_geschkename: image: geschke/nginx-swrm volumes: - type: bind source: ./html target: /var/www/html deploy: replicas: 1 placement: constraints: - node.hostname == waren.mushaake.org configs: - source: nginx_config_default target: /etc/nginx/sites-enabled/default mode: 0440 networks: - website_net environment: VIRTUAL_HOST: www.geschke.name,geschke.name LETSENCRYPT_HOST: www.geschke.name,geschke.name LETSENCRYPT_EMAIL: ralf@geschke.name phpbackend_geschkename: image: geschke/php-fpm-swrm volumes: - type: bind source: ./html target: /var/www/html deploy: replicas: 1 placement: constraints: - node.hostname == waren.mushaake.org networks: - website_net mariadb_geschkename: image: mariadb:latest deploy: replicas: 1 placement: constraints: - node.hostname == waren.mushaake.org volumes: - ./mariadb/data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD_FILE: /run/secrets/geschkename_mysql_root_password MYSQL_DATABASE_FILE: /run/secrets/geschkename_mysql_database MYSQL_USER_FILE: /run/secrets/geschkename_mysql_user MYSQL_PASSWORD_FILE: /run/secrets/geschkename_mysql_password secrets: - geschkename_mysql_root_password - geschkename_mysql_database - geschkename_mysql_password - geschkename_mysql_user networks: - website_net configs: nginx_config_default: file: ./nginx/sites-enabled/default secrets: geschkename_mysql_root_password: external: true geschkename_mysql_database: external: true geschkename_mysql_password: external: true geschkename_mysql_user: external: true networks: website_net: driver: overlay external: true
Die meisten Abschnitte dürften bereits bekannt sein, daher möchte ich nur auf die Besonderheiten eingehen.
Eineindeutigkeit der Service-Bezeichner
Die Laravel-Anwendung läuft auf demselben Host – die Services heißen nginx, phpbackend und mariadb. Beim Start des Docker Stacks werden die einzelnen Services kreiert. Der Name der Services setzt sich zusammen aus einem Prefix und dem Service-Bezeichner aus dem docker-compose-File. Im Beispiel:
geschke@waren:~/services/geschkename$ docker service ls ID NAME MODE REPLICAS IMAGE PORTS nx2smzd1ksji website_mariadb replicated 1/1 mariadb:latest *:3306->3306/tcp uj4zdxw4n232 website_nginx replicated 1/1 geschke/nginx-swrm:latest miu39av5daun website_phpbackend replicated 1/1 geschke/php-fpm-swrm:latest
Der Prefix ist dabei der Name des Stacks, der beim Start des docker-stack-Kommandos angegeben wird. Nun ist der Name Service-Name aus dem docker-compose-File jedoch gleichzeitig der Host-Name, unter dem im Swarm-Cluster der Service ansprechbar ist.
Zwar können Services in einem Swarm-Cluster unter demselben Namen definiert werden, aber genau das führt zu nichtdeterministischem Verhalten: Es ist nicht vorhersagbar, welcher Service (welcher „Hostname“) auf einen Request antwortet. Wenn nun z.B. zwei Services „mariadb“ existieren, ist nicht klar, welche Datenbank angesprochen wird – in Zweifelsfall ist es die falsche. Daher wurden die Service-Namen im docker-compose-File entsprechend modifiziert, so dass sie im Swarm-Cluster eineindeutig sind.
Docker Secrets für MariaDB
Eine weitere Änderung ist bei der MariaDB-Umgebung zu finden – hier wurden alle vorab definierten Docker Secret Parameter verwendet. Damit wird beim Anlegen des MariaDB-Containers eine Datenbank erstellt, falls zuvor noch nicht vorhanden.
Docker-Stack-Deployment
Alle übrigen Parameter sollten bereits aus den vorherigen Artikeln bekannt sein. Damit kann der Docker Stack gestartet werden:
geschke@waren:~/services/geschkename$ docker stack deploy -c website_www_geschke_name.yml geschkename Creating config geschkename_nginx_config_default Creating service geschkename_nginx Creating service geschkename_phpbackend Creating service geschkename_mariadb geschke@waren:~/services/geschkename$
Es kann einen Moment dauern, bis beim ersten Start die Datenbank angelegt ist, darüber hinaus muss Nginx-Proxy-Companion erst die Zertifikate von Let’s Encrypt anfordern und einrichten. Ggf. lohnt es sich somit, erst ein ein wenig zu warten, bevor die Fehlersuche gestartet wird. Falls alles erfolgreich eingerichtet wurde, sollte eine Prüfung ungefähr so aussehen:
geschke@waren:~/services/geschkename$ docker stack ls NAME SERVICES geschkename 3 nginx-proxy 2 geschke@waren:~/services/geschkename$ docker service ls ID NAME MODE REPLICAS IMAGE PORTS k738en4sj4c8 geschkename_mariadb_geschkename replicated 1/1 mariadb:latest kmuz2xppww44 geschkename_nginx_geschkename replicated 1/1 geschke/nginx-swrm:latest 4ookr5m3t9b8 geschkename_phpbackend_geschkename replicated 1/1 geschke/php-fpm-swrm:latest
WordPress läuft… nicht!
Nur um es zu erwähnen – als Voraussetzung für die Erstellung des Let’s-Encrypt-Zertifikates ist natürlich ein korrekter DNS-Eintrag für die entsprechende Domain notwendig. Nachdem alle Services erfolgreich gestartet wurden, sollte WordPress bzw. dessen Installations-Tool unter der soeben konfigurierten Domain erreichbar sein. Jedoch zeigte sich die Setup-Routine wie folgt:
Wie unschwer zu erkennen ist, fehlen hier sämtliche Stylesheet-Definitionen. Das hat einen einfachen Grund – die Seite wurde mit https aufgerufen, während die URLs für externe Stylesheets und JavaScript-Dateien von WordPress nur mit http eingebunden werden. Aus Sicherheitsgründen blockiert der Browser diese „unsicheren“ Dateien, so dass die Seite ohne jegliche Stylesheets erscheint.
Das wäre im Prinzip nicht weiter schlimm, denn das Formular zur Einrichtung der Datenbank-Verbindung, was durch den Klick auf „Los geht’s“ erreichbar ist, funktioniert dennoch. Aber gerade diese nicht ganz unkritischen Daten müssten dann unverschlüsselt übertragen werden.
Zudem wird die Situation danach nicht viel besser. Ohne weitere Konfiguration geht WordPress davon aus, dass das http-Protokoll benutzt werden soll – und generiert im Quelltext entsprechende URLs. Zwar beschäftigen sich einige Seiten mit dem Problem und geben Hinweise zur Umstellung von http auf https bei WordPress, doch meist wird davon ausgegangen, dass die Anwendung bereits erfolgreich installiert wurde und somit läuft, d.h. dass die Datei wp-config.php
im Hauptverzeichnis verfügbar ist. Genau diese wird jedoch während des Setup-Prozesses erst generiert. Damit ergibt sich ein gewisses Henne-Ei-Problem.
Lösungen für https vs. http bei WordPress
Letztlich soll WordPress ohne Änderung auf Seiten des Nginx-Proxy-Containers funktionieren. Da Nginx-Proxy alle notwendigen Server-Variablen durchschleift, besteht die Lösung im Hinzufügen einiger Code-Zeilen in der wp-config.php
. Die Konfiguration ist somit dieselbe wie beim Betrieb eines Reverse-Proxys mit Nginx, der nach außen hin https anbietet, intern jedoch unverschlüsseltes http weitergibt. Zum Zeitpunkt der Installation fehlt jedoch genau die betreffende Datei wp-config.php
.
Zwar gibt es wie immer mehrere Lösungen für das Dilemma, aber so richtig empfehlen möchte ich an dieser Stelle keine. Eine mögliche Lösung wäre, die Generierung der Let’s-Encrypt-Zertifikate auf einen späteren Zeitpunkt zu verschieben und zunächst die grundlegende Einrichtung per http vorzunehmen. Dazu müssten die folgenden Zeilen aus der docker-compose-Datei entfernt (und später wieder eingefügt) werden:
LETSENCRYPT_HOST: www.geschke.name,geschke.name LETSENCRYPT_EMAIL: ralf@geschke.name
Damit wäre sicher gestellt, dass die Admin-Seite von WordPress zunächst ohne weitere Code-Änderungen erreichbar ist. Anschließend können die bei WordPress notwendigen Umstellungen im Admin-Bereich vorgenommen werden, nach Hinzufügen der Let’s-Encrypt-Parameter kann der Service bzw. Docker Stack neu gestartet werden.
Eine weitere Möglichkeit wäre, tatsächlich das „rohe“ Formular zu nutzen und die Verbindungsparameter dort einzugeben. Es sieht zwar wenig ansprechend aus, funktioniert jedoch bis zu einem gewissen Punkt.
Nachdem die entsprechenden Verbindungsdaten eingegeben sind, wird die Datei wp-config.php
im WordPress-Verzeichnis erstellt. Das funktioniert jedoch nur, sofern die Zugriffsrechte stimmen. Ggf. müssen daher die Rechte für den User www-data gesetzt werden:
sudo chown -R www-data:www-data html/
Sobald die Konfigurationsdatei vorhanden ist, ist theoretisch WordPress verfügbar. Doch auch auf der Startseite zeigt sich das bekannte Problem – sämtliche CSS- und JavaScript-Dateien fehlen:
Zu allem Überfluss ist die Admin-Oberfläche aufgrund zu vieler Redirects gar nicht erreichbar, somit können dort keine weiteren Einstellungen vorgenommen werden. Die allgemeinen Hinweise zur Umstellung von http auf https helfen ebenfalls nicht, jedoch finden sich in der WordPress-Dokumentation Tipps, wie sich WordPress hinter einem Reverse-Proxy betreiben lässt.
Dabei genügt es, die folgenden Zeilen in der Konfigurationsdatei wp-config.php
hinzuzufügen (ein geeigneter Platz wäre oberhalb der „That’s all…“-Zeile:
if($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'){ $_SERVER['HTTPS'] = 'on'; $_SERVER['SERVER_PORT'] = 443; }
Damit wird WordPress „auf die harte Tour“ signalisiert, dass das https-Protokoll verwendet werden soll, somit werden auch die URLs entsprechend generiert. Nach dieser Änderung ist die Admin-Oberfläche wie gewohnt erreichbar, ebenfalls zeigt sich die Website bzw. das Blog nun im gewünschten (Standard-)Design:
In der Admin-Oberfläche kann anschließend unter „Einstellungen > Allgemein“ die URL der Site für https eingerichtet werden:
Falls bereits Artikel vorhanden gewesen wären, müssten deren URLs ebenfalls geändert werden, dazu existieren Plugins wie „Better Search Replace“, doch da es sich hier um eine Neueinrichtung handelt und der erste Test-Beitrag in nahezu allen Fällen gelöscht oder deaktiviert werden dürfte, wird darauf nicht weiter eingegangen.
Tatsächlich genügen die o.g. PHP-Zeilen, um WordPress SSL beizubringen. Rewrite-Rules oder ähnliches sind nicht notwendig, da sich Nginx-Proxy um alles Weitere kümmert und für den Redirect von http auf https sorgt.
Nebenschauplatz: Austausch eines Docker-Images
Nachdem WordPress erst einmal lief, stellte ich fest, dass die GD-Library fehlte. Kein Problem – sie konnte schnell im php-fpm-swrm-Image hinzugefügt werden. Die aktuelle Fassung des Images ist somit bereits mit der GD-Library ausgestattet. Doch wie kann auf einfache Art und Weise das neue Image genutzt werden? Docker sieht dafür die Möglichkeit eines Service-Updates vor. Somit muss nicht der komplette Docker Stack mit allen Services neu gestartet werden, sondern es genügt, einen einzelnen Service zu aktualisieren. Dabei sind vielerlei Änderungen möglich, z.B. bzgl. Netzwerkkonfiguration, Hinzufügen von Environment-Variablen etc.. Der Austausch des Images war dabei noch eines der einfacheren Verfahren:
geschke@waren:~/services/geschkename$ docker service update --image geschke/php-fpm-swrm geschkename_phpbackend_geschkename geschkename_phpbackend_geschkename overall progress: 1 out of 1 tasks 1/1: running [==================================================>] verify: Service converged
Da hierbei nur ein Service auf einem Node vorhanden war, wurde der Betrieb natürlich kurzzeitig unterbrochen. Die Service-Updates spielen ihren wahren Vorteil erst aus, sofern replizierte Services auf mehreren Nodes laufen, denn dann können Updates bzw. Änderungen sukzessive erfolgen, so dass die jeweilige Anwendung ohne Unterbrechung weiter läuft.
Fazit
WordPress mit Docker im Swarm Mode auf einem Host – es funktioniert, und das sogar sehr gut, sofern man einige Kleinigkeiten beachtet. Die Installation von Grund auf war nicht ganz hürdenlos, aber letztlich hat es sich gelohnt. Nun fehlt nur noch die skalierbare und ausfallsichere Lösung für kleines Budget…
Ein Gedanke zu „Howto: WordPress im Docker Swarm Mode mit Nginx-Proxy auf einem Host“