Howto: WordPress im Docker Swarm Mode mit Nginx-Proxy auf einem Host

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:

Howto: WordPress im Docker Swarm Mode mit Nginx-Proxy auf einem Host 1

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.

Howto: WordPress im Docker Swarm Mode mit Nginx-Proxy auf einem Host 2

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:

Howto: WordPress im Docker Swarm Mode mit Nginx-Proxy auf einem Host 3

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:

Howto: WordPress im Docker Swarm Mode mit Nginx-Proxy auf einem Host 4

 

In der Admin-Oberfläche kann anschließend unter „Einstellungen > Allgemein“ die URL der Site für https eingerichtet werden:

Howto: WordPress im Docker Swarm Mode mit Nginx-Proxy auf einem Host 5

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“

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Tags: