Drei Plesk-Mailserver, rund 30 WordPress-Seiten, eine veraltete Lizenz-Webmail-Installation und ein Login-Bug, der mich einen ganzen Abend gekostet hat — das war mein Wochenende mit Claude Code. Kein einzelnes Feature, sondern ein kompletter Infrastruktur-Sweep: PHP-Version anheben, eine Webmail-Anwendung austauschen, und dabei drei production-Bugs finden, die seit Jahren unbemerkt in Kundencode schlummerten.
Der Auslöser war banal: „Installier doch mal die neueste PHP-Version auf allen Servern.“ Am Ende sind daraus zwei Tage geworden, in denen Claude Code über weite Strecken mit mehreren parallelen Agents gleichzeitig auf drei Servern gearbeitet hat, während ich nebenbei Rückfragen beantwortet habe.
PHP 8.5 auf ~30 WordPress-Installationen
Der einfache Teil zuerst: PHP 8.5 auf allen drei Servern installieren, ohne eine einzige laufende Domain umzustellen, bis sie geprüft ist. Danach ging es an den eigentlichen Umfang — knapp 30 WordPress-Seiten, überwiegend auf einem bekannten Theme-Framework mit Dutzenden aktiven Plugins pro Seite.
Statt zu raten, habe ich Claude Code einen echten statischen Compatibility-Scanner (PHPCompatibility als phpcs-Ruleset) gegen jedes aktive Theme und Plugin laufen lassen — nicht gegen den kompletten Ordner, sondern gezielt nur gegen das, was auch wirklich lädt. Bei einer Seite mit fast 50 aktiven Plugins kam dabei eine Fehlerliste mit über 400 Treffern heraus. Erster Schreckmoment. Zweiter Blick: fast alle Treffer steckten in einem Build-Tool-Ordner, den ein Plugin versehentlich mit in sein Release-Paket gepackt hatte — nie zur Laufzeit geladen, komplett irrelevant.
Das war die eigentliche Lektion: die Rohzahl eines Compatibility-Scans sagt fast nichts. Entscheidend ist, ob der betroffene Code tatsächlich im Request-Pfad liegt. Also für jede Seite: scannen, jeden Fund, der nicht in ein bereits bekanntes, harmloses Muster fiel (verschluckte Legacy-Funktionsaufrufe in Vendor-Bibliotheken, die längst tot sind), einzeln nachschlagen — und erst danach den PHP-Handler pro Domain umstellen, mit sofortigem HTTP-Check danach. Am Ende liefen alle ~30 Seiten sauber auf PHP 8.5.
Drei echte Bugs, die der Scan aufgedeckt hat
Der Scan war nicht nur Formsache. In einer individuell entwickelten PHP-Anwendung (kein WordPress, sondern eine eigenständige Kundenanwendung mit Formularen und PDF-Export) fand sich Code, der auf der aktuell laufenden PHP-Version bereits fatal abstürzte — nur unbemerkt, weil OPcache seit Jahren dieselbe kompilierte Version ausliefert und der betroffene Codepfad selten genug getroffen wird.
- Entfernte String-Offset-Syntax:
$var{0}statt$var[0]— seit PHP 8.0 ein Parse-Error. Sieben Stellen in einer Funktion zur IBAN-Prüfsummenberechnung. - Toter PDF-Generator: eine eingebundene PDF-Bibliothek rief im Konstruktor
get_magic_quotes_runtime()auf — eine Funktion, die seit PHP 8.0 schlicht nicht mehr existiert. Jeder Versuch, ein PDF zu erzeugen, endete in einem Fatal Error. Fand sich zweimal, in zwei komplett unabhängigen Plugins auf zwei verschiedenen Seiten. - Kommentar-Falle: auskommentierte Array-Zeilen im Stil
#[97, 130],— seit PHP 8.0 wird#[ausnahmslos als Beginn einer Attribut-Syntax interpretiert, nicht mehr als Kommentar. 51 Stellen in 17 Konfigurationsdateien, alle mit demselben Muster.
Alle drei Fehlerklassen waren streng genommen unabhängig vom eigentlichen PHP-8.5-Umstieg — sie waren schon unter der vorher laufenden Version kaputt, nur eben unsichtbar. Ohne den systematischen Scan wären sie erst aufgefallen, wenn irgendwann ein Server-Neustart den OPcache geleert hätte und plötzlich die PDF-Erzeugung für Kundenanfragen komplett ausgefallen wäre.
Der Webmail-Umzug: von Lizenzsoftware zu Open Source
Auf einem der drei Server lief zusätzlich eine ältere, kommerzielle Webmail-Anwendung. Die naheliegende Frage — „einfach aktualisieren“ — entpuppte sich als Sackgasse: der Hersteller hatte die Migration vom alten Major-Release auf die aktuelle Version bereits offiziell eingestellt, ein Umstieg hätte eine neue, kostenpflichtige Lizenz erfordert. Also stattdessen der Wechsel auf die Open-Source-Schwester-Version desselben Herstellers — funktional reduziert, aber kostenlos und aktiv gepflegt.
Der Build selbst war schon ein kleines Abenteuer (ein kaputtes Node.js-Build-Script, das beim Kompilieren des Admin-Panels versehentlich die komplette Shell-Umgebung überschreibt und damit den PATH verliert — von Hand nachgebaut), aber der eigentliche Spaß begann danach: die Superadmin-Anmeldung funktionierte einwandfrei, jeder normale Postfach-Login scheiterte mit einer nichtssagenden „Authentifizierung fehlgeschlagen“.
Das Tückische daran: die IMAP-Verbindung zum Mailserver war zu jedem Zeitpunkt hundertprozentig in Ordnung. Ich habe das mit einem extra angelegten Testpostfach und bewusst falschem Passwort verifiziert — sauberer, korrekter Auth-Reject vom Mailserver. Die eigentliche Anwendung akzeptierte das Passwort ebenfalls korrekt in Richtung IMAP, warf aber danach trotzdem einen generischen Fehler zurück.
Um dem auf den Grund zu gehen, habe ich mit Claude Code temporäres Logging direkt in den Anwendungscode eingebaut, Schritt für Schritt tiefer — bis sich herausstellte: die Konfiguration per API-Aufruf (statt über den browserbasierten Setup-Assistenten) hatte zwei interne Datenbanktabellen leer gelassen, die die Anwendung für die automatische Anlage neuer Postfach-Konten braucht. Das Symptom „Admin geht, normaler User nicht“ ist dabei fast schon die Definition dieses einen fehlenden Bootstrapping-Schritts — der Admin-Login umgeht die betroffene Prüfung komplett, jeder andere Login läuft mitten hinein. Zwei API-Aufrufe später (einen „Channel“, einen „Tenant“ anlegen) liefen alle Logins.
Die Prompts zum Nachbauen
Damit das nicht bei „hab ich mit KI gemacht“ stehen bleibt, hier die tatsächlichen Anweisungen, mit denen Claude Code die Arbeit erledigt hat — anonymisiert, aber strukturell unverändert. Alle drei liefen als eigenständige Hintergrund-Agents, teils mehrere parallel auf verschiedenen Servern.
1. Compatibility-Audit einer einzelnen Seite
Server: server.example.com, Zugang per SSH-Key. Ziel: PHP-8.5-Kompatibilität für die aktiven Theme- und Plugin-Dateien von domain.example prüfen, NICHT den gesamten wp-content-Ordner. 1. Aktive Theme(s) und Plugins per wp-cli ermitteln (wp theme list --status=active, wp plugin list --status=active). 2. NUR diese Verzeichnisse in einen Scratch-Ordner kopieren (sudo cp -r, danach sudo chmod -R o+rX für Lesezugriff ohne root). 3. Scan mit PHPCompatibility (phpcs-Ruleset) gegen testVersion 8.5, --report=summary zuerst für den Überblick. 4. Zusätzlich php -l mit dem Ziel-PHP-Binary über alle kopierten Dateien für echte Parse-Fehler. 5. Jeden Fund einordnen: liegt er in Vendor-/Bibliothekscode, der nachweislich schon unter der aktuell laufenden PHP-Version tot ist (z.B. durch function_exists()-Guards abgesichert)? Dann kein Blocker. Liegt er in eigenem/Kundencode? Dann mit --report=full auf die Datei draufschauen und das Risiko konkret einschätzen, nicht raten. 6. NICHT selbstständig den PHP-Handler umstellen. Nur Befund melden: Domain | Fehler | Einschätzung | Empfehlung. Scans laufen bei größeren Seiten 15-25 Minuten — nicht mit einem einzelnen blockierenden Kommando warten, sondern im Hintergrund starten und mit einer Warteschleife pollen, die erst zurückkehrt, wenn der Prozess tatsächlich fertig ist.
2. Umstellen — erst nach Freigabe, nie im Blindflug
Für jede Domain, die den Scan bestanden hat, einzeln (nicht im Batch):
sudo plesk bin domain --update DOMAIN -php_handler_id plesk-php85-fpm
Direkt danach per curl verifizieren (HTTP-Code, Redirects folgen):
curl -sk -o /dev/null -w '%{http_code}' -L https://DOMAIN/
Bei 5xx oder Fehler: sofort zurückrollen auf den alten Handler und
als fehlgeschlagen melden. Erst nach erfolgreicher Prüfung die
nächste Domain angehen — nie mehrere gleichzeitig umstellen, damit
im Fehlerfall klar ist, welche Änderung die Ursache war.
3. Den Login-Bug aufspüren
Hier gab es keinen fertigen Prompt im Voraus — das war iteratives Debugging in Echtzeit. Der Ablauf, der am Ende funktioniert hat:
- Erst empirisch eingrenzen: mit einem eigens angelegten Testpostfach und bewusst falschem Passwort prüfen, ob die Fehlermeldung überhaupt bis zum Mailserver durchkommt (Mail-Log auswerten). Ergebnis: ja, sauberer Auth-Reject — die Verbindung selbst ist also nicht das Problem.
- Dann mit korrektem Test-Passwort denselben Weg gehen. Wenn das auch mit korrektem Passwort fehlschlägt, liegt der Fehler nicht am eingegebenen Kennwort, sondern irgendwo in der Anwendungslogik danach.
- Anwendungsseitiges Logging aktivieren (falls vorhanden) — oft reicht das noch nicht, weil generische Fehlerklassen den eigentlichen Grund verschlucken.
- Gezielt
try/catch-Blöcke mit expliziter Fehlerausgabe um die verdächtige Codestelle legen, den Request erneut auslösen, Ausgabe auswerten — Schritt für Schritt tiefer, bis der exakte Wurfpunkt der Exception feststeht. - Sobald die Ursache klar ist: Fix anwenden, denselben Testweg (Testpostfach, dann echter Login) zur Verifikation nutzen, alle Debug-Änderungen rückstandsfrei wieder entfernen.
Der Kernpunkt an diesem Debugging-Muster: nicht raten, sondern bei jedem Schritt einen Fakt schaffen, der die nächste Hypothese entweder bestätigt oder ausschließt. Ein Testkonto mit garantiert korrektem Passwort ist dabei ein unterschätztes Werkzeug — es trennt sauber zwischen „Nutzereingabe ist falsch“ und „die Anwendung selbst ist kaputt“.
4. Incident-Response: die 500er-Seite und der systemweite Check
Meldung: https://domain.example ist down.
1. Sofort HTTP-Status + Ladezeit prüfen (curl -w '%{http_code} %{time_total}').
2. Error-Log der betroffenen Domain auswerten, nicht raten — bei
Plesk-Setups liegt der php-fpm-Fehler oft im proxy_error_log,
nicht im Standard error_log.
3. Root Cause exakt benennen (hier: memory_limit durch den
Handler-Wechsel auf den PHP-Standardwert zurückgesetzt), NICHT
vorschnell die gesamte Migration zurückrollen.
4. Sobald eine Domain betroffen ist: sofort die Hypothese
aufstellen "vermutlich nicht die einzige" und ALLE migrierten
Domains auf dasselbe Muster durchchecken, nicht nur die eine,
die sich gemeldet hat:
for d in domain1 domain2 domain3 ...; do
grep '^memory_limit' /var/www/vhosts/system/$d/etc/php.ini
done
5. Fix domain-übergreifend ausrollen, PHP-FPM reloaden (nicht
restarten — das würde laufende Requests killen), und die
ursprünglich gemeldete Domain als erstes verifizieren, bevor
der Rest gemeldet wird.
Der entscheidende Schritt ist Punkt 4: bei einer Migration über viele gleichartige Ziele hinweg ist ein gemeldeter Einzelfall so gut wie nie ein Einzelfall. Nur weil sich noch keine zweite Seite gemeldet hat, heißt das nicht, dass keine zweite betroffen ist — nur, dass noch niemand hingeschaut hat.
Nachspiel: der vierte Bug, der erst nach dem Livegang auftauchte
Am Tag danach die Ernüchterung: eine der umgestellten Seiten meldete sich mit HTTP 500. Panik-Modus, sofort ins Error-Log. Der Fehler: Allowed memory size of 134217728 bytes exhausted — 128 MB, das PHP-Standardlimit, an mehreren völlig unterschiedlichen Stellen im Code gerissen (Theme-Helper-Funktionen, Übersetzungs-Cache, Objekt-Cache).
Der Verdacht lag nahe, aber falsch: nicht PHP 8.5 selbst braucht plötzlich mehr Speicher. Der eigentliche Grund war banaler und tückischer zugleich — beim Umstellen des PHP-Handlers greift Plesk auf ein eigenes Einstellungsprofil pro PHP-Version zurück. Individuelle Anpassungen wie ein höheres memory_limit, die für PHP 8.4 händisch gesetzt waren, existieren für die frisch installierte PHP-8.5-Instanz schlicht nicht — und Plesk fällt kommentarlos auf den Standardwert zurück. Bei einer schlanken Seite fällt das nie auf. Bei einer mit Page-Builder, Umkreissuche und zwei Dutzend aktiven Plugins reicht 128 MB nicht mehr.
Der eigentliche Schreckmoment kam beim systematischen Nachprüfen: von den gut 30 umgestellten Domains hatten 21 exakt dasselbe stillschweigende Zurücksetzen erlebt — nur eben noch nicht genug Traffic gesehen, um es zu bemerken. Nur weil eine Seite unter echter Last als erste einbrach, ist der Rest überhaupt aufgefallen. Wäre die Reihenfolge anders gewesen, hätte ich das vielleicht erst Tage später und an einer wichtigeren Seite gemerkt.
Die Lehre daraus für jeden künftigen PHP-Versionswechsel: nach dem Umstellen nicht nur den HTTP-Status prüfen, sondern alle individuell gesetzten php.ini-Direktiven mit den Werten der vorherigen Version abgleichen — memory_limit allein reicht als Verdächtiger nicht, potenziell betrifft es jede Direktive, die einmal manuell angepasst wurde.
Der Rest des Eisbergs
Nach dem ersten Durchlauf über die ~30 ursprünglich geplanten Seiten kam die logische Nachfrage: „Und der Rest?“ Auf den drei Servern liefen noch gut 20 weitere Domains auf der alten PHP-Version — Subdomains, interne Tools, Mail-Infrastruktur-Endpunkte, die schlicht nie auf der ursprünglichen Liste standen.
Die gute Nachricht: der zweite Durchgang war fast reine Formsache. Ein gutes Drittel dieser Domains stellte sich beim genaueren Hinsehen als reine statische Platzhalter ohne aktive PHP-Verarbeitung heraus — bei denen läuft ein Versuch, den PHP-Handler umzustellen, ins Leere („PHP support is not enabled“), was kein Fehler ist, sondern schlicht bedeutet: nichts zu tun. Der Rest waren entweder bereits im ersten Durchlauf mitprofitierte Individual-Anwendungen oder direkt verifizierbare, unkritische Endpunkte.
Ein Fund am Rande, der zeigt, warum man auch bei vermeintlichen Nebenkriegsschauplätzen genau hinschauen sollte: eine kleine Individual-Anwendung antwortete nach dem Umstellen mit HTTP 404 — auf den ersten Blick ein Verdacht auf einen neuen PHP-8.5-Regressionsfehler. Der zweite Blick ins Error-Log zeigte: PHP wurde für den fehlgeschlagenen Request gar nicht erst aufgerufen. Ein .htaccess-Rewrite verwies auf eine index.php, die im Wurzelverzeichnis gar nicht existierte — ein vorbestehendes Webserver-Routing-Problem, komplett unabhängig von der PHP-Version. Ohne den Blick ins Log wäre das fälschlich als Migrationsschaden durchgegangen.
Am Ende: jede einzelne Domain auf allen drei Servern, die tatsächlich PHP ausführt, läuft jetzt auf PHP 8.5 — nachgewiesen durch einen abschließenden Datenbank-Query statt durch Bauchgefühl.
5. „Mach den Rest auch“ — den vollständigen Zielzustand verifizieren
Nach jeder Migrationsrunde: nicht der eigenen Erinnerung vertrauen,
welche Domains schon erledigt sind, sondern die Datenbank selbst
befragen, welche NICHT auf der Zielversion laufen:
plesk db -Ne "SELECT d.name, h.php_handler_id
FROM domains d JOIN hosting h ON h.dom_id=d.id
WHERE h.php_handler_id NOT LIKE 'plesk-php85%';"
Für jeden verbleibenden Treffer einzeln klären: läuft dort
überhaupt aktiv PHP (dann migrieren + verifizieren), oder ist es
eine reine statische/Platzhalter-Domain (dann bewusst als "nicht
zutreffend" markieren, nicht stillschweigend ignorieren)?
Erst wenn die Abfrage null Zeilen mit tatsächlich PHP-ausführenden
Domains zurückgibt, ist die Migration wirklich vollständig — nicht
wenn die ursprüngliche To-do-Liste abgearbeitet ist.
Fazit
Was mich an dieser Art von Arbeit mit Claude Code überzeugt: sie skaliert horizontal. Während ein Agent auf Server A einen 27-minütigen Compatibility-Scan laufen ließ, hat ein zweiter parallel auf Server B den nächsten Webmail-Build vorbereitet, und ich konnte währenddessen ganz normal weiterarbeiten und nur an den Entscheidungspunkten eingreifen — welche Domain umstellen, welchen Bug sofort fixen, was besser erstmal so bleibt. Für einen klassischen Ein-Personen-Systemadmin-Alltag mit mehreren Servern ist das genau die Art von Hebel, die den Unterschied zwischen „mach ich nächstes Wochenende mal“ und „ist heute Abend fertig“ macht.