Ein zero downtime deploy bedeutet, eine neue Version auszurollen, ohne dass auch nur ein einziger Nutzer jemals eine Fehlerseite zu sehen bekommt. Im klassischen Ansatz verbindest du dich mit dem Server, kopierst Dateien über das Live-Verzeichnis, installierst Abhängigkeiten und startest den Dienst neu — aber während dieser wenigen Sekunden liefert die Seite entweder halb geschriebene Dateien aus oder fällt komplett aus. In diesem Leitfaden zeige ich dir Schritt für Schritt, wie du eine nahtlose Umschaltung für kleine und mittlere Projekte einrichtest, mit nichts weiter als einem Symlink (symbolischen Link) und einem Health Check, ohne zu einem schweren Orchestrierungswerkzeug greifen zu müssen.
Das Problem: warum "kopieren und neu starten" Ausfallzeit verursacht
Dateien direkt über das Live-Verzeichnis zu schreiben birgt zwei Gefahren. Erstens: Wird das Kopieren unterbrochen, liefert der Webserver eine Mischung aus altem und neuem Code aus — was in PHP fatale Fehler, Asset-Inkonsistenzen oder halb gerenderte Templates bedeutet. Zweitens dauern Schritte wie composer install oder das Ausführen von Migrationen mehrere Sekunden, und während dieses gesamten Zeitfensters bleibt die Anwendung in einem inkonsistenten Zustand.
Der Kern der Lösung ist einfach: Du installierst die neue Version in einem separaten Ordner, und sobald alles bereit ist, schaltest du in einer einzigen Operation den Link, der auf das Live-Verzeichnis zeigt, auf den neuen Ordner um. Unter Linux ist das Ersetzen eines Symlinks eine atomare Operation — der Server sieht entweder den komplett alten oder den komplett neuen Ordner, niemals eine Mischung aus beiden.
Verzeichnisstruktur: releases, shared und current
Es gibt eine bewährte Struktur, die Werkzeuge wie Capistrano und Deployer seit Jahren verwenden. Richte diese Aufteilung auf dem Server ein:
/var/www/app/
├── releases/
│ ├── 2026-06-28-101500/
│ └── 2026-06-28-094200/
├── shared/
│ ├── .env
│ └── storage/
└── current -> releases/2026-06-28-101500/
Die Logik ist folgende: Jeder Deploy landet als neuer, mit Zeitstempel versehener Ordner unter releases/. Dinge, die sich zwischen Versionen nicht ändern und erhalten bleiben müssen (die Environment-Datei, hochgeladene Bilder, Logs), liegen unter shared/ und werden per Symlink in jede Release eingebunden. Das Document Root des Webservers zeigt immer auf den current-Link. Die Umschaltung besteht lediglich darin, diesen current-Link zu verschieben.
Der Deploy-Ablauf, Schritt für Schritt
Die folgende Reihenfolge ist das Gerüst einer sauberen Umschaltung. Führe jeden Schritt nur aus, wenn der vorherige erfolgreich war:
- Den neuen Release-Ordner anlegen und den Code darin entpacken (git clone, tar pipe oder ein CI-Artefakt).
- Die gemeinsamen Ressourcen verlinken: Elemente wie
.envundstorageper Symlink ausshared/in die neue Release einbinden. - Abhängigkeiten installieren:
composer install --no-dev -o, gefolgt von einem Frontend-Build, falls nötig. - Migrationen und Cache: Wenn sich das Schema geändert hat, führe
php artisan migrate --forceaus, dannphp artisan optimize. - Den Health Check ausführen: prüfen, dass die neue Release tatsächlich hochfährt.
- Den Symlink umschalten: nur wenn die Prüfung besteht, lass
currentauf den neuen Ordner zeigen.
Der entscheidende Punkt: Heb dir den Symlink für den Schluss auf. Da die langsamen Operationen — Build, Installation der Abhängigkeiten, Migration — alle in einem Ordner stattfinden, der noch nicht live ist, sind die Nutzer davon nie betroffen.
Das Herzstück der Umschaltung: den Symlink ersetzen
Der richtige Weg, einen Symlink an Ort und Stelle zu aktualisieren, ist ln -sfn. Das Flag -f überschreibt den bestehenden Link, während -n verhindert, dass der Link innerhalb des Ziels erstellt wird, wenn dieses Ziel ein Verzeichnis ist:
ln -sfn /var/www/app/releases/2026-06-28-101500 /var/www/app/current
Dieser eine Befehl ist der Moment, in dem die gesamte Umschaltung geschieht. Auf den meisten Systemen erstellt ln das Ziel zunächst unter einem temporären Namen und verschiebt es dann mit einem rename()-Aufruf an seinen Platz, und rename() ist auf demselben Dateisystem atomar. Nach der Umschaltung haben Prozesse wie PHP-FPM möglicherweise noch den alten Pfad im OPcache; lade PHP-FPM also danach sanft neu (zum Beispiel systemctl reload php8.3-fpm) oder setze OPcache zurück.
Health Check: niemals eine kaputte Version freigeben
Die letzte Barriere vor der Umschaltung ist der Health Check. Das Ziel ist, — bevor der Symlink umgeschaltet wird — zu bestätigen, dass die neue Release tatsächlich antwortet. Füge deiner Anwendung einen einfachen Endpunkt hinzu (zum Beispiel /health) und frage ihn im Deploy-Skript ab:
URL="http://127.0.0.1/health"
for i in $(seq 1 10); do
code=$(curl -s -o /dev/null -w "%{http_code}" "$URL")
if [ "$code" = "200" ]; then
echo "Health OK"; exit 0
fi
sleep 2
done
echo "Health Check fehlgeschlagen, Deploy abgebrochen"; exit 1
Erhält das Skript etwas anderes als einen 200, stoppt es mit exit 1 und der current-Link zeigt weiterhin auf die alte, funktionierende Version. Der Health-Endpunkt sollte nicht oberflächlich sein: Wenn er Abhängigkeiten prüft — eine Datenbankverbindung, den Zugriff auf einen kritischen Cache — statt nur "PHP läuft" zu sagen, schließt er das Risiko aus, eine halb kaputte Version freizugeben.
Rollback und Aufräumen alter Releases
Das Beste an dieser Aufteilung ist, dass das Zurückrollen genauso schnell geht wie das Deployen. Entdeckst du ein Problem, genügt es, current wieder auf die vorherige Release zeigen zu lassen:
ln -sfn /var/www/app/releases/2026-06-28-094200 /var/www/app/current
systemctl reload php8.3-fpm
Damit sich alte Releases nicht auf der Festplatte ansammeln, füge einen kleinen Aufräumschritt hinzu, der die neuesten 3 bis 5 behält und den Rest löscht. So behältst du ein paar Backups für das Rollback, während die Festplatte nie volläuft.
Ein Hinweis zu Migrationen: abwärtskompatible Änderungen
Der Symlink mag atomar sein, das Datenbankschema ist es nicht. Alter und neuer Code teilen sich während der Umschaltung kurz dieselbe Tabelle. Teile destruktive Schemaänderungen (eine Spalte löschen oder umbenennen) daher in zwei Phasen: füge zuerst die neue Struktur hinzu und deploye so, dass beide Versionen weiter funktionieren; bereinige dann, sobald der alte Code vollständig stillgelegt ist, die alten Spalten in einem späteren Deploy. Dieser "Expand and Contract"-Ansatz ist die verborgene andere Hälfte echter Ausfallfreiheit.
Häufig gestellte Fragen
Brauche ich dafür Docker oder Kubernetes?
Nein. Symlink-basierte Deploys funktionieren perfekt auf einem einzelnen VPS oder Shared Hosting, ganz ohne Container. Docker und Kubernetes bringen einen Mehrwert, sobald du skalierst und mehrere Server oder Replicas verwalten musst, aber für kleine bis mittlere Projekte ist der Symlink-Ansatz weitaus einfacher und völlig ausreichend.
Brauche ich wirklich einen eigenen Health-Check-Endpunkt?
Die Startseite abzufragen funktioniert auch, aber ein dedizierter /health-Endpunkt ist vorzuziehen. Er kann gezielt Abhängigkeiten prüfen (DB, Cache), leichtgewichtig bleiben und überwacht werden, ohne Rauschen in deinen Logs zu erzeugen.
Warum PHP-FPM nach dem Umschalten des Symlinks neu laden?
OPcache kann Dateien anhand ihrer aufgelösten realen Pfade cachen. Selbst nach dem Umschalten des Links können Prozesse weiterhin den alten Code ausliefern. Ein reload (kein Restart) frischt diesen Cache auf, und zwar ohne Ausfallzeit zu verursachen.
Möchtest du eine Deployment-Pipeline ohne Ausfallzeit? Ob Laravel oder ein anderer Stack — ich helfe dir, auf deinem bestehenden Server einen symlink-basierten Deploy-Ablauf einzurichten, mit Health Check und Rollback per einzelnem Befehl. Kontaktiere mich und lass uns über dein Projekt sprechen.