Diese Website ist eine WordPress-Seite bei einem gewöhnlichen Webhoster. Bearbeitet wird sie aber nie dort, sondern in einer Kopie in einem LXC-Container auf meinem Proxmox-Cluster. Nachts geht der Stand automatisch live, wenn sich etwas geändert hat, und im Intranet gibt es einen Knopf für sofort. Hier steht, warum ich das so gebaut habe, wie die Veröffentlichung funktioniert, ohne Kommentare und Anmeldungen zu überschreiben, und welche Fehler mir beim ersten Livegang passiert sind.

Warum nicht einfach alles zu Hause?
Eine öffentliche Website ist ein Angriffsziel. Zu Hause hätte ich sie in eine eigene, abgeschottete Zone (DMZ) stellen müssen, getrennt von Domäne, Backups und Verwaltung. Die habe ich noch nicht. Ein Webhoster mit PHP und MySQL kostet dagegen ein paar Euro, kümmert sich um das Betriebssystem und hat schon ein Let’s-Encrypt-Zertifikat. Zu Hause bleibt nur, was das Internet nicht sehen muss: die Testumgebung.
Die Testumgebung im LXC
Das Staging ist ein unprivilegierter Debian-13-Container mit 2 vCPU und 2 GB RAM: Apache, PHP 8.4 über FPM und MariaDB. Er ist nur im Heimnetz erreichbar und hat ein Zertifikat meiner eigenen CA, das ein Automat erneuert. Theme und Artikel liegen zusätzlich in einem Git-Repository. Der Container steht im nächtlichen Proxmox-Backup.
Die wichtigste Regel ist organisatorisch: Geändert wird nur auf dem Staging. Texte, Bilder, Plugins, Theme, Updates. Live wird nie direkt bearbeitet. Damit gibt es genau eine Quelle, ich kann alles vorher ansehen, und die Live-Seite lässt sich jederzeit aus dem Staging neu aufbauen.
Veröffentlichen, ohne Live-Daten zu verlieren
Ganz so einfach wie „Staging drüberkopieren“ ist es nicht. Ein paar Dinge entstehen nur live und dürfen beim Veröffentlichen nicht verschwinden: Kommentare von Lesern, mein Benutzerkonto mit Kennwort und Zwei-Faktor-Anmeldung. Mein Veröffentlichen-Script macht deshalb Folgendes:
- Live-Datenbank vollständig sichern (die letzten zehn Sicherungen bleiben liegen).
- Die Tabellen
wp_users,wp_usermeta,wp_commentsundwp_commentmetavon live merken. - Die Datenbank vom Staging einspielen, mit allen Adressen auf die Live-Domain umgeschrieben.
- Die gemerkten Tabellen zurückspielen und die Kommentarzahlen der Beiträge neu zählen.
- Die Dateien per FTPS spiegeln, gelöschte Dateien auch live löschen.
# Adressen umschreiben, ohne die Staging-Datenbank zu ändern
wp search-replace 'https?://staging\.example\.lan' 'https://www.example.de' \
--regex --all-tables --precise --export=/tmp/wordpress.sql
# nach dem Einspielen: Kommentare zählen
UPDATE wp_posts p SET comment_count = (SELECT COUNT(*) FROM wp_comments c
WHERE c.comment_post_ID = p.ID AND c.comment_approved = '1');
Eine Folge der Regel ist mir erst spät aufgefallen: Live dürfen keine automatischen Updates laufen. Sonst wäre WordPress live irgendwann neuer als auf dem Staging, und die nächste Veröffentlichung würde es auf die alte Version zurückdrehen. In der wp-config.php der Live-Seite steht deshalb AUTOMATIC_UPDATER_DISABLED. Updates spiele ich auf dem Staging ein, teste sie und veröffentliche sie dann.
Beim ersten Livegang hatte ich den Namen des Datenbankservers fest ins Script geschrieben. Die alten Datenbanken meines Hosters liegen auf einem anderen Server als neu angelegte, die Verbindung schlug fehl. Seitdem kommt der Servername wie Datenbankname und Benutzer aus dem Kundenmenü.
Nachts nur, wenn sich etwas geändert hat
Ein systemd-Timer startet die Veröffentlichung jede Nacht um 03:30. Damit sie nicht sinnlos jede Nacht die Live-Datenbank ersetzt, bildet das Script vorher eine Prüfsumme über den Stand des Staging: die Dateiliste mit Größe und Zeitstempel, die Inhaltstabellen und die Optionen ohne die flüchtigen Einträge (Transients, Cron). Ist sie gleich wie bei der letzten Veröffentlichung, meldet das Script „unverändert“ und hört auf. Der Knopf im Intranet veröffentlicht dagegen immer. Wie der Knopf abgesichert ist, beschreibe ich in einem eigenen Artikel über SSH-Zwangsbefehle.
Wenn MariaDB auf MySQL trifft
Das Staging nutzt MariaDB 11, der Hoster MySQL 8. MariaDB legt Tabellen mit der Sortierregel utf8mb4_unicode_ci an, die MySQL nicht kennt. Neuere Dumps beginnen außerdem mit einer Zeile für den „Sandbox-Modus“, an der der MySQL-Client scheitert. Beides ersetze ich beim Export. Zunächst übersehen hatte ich, dass Matomo die Sortierregel auch in seiner eigenen Konfigurationsdatei speichert. Es gab Fehler 500, bis ich dort utf8mb4_unicode_ci eingetragen hatte.
sed -i -E '/^\/\*M!999999/d;
s/utf8mb4_uca1400_[a-z_]+/utf8mb4_unicode_ci/g' wordpress.sql
Nach dem Import stand auf der Startseite „fürs Homelab“. Der Export von WP-CLI enthält keine Angabe zum Zeichensatz, und der MariaDB-Client rät ihn aus der Umgebung. In einer SSH-Sitzung ohne Spracheinstellung ist das latin1. Die UTF-8-Bytes wurden als latin1 gelesen und noch einmal nach UTF-8 umgewandelt.

Die Lösung ist eine Zeile am Anfang jeder SQL-Datei und eine Option für den Client:
sed -i '1i SET NAMES utf8mb4;' wordpress.sql
mysql --default-character-set=utf8mb4 datenbank < wordpress.sql
Weil die Seite erst seit ein paar Minuten online war, konnte ich beide Datenbanken einfach leeren und neu einspielen. Mein Prüfschritt nach jeder Veröffentlichung zählt seitdem kaputte Umlaute auf der Startseite, also Zeichenfolgen wie „ü“. Steht da mehr als null, stimmt etwas nicht.
Statistik für alle Projekte an einer Stelle
Die Besucherstatistik läuft mit Matomo ohne Cookies und mit gekürzten IP-Adressen. Ich habe sie nicht in diese Website gepackt, sondern auf eine eigene Domain mit eigener Datenbank, und zwar in einem Unterordner statt auf der Startseite. So kann ich später weitere Projekte in dieselbe Statistik aufnehmen, und eine Veröffentlichung dieser Website fasst die Statistik nie an. Ein Hinweis zum Hoster: Ohne Cron-Job erstellt Matomo seine Berichte nur, wenn die Archivierung beim Aufruf im Browser eingeschaltet ist.
Schreibe einen Kommentar