Der Weg vom lokalen Rechner auf den Produktionsserver entscheidet mit darüber, wie stressfrei neue Versionen ausgeliefert werden. Je nach Hosting und Team-Größe reicht die Bandbreite vom simplen FTP-Upload bis zur automatisierten Pipeline. Dieses Tutorial ordnet die gängigen Deployment-Strategien für PHP ein und zeigt, welche wann passt.
Welche PHP Deployment-Strategie passt zu meinem Projekt?
Beim PHP Deployment geht es darum, eine lokal entwickelte Anwendung zuverlässig auf einen Webserver zu bringen. Beim Deployment von PHP-Anwendungen reicht das Spektrum vom manuellen FTP-Upload bis zur vollautomatischen CI/CD-Pipeline mit Tests, Atomic Deploy und Rollback. Welcher Weg richtig ist, hängt von Projektgröße, Hosting-Umgebung und Team-Reife ab.

Bevor die einzelnen Strategien von FTP bis CI/CD im Detail folgen, ein kurzer Überblick über die Querschnittsthemen, die in jeder Variante auftauchen.
Dieses Tutorial führt durch die wichtigsten Strategien, vom klassischen Upload über Git-Pull-on-Server, Composer-Install bis hin zu CI/CD mit GitHub Actions. Dabei kommen auch typische Praxisprobleme zur Sprache: wo legt man .env-Dateien hin, wie steuert man Datenbank-Migrationen, und wie sieht ein schneller Rollback bei Fehlern aus.
Klassisches FTP-PHP Deployment für Shared Hosting
Auf vielen Shared-Hosting-Paketen gibt es weder SSH noch Composer auf dem Server. In diesem Fall bleibt das PHP Deployment über FTP oder SFTP der einzige Weg. PHP selbst bringt mit den ftp_*-Funktionen alles mit, um aus einem Build-Skript heraus Dateien hochzuladen.
<?php
$ftp = ftp_connect('ftp.example.com');
ftp_login($ftp, 'user', 'passwort');
ftp_pasv($ftp, true);
ftp_put($ftp, '/htdocs/index.php', __DIR__ . '/build/index.php', FTP_BINARY);
ftp_close($ftp);
echo 'Upload abgeschlossen.' . PHP_EOL;
Diese Variante hat einen klaren Nachteil: Während der Upload läuft, sind manche Dateien schon neu, andere noch alt. Im schlechtesten Fall sieht ein User eine halb-deployte Version mit fehlenden Klassen oder gebrochenen Includes. Wer auf Shared Hosting angewiesen ist, sollte das mit einem ZIP-basierten Deploy entschärfen: Erst alle Dateien als ZIP hochladen, dann auf dem Server in einem Schritt entpacken. Damit ist die Umstellung in einem einzigen Moment vollzogen, statt über viele Sekunden Upload verteilt.
Git-Pull auf dem Server mit Composer Install
Sobald SSH und Git auf dem Server verfügbar sind, öffnen sich deutlich elegantere Wege für das PHP Deployment. Der häufigste Workflow für kleine bis mittlere Projekte ist ein Bash-Skript, das per SSH ausgeführt wird, das aktuelle Repository pullt und Composer-Abhängigkeiten installiert.
#!/bin/bash
set -e
cd /var/www/projekt
git pull origin main
composer install --no-dev --optimize-autoloader
# Cache leeren (z.B. fuer eigene Anwendung)
rm -rf var/cache/*
echo "Deployment abgeschlossen am $(date)"
Drei Punkte sind wichtig. Erstens: set -e bricht das Skript bei jedem Fehler sofort ab, sodass kein halb-deployter Stand entsteht. Zweitens: --no-dev installiert keine Test-Pakete auf der Production. Drittens: --optimize-autoloader baut einen statischen Klassen-Map, der bei jedem Request Millisekunden spart. Wer einen Cache für Templates oder kompilierte Dateien hat, sollte ihn am Ende des Deploys leeren, sonst sehen User noch eine ganze Weile alte Inhalte.
Atomic Deploy mit Symlinks für sicheres PHP Deployment
Bei wichtigen Anwendungen sollte ein PHP Deployment nie zu einem halb-aktualisierten Stand führen. Das Atomic-Deploy-Pattern löst das mit einer Verzeichnisstruktur aus mehreren Releases und einem current-Symlink, der nach dem erfolgreichen Build umgeschaltet wird. Vor dem Swap ist die alte Version aktiv, danach die neue. Dazwischen liegt nur ein einziger atomarer Symlink-Wechsel.
#!/bin/bash
set -e
cd /var/www/projekt
RELEASE="releases/$(date +%Y%m%d%H%M%S)"
mkdir -p "$RELEASE"
git clone --depth 1 git@github.com:user/projekt.git "$RELEASE"
cd "$RELEASE"
composer install --no-dev --optimize-autoloader
# .env aus shared-Ordner verlinken
ln -sfn ../../shared/.env .env
# Symlink-Swap (atomic dank ln -sfn)
cd /var/www/projekt
ln -sfn "$RELEASE" current
echo "Deploy abgeschlossen: $RELEASE"
Die Verzeichnisstruktur sieht typischerweise so aus: releases/ enthält alle bisherigen Stand-Snapshots, shared/ enthält persistente Daten wie .env, Upload-Verzeichnisse und Logs, und current ist der Symlink, den der Webserver tatsächlich anspricht. Der Trick liegt im Flag -sfn von ln: es überschreibt einen vorhandenen Symlink in einer atomaren Operation, ohne dass dazwischen ein Zustand "kein Symlink" entsteht. Damit gibt es kein Zeitfenster, in dem Requests ins Leere laufen. Genau dieses Prinzip wird in der Praxis als Zero-Downtime-Deploy bezeichnet: Aus User-Sicht ist die Umstellung in einem einzigen Augenblick vollzogen, ohne 502-Fehler oder weisse Seiten.
PHP Deployer als spezialisiertes Tool
Statt eigene Bash-Skripte zu pflegen, nutzen viele Teams Deployer, ein dediziertes PHP-Tool, das genau diese Atomic-Deploy-Mechanik fertig mitbringt. Es läuft lokal, baut per SSH die releases/-Struktur auf dem Server auf und liefert Rezepte für Symfony, Laravel, WordPress und reines PHP. Konfiguriert wird alles in einer deploy.php im Projekt-Root.
<?php
/* deploy.php: minimales Deployer-Setup */
namespace Deployer;
require 'recipe/common.php';
set('application', 'mein-projekt');
set('repository', 'git@github.com:user/projekt.git');
set('keep_releases', 5);
add('shared_files', ['.env']);
add('shared_dirs', ['storage', 'uploads']);
host('production')
->set('hostname', 'example.com')
->set('remote_user', 'deploy')
->set('deploy_path', '/var/www/projekt');
task('deploy', [
'deploy:prepare',
'deploy:vendors',
'deploy:publish',
]);
Der Aufruf vendor/bin/dep deploy production erledigt dann den kompletten Workflow: Code clonen, Composer-Install, Symlinks setzen, alte Releases aufräumen. Bei einem Fehler greift vendor/bin/dep rollback production und schaltet auf das vorherige Release zurück. Für Teams, die mehrere Projekte parallel betreuen, spart das viel handgeschriebenen Code im Vergleich zu eigenen Bash-Skripten.
CI/CD mit GitHub Actions als modernes PHP Deployment
Eine moderne Pipeline koppelt das PHP Deployment an automatische Tests. Erst wenn PHPUnit und der Linter erfolgreich durchlaufen, schlägt die Pipeline auf den Server durch. Bei Fehlern bleibt die alte Version aktiv, und das Team bekommt eine Benachrichtigung. Wer nicht selbst skripten möchte, kann auf ein dediziertes Deployment-Werkzeug wie Deployer zurückgreifen, das als spezialisiertes Werkzeug für PHP-Projekte fertige Rezepte für Frameworks wie Symfony oder Laravel mitbringt.
name: PHP Deploy
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- run: composer install --no-progress
- run: vendor/bin/phpunit
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: SSH Deploy
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd /var/www/projekt
./deploy.sh
Der needs: test-Eintrag stellt sicher, dass der Deploy-Job nur startet, wenn die Tests grün sind. Secrets wie SSH-Hostname, User und Private Key liegen in den GitHub-Repository-Settings unter "Secrets and variables", nicht im YAML selbst. Wer mehr Verlässlichkeit will, ergänzt einen Slack- oder E-Mail-Step, der Erfolg und Fehler an das Team meldet. Mit PHPUnit-Tests im Vorfeld ist die Pipeline bereit, mehrfach pro Tag zu deployen, ohne dass jemand die Knoten manuell anfasst.
.env-Dateien und Secrets im PHP Deployment sicher handhaben
Datenbank-Passwörter, API-Keys und SSH-Schlüssel dürfen im PHP Deployment niemals in Git landen. Der pragmatische Standard ist eine .env.example im Repository, die alle benötigten Schlüssel mit Platzhaltern enthält, und eine echte .env auf dem Server, die außerhalb von Git gepflegt wird. Per .gitignore wird .env ausgeschlossen, sodass auch ein versehentlicher Commit nicht passiert.
# .env.example (committed)
DB_HOST=localhost
DB_NAME=app
DB_USER=
DB_PASS=
APP_KEY=
# .gitignore
.env
*.log
vendor/
node_modules/
Auf dem Server liegt die .env typischerweise im shared/-Ordner und wird per Symlink in jedes neue Release verlinkt. So überlebt sie jedes Deploy unangetastet. Wer mit mehreren Servern arbeitet, sollte einen Secret-Manager wie HashiCorp Vault oder die AWS-Parameter-Store-Variante einsetzen. Im Skript wird dann nicht die .env kopiert, sondern die Werte werden zur Laufzeit aus dem Secret-Manager als Umgebungsvariablen geladen.
Datenbank-Migrationen kontrolliert ausführen
Schema-Änderungen müssen reproduzierbar sein. Die einfachste, aber bewährte Variante ist eine migrations-Tabelle in der Datenbank, die festhält, welche Migrationen bereits gelaufen sind. Vor jedem Deploy iteriert ein kleines PHP-Skript über alle SQL-Dateien im Verzeichnis und führt nur jene aus, die noch nicht in der Tabelle stehen.
<?php
$pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pw');
$pdo->exec('CREATE TABLE IF NOT EXISTS migrations (
name VARCHAR(255) PRIMARY KEY,
applied_at DATETIME NOT NULL
)');
foreach (glob(__DIR__ . '/migrations/*.sql') as $datei) {
$name = basename($datei);
$stmt = $pdo->prepare('SELECT 1 FROM migrations WHERE name = ?');
$stmt->execute([$name]);
if ($stmt->fetchColumn()) {
echo "[skip] $name" . PHP_EOL;
continue;
}
$pdo->exec(file_get_contents($datei));
$pdo->prepare('INSERT INTO migrations VALUES (?, NOW())')->execute([$name]);
echo "[ok] $name" . PHP_EOL;
}
Das Skript läuft im Deploy als ein Schritt vor dem Symlink-Swap. Schlägt eine Migration fehl, bricht das Deploy ab, der current-Symlink bleibt auf dem alten Release, und das Team kann die Datei in Ruhe reparieren. Naming wie 001_init.sql, 002_add_users.sql hält die Reihenfolge stabil, weil glob() Dateien alphabetisch sortiert ausgibt.
flowchart TD
A[git push origin main] --> B[CI: PHPUnit + Lint]
B --> C{Tests ok?}
C -->|Nein| D[Build abbrechen]
C -->|Ja| E[SSH zum Server]
E --> F[Neues Release-Verzeichnis]
F --> G[Code clonen + composer install]
G --> H[Migrationen ausfuehren]
H --> I[Symlink current -> release_neu]
I --> J[Bei Fehler: Symlink zurueckschalten]
Rollback-Strategien für PHP Deployment bei Problemen
Trotz aller Vorbereitung kann ein PHP Deployment schiefgehen. Mit der Atomic-Deploy-Struktur ist Rollback eine Sache von Sekunden: Der Symlink wird auf das vorherige Release umgeschaltet, und die alte Version ist wieder aktiv. Vor jedem Swap sollte das Deploy-Skript den aktuellen Symlink-Ziel-Wert speichern, sodass im Fehlerfall klar ist, wohin zurückgesprungen werden muss.
#!/bin/bash
# rollback.sh
set -e
cd /var/www/projekt
PREVIOUS=$(ls -1t releases/ | sed -n '2p')
if [ -z "$PREVIOUS" ]; then
echo "Kein vorheriges Release gefunden."
exit 1
fi
ln -sfn "releases/$PREVIOUS" current
echo "Rollback auf $PREVIOUS abgeschlossen."
Bei Datenbank-Änderungen ist ein Rollback ungleich heikler. Wer eine Spalte hinzugefügt hat, kann die alte Anwendung weiterlaufen lassen; wer eine Spalte umbenannt oder gelöscht hat, hat ein Problem. Die Faustregel lautet, Migrationen rückwärtskompatibel zu halten. Erst wird die neue Spalte hinzugefügt, dann die Anwendung umgestellt, in einem späteren Deploy die alte Spalte entfernt. So bleibt jeder Zwischenschritt rollback-fähig.
Container-basiertes Deployment kurz eingeordnet
Alle bisherigen Strategien gehen von einem klassischen Server mit installierter PHP-CLI und SSH-Zugang aus. Bei einem Container-basierten Deployment mit Docker oder Kubernetes ändert sich der Workflow grundlegend: Statt Code in ein bestehendes System zu schieben, wird ein neues Image gebaut, das den kompletten Stack enthält. Der Deploy-Vorgang besteht dann aus docker build, docker push in eine Registry und einem Rolling Update im Cluster, das Pods schrittweise austauscht und damit die Zero-Downtime-Garantie auf Plattform-Ebene umsetzt.
Das ist ein eigenes Themengebiet mit eigenen Werkzeugen wie Helm, ArgoCD oder GitLab Auto-DevOps. Für klassische LAMP-Setups bleiben die hier gezeigten Pattern aus Atomic-Deploy plus CI/CD die naheliegende Wahl. Wer langfristig auf Container umstellen möchte, findet im Tutorial zu PHP mit Docker den passenden Einstieg.
Fazit zum PHP Deployment
Ein gutes PHP Deployment wählt die einfachste Strategie, die für das Projekt noch ausreicht. Hobby-Seiten kommen mit FTP gut aus, mittlere Projekte profitieren von Git-Pull-on-Server plus Composer, professionelle Anwendungen brauchen Atomic Deploys mit Symlinks und CI/CD-Pipelines. Drei Aspekte sind überall gleich: .env-Dateien gehören außerhalb von Git, DB-Migrationen brauchen Tracking, und ein Rollback-Pfad muss im Vorfeld geplant sein. Mit GitHub Actions oder GitLab CI lässt sich aus einem git push ein automatischer, getesteter und reproduzierbarer Deploy machen, der die Auslieferung neuer Versionen vom seltenen Stress-Event zur Routineaufgabe degradiert.