Teil 1: Anforderungen und Problem.. äh, Herausforderungen
Teil 2: Lösungsansätze – Dockerimages und Initialisierung
Teil 3: Lösungsansätze – Dockerimage mit WordPress
Teil 4: Lösungsansätze – Kubernetes
Teil 5: Installation automatisieren
Teil 6: Updates automatisieren
Teil 7: Feinschliff


Fangen wir mit der naheliegendsten Sache an: wir müssen so einige Dateien und Verzeichnisse in der WordPress-Struktur mit Mounts ersetzen. Das betrifft im Einzelnen:

  • /srv/htdocs/wp-config.php
  • /srv/htdocs/wp-content/uploads
  • /srv/htdocs/wp-content/wflogs
  • /srv/htdocs/.htaccess
  • /srv/htdocs/wordfence-waf.php

Ich hab hier mal bewusst die absoluten Pfade angegeben, da diese im Dockerimage genau so sein werden.
Denn es gibt ein offizielles Dockerimage von WordPress – aber das ist wohl entweder nur dazu gedacht, als VM-Ersatz genutzt zu werden, oder als vorkonfigurierter Apache. In dem Image ist jedenfalls das Verzeichnis /var/www/html als Volume markiert, also ist es wohl so gedacht, dass das WordPress komplett auf dem Volume liegt und immer wenn das Dockerimage startet, kopiert er die Basisfiles aus dem Image da hin.

Schön ist was anderes. Der Storage wird noch mehr zum Flaschenhals als ohnehin schon. Und zusätzlich riskieren wir noch, dass er sich mit verschiedenen Versionen in die Quere kommt oder – noch schlimmer – einfach unsere modifizierten Files überschreibt.
Das kann also nicht so ohne weiteres genutzt werden.

Doch zunächst müssen wir schauen, wie unser Dockerimage überhaupt aussehen soll. Klar ist zumindest, dass wir nur das nötigste in Volumes auslagern wollen!
Während wir das wp-content/uploads-Verzeichnis allerdings noch recht einfach mit einem Volumemount überdecken können, landen wir bei Plugins und Themes zunächst in einer Sackgasse.

Um später die PODs skalieren zu können, müssen diese bereits im Image vorhanden sein. Wir kommen also nicht umhin ein eigenes Dockerimage zu bauen – und zwar ggfs. pro Portal!
Bevor wir das jetzt aber einfach machen, gibt es noch weitere Aspekte zu bedenken:

  1. Zum einen werden Themes und Plugins beim Installieren und Aktivieren auch in der Datenbank erfasst – es ist also nicht damit getan, einfach ein paar Files in das Image zu packen.
  2. Zum anderen wollen wir auch direkt der Tatsache Rechnung tragen, dass diese Themes und Plugins auch mal ein Update erfahren werden/müssen.
  3. Und letztlich benötigen wir ja auch noch einen Weg, die Datenbank aufzusetzen und ggfs. zu aktualisieren, falls eine neuere WordPress-Version das erforderlich macht!

Normalerweise wird das alles über das Webinterface geregelt, aber diese Option scheidet aus naheliegenden Gründen aus. Glücklicherweise gibt es eine Alternative: wp-cli
Und ein offizielles Dockerimage das genau dieses Tool bereit stellt, gibt es ebenfalls.

Damit bewaffnet können wir jetzt ein Image erstellen, dass:

  • sich die spezifizierte WordPress-Version zieht und schon mal ins Image packt
  • die benötigten Plugins ins Image packt (und ggfs. vorher downloaded)
  • die benötigten Themes ins Image packt (und ggfs. vorher downloaded)

und das beim Ausführen:

  • eine rudimentäre wp-config erstellt (benötigt wp-cli für die weiteren Schritte)
  • die Datenbank aufsetzt, falls noch nicht geschehen
  • die Datenbank aktualisiert, falls vorhanden und erforderlich
  • die Plugins durchgeht und diese installiert und aktiviert
  • die Themes durchgeht, und diese installiert und eines davon aktiviert

Um das alles zu bewerkstelligen, brauchen wir natürlich ein paar Scripte. Und wir müssen auch irgendwo konfigurieren wo er sich die Plugins und Themes downloaden soll und was davon aktiviert werden muss.
Die Verlockung ist zwar groß, zu sagen „wir haben eh nur ein Theme, dass soll er einfach immer aktivieren“, aber viele Custom Themes sind Child-Themes – und die benötigen auch das Basis-Theme. In dem Fall müssen zwar beide installiert werden, aber nur das Child-Theme kann und soll aktiviert werden. Bei Plugins ist das einfacher – die können immer alle aktiviert werden.

Initializer Image

Schauen wir uns also zunächst an, wie ein entsprechendes Dockerimage aussehen könnte bzw. sollte:

FROM wordpress:cli-2

ARG VERSION

USER root

RUN apk update
RUN apk add --update jq

RUN mkdir -p /wp/plugins
RUN mkdir -p /wp/themes
RUN mkdir -p /wp/instance

COPY setup-or-update-wp.sh /wp
RUN chmod +x /wp/setup-or-update-wp.sh
RUN chown -R www-data:www-data /wp

USER www-data

# DO NOT install in /var/www/html
# It's marked as volume in the parent image
WORKDIR /wp/instance
RUN wp core download --version=$VERSION --locale=de_DE

WORKDIR /wp/plugins
COPY plugin-list.json .
COPY download-plugins.sh .
COPY aad-sso-wordpress.zip .
RUN ./download-plugins.sh

WORKDIR /wp/themes
COPY theme-list.json .
COPY covfefe-childtheme.0.9.zip .
COPY download-themes.sh .
RUN ./download-themes.sh

WORKDIR /wp/instance

ENTRYPOINT [ "/wp/setup-or-update-wp.sh" ]

Wie schon angedeutet, nutzen wir das offizielle Image von wp-cli als Eltern-Image, und richten erst einmal ein paar grundlegende Sachen ein, die wir in den Scripten später brauchen (bspw. jq und die Verzeichnisstruktur).
Danach weisen wir wp-cli an, WordPress in der angegebenen Version zu downloaden. Dann geht es direkt mit den Plugins weiter. Die custom Plugins, müssen logischerweise direkt in das Image kopiert werden. Den Rest holen wir uns via download-script.
Anschließend das ganze noch mal für die Themes, und natürlich wollen wir unser Script als Entrypoint.

Wie schon im Kommentar oben angemerkt, können wir das nicht direkt nach /var/www/html kopieren, weil das als Volume markiert ist und Docker sich dann einfach weigert.

Plugins downloaden

Werfen wir zunächst einen Blick auf das JSON, das die erforderlichen Plugins enthält:

{
  "plugins": [
    {
      "download": "https://downloads.wordpress.org/plugin/classic-editor.1.6.zip",
      "version": "1.6",
      "key": "classic-editor/classic-editor.php"
    },
    {
      "download": "https://downloads.wordpress.org/plugin/wordfence.7.4.14.zip",
      "version": "7.4.14",
      "key": "wordfence/wordfence.php"
    }
  ]
}

Hier fällt zunächst auf, dass neben der Download-URL noch zwei weitere Felder stehen, die für einen einfachen Download nicht nötig sind. Diese werden aber später für einen automatischen Update-Mechanismus benötigt. Doch dazu in einem späteren Beitrag mehr.

Das zugehörige Download-Script gestaltet sich entsprechend einfach:

#!/bin/bash

while read -r p;
do
    wget "${p}";
done < <(jq -r '.plugins[].download' plugin-list.json)

Mit jq holen wir die Download-URLs aus dem JSON und der Parameter -r sorgt dafür, dass die nicht in Quotes stehen.
Nichts vor dem man sich fürchten müsste.

Anzumerken bleibt nur, dass alle custom Plugins, die manuell ins Dockerimage kopiert werden, nicht in der Liste aufgeführt werden!

und Themes auch…

{
  "themes": [
    {
      "download": "https://downloads.wordpress.org/theme/covfefe.1.0.3.zip",
      "version": "1.0.3",
      "filename": "covfefe.1.0.3.zip",
      "activate": false
    },
    {
      "download": "",
      "version": "0.9",
      "filename": "covfefe-childtheme.0.9.zip",
      "activate": true
    }
  ]
}

Das JSON für die Themes sieht ein wenig anders aus und wird auch anders behandelt.
Wie weiter oben bereits erwähnt, müssen wir bei Themes angeben, welches davon aktiviert werden soll. Und deswegen müssen – im Gegensatz zu den Plugins – hier auch die custom Themes mit angegeben werden!

Das Download-Script muss dem natürlich Rechnung tragen, weshalb es leicht anders aussieht:

#!/bin/bash

while read -r p;
do
  if [ -n "$p" ]; then
    wget "${p}";
  fi
done < <(jq -r '.themes[].download' theme-list.json)

Hier prüfen wir im Prinzip nur, ob tatsächlich eine Download-URL vorhanden ist, und gehen ansonsten wie bei den Plugins vor.

Setup-Script

Und damit kommen wir auch schon zum „Herzstück“ des ganzen:

#!/bin/bash

pwd
ls -alFh .

echo "creating wordpress-config...";
wp config create --skip-check --dbhost=$DB_HOST --dbname="$DB_NAME" --dbuser="$DB_USER" --dbpass="$DB_PASSWORD" --extra-php <<PHP
define('MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL);
PHP

echo "check for existing installation...";
wp core is-installed
installed=$?

if [[ ${installed} == 0 ]]; then
  echo "detected existing installation. upgrade database...";
  wp core update-db
else
  echo "setup wordpress-database...";
  wp core install --url="$PAGE_URL" --title="$PAGE_TITLE" --admin_user="$ADMIN_USER" --admin_password="$ADMIN_PASSWORD" --admin_email="$ADMIN_MAIL"
fi

echo "configure plugins...";
while read p; do
  echo "processing plugin: $p"
  wp plugin install "$p" --activate
done < <(ls -1 /wp/plugins/*.zip)

echo "configure themes...";
while read t; do
  filename=$(echo "$t" | base64 -d | jq -r '.filename')
  if [ ! -f "/wp/themes/${filename}" ]; then
    echo "ERROR: no such theme-file: /wp/themes/${filename}";
    continue;
  fi
  echo "processing theme: ${filename}"
  if [ $(echo "$t" | base64 -d | jq -r '.activate') = true ]; then
    wp theme install "/wp/themes/${filename}" --activate
  else
    wp theme install "/wp/themes/${filename}"
  fi
done < <(jq -r '.themes[] | @base64' /wp/themes/theme-list.json)

echo "DONE";

Die ersten beiden Befehle dienen nur zu Debugging-Zwecken. Falls etwas schief läuft, hat man schon mal ein paar Hilfreiche Daten.

Danach erzeugen wir eine rudimentäre wp-config, die von wp-cli im folgenden benötigt wird. Der Parameter --skip-check ist dabei leider notwendig, weil es einen hässlichen Bug in wp-cli gibt, der seit Monaten ungelöst ist (Stand Januar 2021): https://github.com/wp-cli/config-command/issues/113
Immerhin betrifft er nur das Erzeugen der wp-config – im späteren Verlauf greift dann das „define“ was als extra-php angegeben wird. Wenn die Datenbank ohne SSL läuft, kann das natürlich weg gelassen werden – aber das tut ja hoffentlich keiner!!!!

Die Variablen müssen dem Docker als Environment-Variable mitgegeben werden. Das ist bzgl. des Passwort natürlich nicht schön, war in unserem Fall aber OK, weil wir das in einer Azure Pipeline laufen lassen. Da können wir das Passwort direkt aus dem KeyVault injecten und Azure filtert das in der Logausgabe raus!

Nachdem wir nun eine Config haben, schauen mir mit wp-cli ob in der DB schon ein WordPress installiert ist und lassen wahlweise ein Update laufen oder setzen das ganze neu auf.

Als nächstes „installieren“ wir die Plugins. Da wp-cli hierfür ein passendes Zip-File reicht, können wir einfach die vorhandenen Files durchgehen – einfache Sache!

Bei den Themes müssen wir uns da schon mehr Mühe geben!
Zum einen weil wir unterscheiden müssen, welches davon aktiviert wird. Zum anderen weil wir die Bash davon abhalten müssen, das JSON zu zerstückeln!
Das jq können wir leicht dazu bringen die Themes durchzugehen und uns jeweils den kompletten JSON-Block zurück zu geben. Aber damit die while-Schleife das frisst, müssen wir es einmal durch base64 jagen (wofür jq zum Glück ein eingebautes Feature hat) und es dann innerhalb der Schleife wieder dekodieren – wobei zu beachten ist, dass wir als Parameter dafür -d statt „–decode“ angeben müssen, weil das Dockerimage in dem wir uns befinden alpine/busybox-basiert ist und nicht Debian-basiert!

Der Rest ist dann wieder straight forward. Wir checken noch mal, ob das File auch existiert und installieren es dann wahlweise mit oder ohne „activate“.

Fazit

Damit haben wir auch schon alles was wir brauchen um die Datenbank aufzusetzen.
Und damit wir den Großteil davon nicht noch mal wiederholen müssen, können wir das direkt benutzen um das Image, in dem das WordPress laufen soll, zusammen zu stellen.

Doch das machen wir im nächsten Beitrag.


Weiter zu Teil 3: Lösungsansätze – Dockerimage mit WordPress