Themen per FTP hochzuziehen endet 2026 meist gleich: eine Datei aus inc/ fehlt, composer.lock stammt von einem anderen Laptop, und Produktion liegt, weil jemand wp-config.php überschrieben hat. Continuous Integration und Continuous Deployment sind keine Enterprise-Show - sie sorgen dafür, dass jede Git-Änderung denselben Build, dieselben Tests und denselben Weg auf den Server nimmt.
Dieser Leitfaden gilt für WordPress mit Theme oder Plugin im Repository (Composer, npm, optional Docker), nicht für Seiten, die nur im Admin leben. Liegt Custom-Code noch nur in Datenbank und Medienbibliothek, bring Themes und eigenen Code zuerst in Git - sonst hat CI/CD nichts zu bauen.
Runner-Dokumentation: GitHub Actions. Parallel lesenswert: Hardening WordPress. Für Befehle nach dem Deploy: WP-CLI.
Warum WordPress per FTP deployen riskant ist
Drei Muster tauchen in Audits deutscher WooCommerce-Shops und Agentur-Übergaben immer wieder auf:
- Umgebungsdrift - lokal PHP 8.2 und Composer 2.7, auf dem Hetzner-VPS PHP 8.1 ohne
intl. Lokal „läuft“composer install, Produktion stirbt an class not found. - Kein Audit-Trail - nach einem Vorfall ist unklar, welcher Commit live war. Das Hosting-Panel zeigt ein Dateidatum, keine Git-SHA.
- Secrets auf der Platte - SFTP-Passwörter in FileZilla oder eine „kurz“ committed
.envbleiben der klassische Leak-Pfad - und bei DSGVO-Fällen teuer.
CI/CD löst das mechanisch: Build auf bekanntem Image (shivammathur/setup-php oder eigenes Docker), Artefakt versioniert nach SHA, Deploy per SSH-Key aus Secrets.
WordPress mit GitHub Actions deployen
Typischer Workflow für Custom-Theme plus Composer-Plugins:
- Trigger - Push auf
mainoder Merge eines reviewed Pull Requests.developgeht oft nur auf Staging. - Build -
composer install --no-dev --optimize-autoloader,npm ci,npm run build. PHP und Node im Workflow pinnen (php-version: '8.3',node-version: '24'). - Test - PHPUnit für Plugins, optional Playwright auf dem Checkout. Fail = Stop, kein Deploy.
- Artefakt - Release ohne
.git, ohnenode_modules, mitvendor/und gebautemassets/dist/. - Deploy - rsync/SCP nach
releases/<run_id>/, danach atomares Symlink-Umschalten.
Pinnen Sie Versionen, die noch Fixes bekommen. PHP 8.2 erhält Sicherheitsupdates nur bis 31. Dezember 2026, PHP 8.3 bis 31. Dezember 2027 (unterstützte PHP-Versionen). Node.js 20 hat am 30. April 2026 sein Lebensende erreicht; Node.js 22 wird bis 30. April 2027 unterstützt, Node.js 24 bis 30. April 2028 (Node.js-Release-Plan). Die PHP-Minor-Version richtet sich nach der Produktion, nicht nach dem neuesten Release.
Kurzskizze in GitHub Actions, mit Major-Tags zur besseren Lesbarkeit; warum produktive Workflows Commit-SHAs pinnen, erklärt der Abschnitt zur Härtung weiter unten:
name: deploy-production
on:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
build-and-deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install --no-dev --optimize-autoloader
- uses: actions/setup-node@v7
with:
node-version: '24'
cache: npm
- run: npm ci && npm run build
- name: rsync release
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
# Key schreiben, rsync nach releases/$GITHUB_SHA, ln -sfnDas volle Symlink-Skript liegt im Repo als bin/release.sh - der Workflow ruft es nur auf. Derselbe Skriptpfad funktioniert vom Laptop, wenn Actions ausfällt.
Parallele WordPress-Deployments in GitHub Actions verhindern
Zwei schnelle Merges auf main starten zwei Workflow-Läufe. Ohne Sperre schreiben beide per rsync nach releases/ und schalten current in einer Reihenfolge um, die niemand gewählt hat. Eine concurrency-Gruppe in GitHub Actions erlaubt höchstens einen laufenden Workflow pro Gruppe; ein neu eingereihter Lauf wartet als pending (GitHub-Doku: Workflow-Parallelität steuern).
Für Deployments zählen zwei Details. Standardmäßig hält eine Gruppe einen laufenden und einen wartenden Lauf, und ein neuerer Lauf bricht den älteren wartenden ab: Der letzte Commit gewinnt, der mittlere wird übersprungen. cancel-in-progress: true würde auch den laufenden Job abbrechen und kann rsync mitten in einem Release-Verzeichnis stoppen. Für Produktion bleibt es bei false; true passt zu Test-Workflows auf Pull Requests, wo nur der neueste Commit zählt.
concurrency:
group: deploy-production
cancel-in-progress: falseWeil der Symlink erst nach vollständigem rsync umschaltet, berührt ein abgebrochener wartender Lauf current nie.
Produktion mit GitHub-Actions-Umgebungen schützen
Ergänzen Sie environment: production im Deploy-Job und konfigurieren Sie die Umgebung in den Repository-Einstellungen. Dann greifen drei Schutzmechanismen (GitHub-Doku: Deployments und Umgebungen):
- Erforderliche Reviewer: bis zu sechs Personen oder Teams, eine Freigabe genügt. Mit aktivierter Sperre gegen Selbstfreigabe kann die Person, die den Deploy gestartet hat, ihn nicht genehmigen.
- Deployment-Branches und -Tags: Nur
mainoder Release-Tags dürfen nachproductiondeployen, ein Workflow auf einem Feature-Branch erreicht den Produktionsserver also nicht. - Umgebungs-Secrets: nur für Jobs verfügbar, die die Umgebung referenzieren, und bei erforderlichen Reviewern erst nach der Freigabe. Der produktive
DEPLOY_SSH_KEYgehört hierher statt in die Repository-Secrets.
Prüfen Sie vorher den Plan. In GitHub Free, Pro und Team funktionieren erforderliche Reviewer und Wartezeiten nur in öffentlichen Repositories. Deployment-Branches und Umgebungs-Secrets gehen in privaten Repositories mit Pro und Team, aber nicht mit Free. Agentur-Repositories für WordPress sind meist privat, auf diesen Plänen fehlt die Freigabe-Schranke also, und die Branch-Regeln tragen den Schutz.
GitHub-Actions-Workflow für das Deployment härten
Der Deploy-Job hält einen SSH-Schlüssel zur Produktion, also verdient die Workflow-Datei dasselbe Review wie PHP-Code. GitHubs Referenz zur sicheren Nutzung nennt die Maßnahmen, die hier zählen:
- Fremde Actions auf den vollständigen Commit-SHA pinnen. Ein Tag wie
@v2lässt sich verschieben oder löschen. Einen SHA umzubiegen bräuchte eine SHA-1-Kollision. Den SHA aus dem Repository der Action selbst nehmen, nicht aus einem Fork. GitHub nennt inzwischen eine Lockfile für Workflow-Abhängigkeiten als stärkere Option für Repositories, die sie einführen. GITHUB_TOKENminimal halten.permissions: contents: readoben setzen und nur pro Job erhöhen, wo ein Schritt mehr braucht. Ein rsync-Deploy über SSH braucht nur Lesezugriff auf das Repository.- Nicht vertrauenswürdige Eingaben aus
run:heraushalten. Ein Pull-Request-Titel oder Branch-Name in${{ }}innerhalb eines Shell-Skripts öffnet Skript-Injection. Den Wert über eine Zwischen-Umgebungsvariable übergeben. - Ein Secret pro Wert. Strukturierte Daten wie ein JSON-Block können die Maskierung in Logs aushebeln, deshalb Host, Benutzer, Pfad und Schlüssel als getrennte Secrets ablegen.
Dieselbe Seite weist darauf hin, dass nur Secrets maskiert werden, die im Job verwendet werden und für den Runner zugänglich sind. Abgeleitete Werte wie einen dekodierten Schlüssel also nie per echo ausgeben.
Zero-Downtime-Deployment für WordPress per Symlink
Dateien „in place“ unter public_html zu überschreiben heißt: ein paar Sekunden lang sieht die Hälfte der Requests altes functions.php und neues Template. Bei WooCommerce in der Black-Week-Woche kann das Warenkorb-Sessions zerstören.
Atomares Layout:
- Basis:
/var/www/site/ - Releases:
/var/www/site/releases/20260920-142211/ - Shared: Uploads,
wp-config.php, Object-Cache - außerhalb des Releases current: Symlink auf den aktiven Release
Nach erfolgreichem rsync:
ln -sfn /var/www/site/releases/20260920-142211 /var/www/site/currentNginx root zeigt auf .../current/public (oder das Äquivalent). Der Wechsel ist sofort. Alte Releases nach N Tagen löschen oder die letzten drei für Rollback behalten.
WordPress-Uploads müssen außerhalb des Releases liegen - sonst löscht jeder Deploy Medien oder kopiert Gigabytes. Klassiker: shared/uploads verlinkt nach wp-content/uploads in jedem Release.
WordPress Staging und Deployment-Strategien im Vergleich
2026 ist der Weg vom Feature-Branch über Pull Request und Staging bis main und Produktion für Shops und Lead-Seiten nicht optional. Staging braucht:
- dieselbe PHP-Version und dieselben Extensions wie Produktion
- eine DB-Kopie (anonymisiert, wenn personenbezogene Daten unter der DSGVO liegen)
- Payment-Secrets nur im Sandbox-Modus
Preview-URLs pro PR helfen bei Redesign und CSS, weniger bei Schema-Migrationen - die testen Sie auf Staging mit einem echten Dump.
| Strategie | Risiko | Pflegeaufwand | Wann sinnvoll |
|---|---|---|---|
| Manuelles FTP | Sehr hoch | Günstig starten, teure Ausfälle | Nur Sandbox / Lernen |
| Git-Hook auf dem Server | Mittel | Niedrig | Kleines Portfolio, ein Entwickler |
| CI + atomares rsync | Niedrig | Mittel | Business, WooCommerce, Agenturen |
| Blue/Green oder zwei VPS | Minimal | Hoch | Viel Traffic, SLA, Zahlungen |
Blue/Green (zwei volle Umgebungen, Load-Balancer-Umschaltung) lohnt sich selten unter hunderten parallelen Sessions. Für die meisten DACH-Shops reicht atomarer Symlink plus Verzeichnis-Rollback - oft auf Hetzner oder einem Managed-Host mit SSH.
Automatisierte Tests in einer WordPress-CI/CD-Pipeline
Eine Pipeline ohne Tests liefert kaputten Code nur schneller aus. Vier Ebenen passen zu einem WordPress-Repository, von schnell nach langsam:
- Unit-Tests mit PHPUnit prüfen einzelne Funktionen eines Plugins oder Themes ohne Datenbank. Sie laufen bei jedem Push.
- Integrationstests laden WordPress gegen eine temporäre Testdatenbank.
wp scaffold plugin-testserzeugt das Gerüst:phpunit.xml.dist,bin/install-wp-tests.shfür Testsuite und Datenbank,tests/bootstrap.php, einen Beispieltest und ein Regelwerk.phpcs.xml.dist. Standard ist eine CircleCI-Datei;--ci=githubschreibt stattdessen eine Konfiguration für GitHub Actions (WP-CLI: wp scaffold plugin-tests). - Statische Analyse mit PHPStan und PHP_CodeSniffer findet Typfehler und Verstöße gegen Coding Standards, ohne den Code auszuführen.
- End-to-End-Tests mit Playwright gehen echte Nutzerwege durch: Kontaktformular, WooCommerce-Warenkorb und Checkout. Sie sind die langsamste Ebene, also auf Pull Requests nach
mainund gegen Staging laufen lassen, nicht bei jedem Commit.
Ein roter Job auf irgendeiner Ebene stoppt den Workflow vor dem Artefakt-Schritt, und nichts erreicht den Server.
Secrets und Datenbank-Migrationen beim WordPress-Deployment
Nie committen:
- SSH-Keys
- Datenbankpasswörter
AUTH_KEY/SECURE_AUTH_KEYauswp-config.php- API-Tokens von Payment-Gateways (PayPal, Stripe, deutsche PSP)
In GitHub liegen sie unter Settings, dann Secrets and variables, dann Actions. Als env im Deploy-Schritt injizieren. Auf dem Server liegt wp-config.php unter shared/ und ist kein Teil des CI-Artefakts.
WP-CLI nach dem Symlink-Wechsel:
wp cache flushbei Object Cachewp rewrite flushnach CPT-Änderungenwp plugin listals Smoke-Zeile im Log
WP-CLI vom Runner aus auf dem Produktionsserver ausführen
Diese Befehle müssen auf dem Server laufen, gegen den neuen Release. WP-CLI erledigt das vom Runner aus mit dem globalen Parameter --ssh ([<scheme>:][<user>@]<host>[:<port>][<path>]) oder mit Aliasen in der projekteigenen wp-cli.yml (WP-CLI-Handbuch: Befehle remote ausführen):
@staging:
ssh: [email protected]/var/www/site/current/public
@prod:
ssh: [email protected]/var/www/site/current/publicMit dieser Datei im Repository läuft wp @prod cache flush auf der Produktion. WP-CLI muss trotzdem auf dem Server installiert und als wp aufrufbar sein: Der Runner reicht den Befehl nur per SSH durch.
Weitere Befehle für den Schritt nach dem Umschalten, alle in der WP-CLI-Befehlsreferenz:
wp core is-installedgibt nichts aus und antwortet über den Exit-Code, eine fehlgeschlagene Prüfung lässt den Schritt also scheitern.wp core update-db --dry-runvergleicht die Datenbankversionen vor dem echtenwp core update-dbnach einem Core-Update.wp core verify-checksumsgleicht die Core-Dateien mit den Prüfsummen von WordPress.org ab und findet Dateien, die außerhalb von Git geändert wurden.wp maintenance-mode activateunddeactivatedecken den seltenen Release ab, der das Schema ändern muss, während nichts hineinschreibt.
Datenbank-Migrationen getrennt vom Datei-Deploy planen. Symlink-Rollback macht kein ALTER TABLE rückgängig. Bei destruktiven Migrationen: zuerst Backup (wp db export auf Staging und Produktion), dann Migration, dann Traffic auf Code, der das Schema braucht - oder Feature-Flag in PHP.
PHP-Abhängigkeiten: Composer install - in CI für Produktion immer --no-dev und gesperrtes composer.lock.
Häufige Fehler in der WordPress-Deployment-Pipeline
Aus Praxis auf VPS (Hetzner, DigitalOcean, OVH) und Managed mit SSH:
.gitignorefrisstvendor/- Produktion bekommt ein Theme ohne Autoloader. Entweder Vendor auf dem Runner bauen und ins Artefakt packen, oder Composer nach dem rsync auf dem Server laufen lassen (dann braucht der Server Composer und Packagist-Zugang).- Falsche Pfade - Skript erwartet
/var/www/html, Hosting nutzt/home/user/domains/.... Pfade in Secrets pro Umgebung pinnen. - Opcache hält alten Code - nach
ln -sfnPHP-FPM reloaden oderopcache_resetper WP-CLI/mu-Plugin nur für den Deploy-User. - Cron und Queues - Action Scheduler von WooCommerce kann Jobs noch Minuten auf altem Code fahren; nach großen Releases Failed Jobs kurz beobachten.
Smoke-Test nach dem Deploy (curl vom Runner oder eigener Job):
- Startseite HTTP 200
/wp-login.php200- eine kritische Produkt- oder Formular-URL
- optional HEAD an das CDN bei gehashten Assets
Schlägt Smoke fehl, ist automatisches Symlink-Rollback billiger als der Kundenanruf um 23:00.
Block-Themes und MU-Plugins mit Git deployen
Nicht jedes WordPress-Stück passt in dieselbe Pipeline. Inhalte in der Datenbank (Beiträge, ACF-Field-Groups ohne JSON-Export, Elementor-CSS) leben außerhalb des Releases. CI/CD schützt Code, ersetzt kein DB-Backup.
Praktische Trennung:
- Klassisches oder hybrides Theme mit Vite - voller CI-Build, Artefakt mit
style.css+dist/. - Block-Theme (FSE) -
theme.jsonund HTML-Templates in Git; Änderungen aus dem Site-Editor zurück per PR, sonst divergieren Produktion und Git nach dem ersten „Speichern“. - Must-Use-Plugins - mit dem Release ausrollen; nicht aus dem Admin updaten. Guter Ort für Deploy-Healthchecks und das Abschalten des Dateieditors.
- Plugins von wordpress.org - Versionen per Composer (
wpackagist) pinnen oder in einem reviewed Job aktualisieren. „Update im Admin“ gemischt mit „CI überschreibtplugins/“ endet in einem Zufallsbaum.
Agentur-Stacks im DACH-Raum mischen oft Custom-Theme, WooCommerce aus wpackagist und zwei Premium-ZIPs über ein Composer-Path-Repository. Die Pipeline muss alle drei Quellen kennen, sonst unterscheiden sich Staging und Produktion um ein ZIP von vor einem halben Jahr.
Checkliste für WordPress Continuous Deployment
Bevor main an Produktion hängt, diese Liste einmal durchgehen - spart ein Wochenende:
- Privates Repo oder Historie ohne Secrets (
git log -pzeigt keine alten.env). - Branch Protection: Pflicht-PR, grüner
test-Workflow. - Getrennte Secrets:
DEPLOY_HOST_STAGING,DEPLOY_HOST_PROD, eigene SSH-Keys. - Staging mit derselben PHP-Version wie Produktion (nicht „wir 8.3, Kunde 8.1“).
- DB-Backup vor dem ersten atomaren Umschalten - auch wenn Code-Rollback steht.
- Uptime-Monitoring (UptimeRobot, Better Stack o. Ä.) mit SMS/Slack auf die Produktions-URL.
- Einseitiges Runbook: manuelles
ln -sfnauf den vorherigen Release, wer Serverzugriff hat.
Erst dann Auto-Deploy von main. Bis dahin reichen „Build + Test“ auf PRs und manueller workflow_dispatch auf Staging.
Zusammenfassung
CI/CD für WordPress ist kein „magisches YAML“, sondern ein wiederholbarer Vertrag: dasselbe PHP, derselbe Lockfile, dasselbe Artefakt, Umschalten ohne Fenster halb hochgeladener Dateien. Sie starten mit Git und Staging, legen Actions für Composer/npm drauf und enden bei atomarem current plus Smoke-Test. FTP wird Notfallwerkzeug, nicht Prozess.
Wenn Sie für Shop oder Lead-Seite einen WordPress-Entwickler brauchen, der die Pipeline neben dem Theme-Code hält, arbeitet WPPoland am realen Stack - nicht an Folien.







