Symfony Deployment: Zero Downtime mit Deployer PHP
AI generated
SF
{ }
Symfony · Deployer PHP · DevOps · CI/CD
Symfony Deployment:
Zero Downtime mit Deployer PHP

Jedes Symfony-Deployment, das die Anwendung kurzzeitig in einen inkonsistenten Zustand versetzt, ist ein vermeidbares Risiko. Deployer PHP löst das mit Atomic Symlinks: Der neue Release ist vollständig gebaut und bereit, bevor ein einziger Request darauf trifft — der Symlink-Swap dauert Millisekunden und macht einen Rollback trivial.

16 Min. Lesezeit Deployer · Atomic Symlinks · Rollback · Migrations · Multi-Stage Symfony 7.x · Deployer 7.x · PHP 8.3+

1. Warum klassische Deployments Ausfallzeiten verursachen

Ein klassisches Symfony-Deployment ohne Zero-Downtime-Strategie folgt einem gefährlichen Muster: Der neue Code wird in das aktuelle Verzeichnis kopiert oder per git pull eingezogen, dann werden Composer-Abhängigkeiten installiert, Assets gebaut und der Cache geleert. Während dieser Sequenz, die je nach Projekt zwischen 30 Sekunden und mehreren Minuten dauert, befindet sich die Anwendung in einem teilweise aktualisierten Zustand. Requests treffen auf neuen PHP-Code, der noch mit dem alten Composer-Autoloader läuft, oder auf alte Templates, während der neue Cache noch aufgebaut wird. Das Resultat sind 500-Fehler, inkonsistente Datenbankzustände durch unfertige Migrationen und im schlimmsten Fall Datenverlust.

Das Problem ist strukturell, nicht operationell. Es liegt nicht daran, dass Deployments falsch ausgeführt werden, sondern daran, dass der Deployment-Prozess keinen atomaren Übergang zwischen zwei Zuständen kennt. Zero-Downtime-Deployment mit Deployer PHP löst das durch Parallelität statt Sequenzialität: Der neue Release wird vollständig neben dem laufenden System aufgebaut. Der Switch passiert nicht durch das Ersetzen von Dateien, sondern durch das Umsetzen eines einzigen Symlinks auf Betriebssystemebene — eine Operation, die das Betriebssystem atomar ausführt. Kein Request landet auf einem halbfertigen Stand.

2. Deployer PHP installieren und konfigurieren

Deployer PHP wird als globales PHAR-Tool oder als Composer-Dev-Dependency installiert. Die Empfehlung für Symfony-Projekte ist die Composer-Dev-Dependency, weil sie die Versionierung im Projekt sichert und im CI/CD-Workflow konsistent verfügbar ist. Das Symfony-Rezept ist als eigenes Paket verfügbar und enthält vordefinierte Tasks für Cache-Warmup, Asset-Installation und Migrations-Ausführung. Die Konfigurationsdatei deploy.php im Projektroot ist der einzige Einstiegspunkt — dort werden Hosts, Pfade, Shared-Verzeichnisse und der Task-Ablauf definiert.

Die Verzeichnisstruktur auf dem Zielserver nach dem ersten Deployer PHP-Deployment zeigt das Muster klar: Ein releases/-Verzeichnis enthält nummerierte Release-Ordner, shared/ enthält die Release-übergreifenden Dateien wie .env.local und den var/log-Ordner, und current ist der Symlink, der auf das aktive Release zeigt. Der Webserver zeigt auf current/public — nicht auf ein konkretes Release-Verzeichnis. Das ist die Basis für den atomaren Release-Wechsel und die Rollback-Fähigkeit ohne manuellen Eingriff.


<?php
// deploy.php — Root configuration file for Deployer PHP
namespace Deployer;

require 'recipe/symfony.php';

// Project name used in log output and notifications
set('application', 'my-symfony-app');

// Git repository — SSH access required on the server
set('repository', 'git@github.com:vendor/my-symfony-app.git');

// Keep last 5 releases on server for rollback capability
set('keep_releases', 5);

// Shared files/dirs between releases — survive across deployments
set('shared_files', ['.env.local']);
set('shared_dirs', ['var/log', 'var/sessions', 'public/uploads']);

// Writable dirs — Deployer sets correct permissions automatically
set('writable_dirs', ['var', 'public/uploads']);

// Symfony-specific: build assets before deployment (locally)
set('bin/console', '{ {release_path} }/bin/console');

// Target server configuration
host('production')
    ->set('hostname', 'your-server.example.com')
    ->set('remote_user', 'deploy')
    ->set('deploy_path', '/var/www/my-symfony-app')
    ->set('branch', 'main');

// Custom task: warm up cache after deployment
after('deploy:vendors', 'deploy:cache:warmup');

3. Atomic Symlinks: das Herzstück des Zero-Downtime-Deployments

Der Atomic Symlink ist der technische Kern jedes Zero-Downtime-Deployments. Auf Unix-Systemen ist das Ersetzen eines Symlinks durch ln -sfn eine atomare Operation: Aus Sicht des Betriebssystems und damit aus Sicht des Webservers gibt es keinen Moment, in dem der Symlink auf kein gültiges Ziel zeigt. Der neue Release ist bis zu diesem Zeitpunkt vollständig aufgebaut — Composer-Abhängigkeiten installiert, Assets kompiliert, Cache gewärmt, Migrationen ausgeführt. Der Moment, in dem PHP-FPM den neuen Release sieht, ist exakt der Moment des Symlink-Swaps.

Deployer PHP setzt dieses Muster konsequent um: Der Task deploy:symlink führt den atomaren Swap aus und ist der einzige Task im Deployment-Flow, der den laufenden Service beeinflusst. Alle vorherigen Tasks arbeiten im neuen Release-Verzeichnis, ohne das laufende System zu berühren. Nach dem Symlink-Swap müssen noch laufende PHP-FPM-Prozesse, die noch das alte Release bearbeiten, nicht zwangsläufig abgebrochen werden — sie laufen im alten Release-Verzeichnis zu Ende, das noch auf der Festplatte existiert. Erst wenn keine Prozesse mehr das alte Verzeichnis nutzen, kann es beim nächsten Deployment-Cleanup gelöscht werden.

4. Shared Dirs und Shared Files konfigurieren

In einem Deployer PHP-Setup sind nicht alle Dateien releasespezifisch. Konfigurationsdateien wie .env.local, die server-spezifische Datenbankpasswörter und API-Keys enthalten, gehören nicht ins Repository und nicht in den Release-Ordner — sie gehören ins shared/-Verzeichnis und werden von Release zu Release verlinkt. Das gleiche gilt für Verzeichnisse wie var/log, var/sessions und Upload-Verzeichnisse: Logs sollen über Releases hinweg akkumulieren, Sessions sollen beim Deployment nicht verfallen und Uploads sollen erhalten bleiben.

Deployer PHP erstellt beim ersten Deployment die Shared-Verzeichnisse und legt Symlinks aus dem Release-Verzeichnis in das Shared-Verzeichnis an. Jedes neue Release findet dieselben Symlinks vor und sieht die gleichen freigegebenen Daten. Das bedeutet: Wenn ein Rollback ausgeführt wird, zeigen die Shared-Symlinks weiterhin auf dasselbe shared/-Verzeichnis. Logs und Uploads gehen nicht verloren, und der Datenbankzustand bleibt konsistent. Das Zero-Downtime-Deployment-Konzept ist damit vollständig: Der Code wechselt atomar, die persistenten Daten bleiben stabil.


<?php
// deploy.php — Full Symfony task flow with migrations and asset compilation
namespace Deployer;

require 'recipe/symfony.php';

set('application', 'symfony-app');
set('repository', 'git@github.com:vendor/symfony-app.git');
set('keep_releases', 5);

// Shared between all releases — never deleted
set('shared_files', ['.env.local', 'config/jwt/private.pem', 'config/jwt/public.pem']);
set('shared_dirs', ['var/log', 'var/sessions', 'public/uploads', 'public/media']);

// Directories that need write permissions for Symfony
set('writable_dirs', ['var', 'public/uploads', 'public/media']);
set('writable_mode', 'acl'); // use 'chmod' if ACL not available

// Custom deployment task sequence
task('deploy', [
    'deploy:info',          // Print deploy info (host, branch, commit)
    'deploy:setup',         // Create release directory structure
    'deploy:lock',          // Prevent concurrent deployments
    'deploy:release',       // Create new release directory
    'deploy:update_code',   // Git clone/checkout into release dir
    'deploy:shared',        // Create shared symlinks (.env.local, var/log …)
    'deploy:vendors',       // composer install --no-dev --optimize-autoloader
    'deploy:assets',        // php bin/console assets:install
    'deploy:cache:warmup',  // php bin/console cache:warmup
    'database:migrate',     // php bin/console doctrine:migrations:migrate --no-interaction
    'deploy:symlink',       // ATOMIC: switch current symlink to new release
    'deploy:unlock',        // Remove deployment lock
    'deploy:cleanup',       // Delete old releases (keep_releases)
    'deploy:success',       // Print success message
]);

// Rollback on failure — keeps deployment lock clean
after('deploy:failed', 'deploy:unlock');

5. Datenbankmigrationen sicher automatisieren

Datenbankmigrationen sind der heikelste Teil des Zero-Downtime-Deployments mit Symfony. Das Problem: Migrationen laufen, bevor der Symlink umgesetzt wird — aber der alte Code läuft noch. Wenn eine Migration eine Spalte umbenennt oder eine NOT-NULL-Spalte ohne Default-Wert hinzufügt, schlagen alle Inserts des alten Codes fehl, bis der neue Code aktiv ist. Die Lösung ist eine Migrationsstrategie, die Backward Compatibility als Designprinzip verfolgt: Jede Migration muss mit dem alten und dem neuen Code gleichzeitig funktionieren.

Backward-Compatible-Migrationen folgen einem Drei-Phasen-Muster: Phase 1 fügt neue Spalten oder Tabellen hinzu (mit DEFAULT-Wert oder als nullable), der alte Code ignoriert sie. Phase 2 deployt den neuen Code, der die neuen Spalten füllt. Phase 3 entfernt in einem späteren Deployment alte Spalten oder NOT-NULL-Constraints. Für Deployer PHP implementiert man einen eigenen Migration-Task, der vor dem Symlink-Swap läuft und bei einem Fehler automatisch abbricht. Der Rollback-Task stellt sicher, dass ein abgebrochenes Deployment nie einen inkonsistenten Datenbankzustand hinterlässt, weil der Symlink bei fehlgeschlagener Migration nie umgesetzt wird.

6. Rollback-Strategie: in Sekunden zurück zum letzten Release

Der größte operative Vorteil des Deployer PHP-Ansatzes liegt im Rollback: dep rollback production setzt den current-Symlink auf das vorletzte Release zurück. Da alle Release-Verzeichnisse vollständig aufgebaut auf der Festplatte liegen, ist das exakt dieselbe atomare Symlink-Operation wie beim Deployment — keine Dateien werden kopiert, kein Code wird neu gebaut. Der Rollback dauert so lange wie das Deployment des letzten erfolgreichen Releases im Terminal anzuzeigen ist: Sekunden, nicht Minuten.

Das Rollback-Fenster ist durch die Konfiguration keep_releases bestimmt. Mit fünf aufbewahrten Releases kann man auf jeden der letzten vier vorherigen Stände zurückrollen. In der Praxis ist das genug: Wenn ein Problem nach mehr als fünf Deployments auffällt, ist ein Hotfix die bessere Strategie als ein Rollback auf weit zurückliegenden Code. Wichtig: Datenbankmigrationen sind nicht automatisch rückgängig zu machen. Ein Rollback des Codes ohne Rollback der Migration kann zu Fehlern führen, wenn der alte Code auf neue Datenbankspalten trifft, die er nicht kennt. Backward-Compatible-Migrations aus dem vorherigen Abschnitt verhindern genau dieses Szenario.


<?php
// deploy.php — Rollback-safe migration task with health check
namespace Deployer;

require 'recipe/symfony.php';

// Custom migration task: runs before symlink, aborts on failure
task('database:migrate', function () {
    // Run migrations in --dry-run first to detect issues before applying
    run('{ {bin/php} } { {release_path} }/bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration');
})->desc('Run Doctrine migrations before symlink swap');

// Health check after deployment — abort and rollback if app responds with error
task('deploy:health_check', function () {
    $host = get('app_url'); // e.g. https://my-symfony-app.com
    $response = run("curl -s -o /dev/null -w '%{http_code}' {$host}/health");

    if ($response !== '200') {
        invoke('rollback');
        throw new \RuntimeException("Health check failed (HTTP {$response}). Rolled back automatically.");
    }
})->desc('HTTP health check after symlink swap');

// Multi-server: rolling deployment across app cluster
host('app-01')->set('hostname', 'app01.example.com')->set('deploy_path', '/var/www/app');
host('app-02')->set('hostname', 'app02.example.com')->set('deploy_path', '/var/www/app');

// Deployment sequence: migrate once on app-01, then deploy to all
task('deploy:cluster', [
    'deploy:setup',
    'deploy:release',
    'deploy:update_code',
    'deploy:shared',
    'deploy:vendors',
    'deploy:cache:warmup',
    'database:migrate',   // Run once — not per host
    'deploy:symlink',
    'deploy:cleanup',
]);

after('deploy:symlink', 'deploy:health_check');
after('deploy:failed', 'deploy:unlock');

7. Multi-Stage-Konfiguration: Staging und Production

In professionellen Symfony-Projekten gibt es mindestens zwei Umgebungen: Staging für interne Tests und Integrationsprüfungen, Production für den Livebetrieb. Deployer PHP unterstützt mehrere Hosts mit unterschiedlichen Konfigurationen in derselben deploy.php. Jeder Host bekommt seinen eigenen Pfad, seinen eigenen Branch und seine eigene Konfiguration. Mit dep deploy staging deployt man auf Staging, mit dep deploy production auf Production — dieselbe Konfigurationsdatei, verschiedene Hosts und Branches.

Eine bewährte Konvention im Zero-Downtime-Deployment-Setup: Staging deployt immer vom develop-Branch, Production deployt immer vom main-Branch. Beide Umgebungen laufen auf identischen Deployer-Tasks, aber Staging kann zusätzliche Debug-Tasks aktivieren, die in Production deaktiviert sind. Das Staging-Deployment hat ein kürzeres keep_releases-Fenster, weil Rollbacks dort seltener benötigt werden. Die Shared-Verzeichnisse sind auf Staging mit Test-Daten befüllt und werden nie mit Production synchronisiert — das ist explizit Teil der Staging-Konfiguration.

8. Deployer in GitHub Actions und GitLab CI integrieren

Deployer PHP integriert sich nahtlos in bestehende CI/CD-Pipelines. In GitHub Actions wird Deployer PHP nach dem Build- und Test-Schritt als eigener Job ausgeführt. Der SSH-Key für den Zielserver wird als GitHub Secret hinterlegt und per ssh-agent zur Pipeline hinzugefügt. Das Deployment wird nur auf dem main-Branch ausgeführt — Feature-Branches lösen keinen Deploy aus. So bleibt die Pipeline schnell, weil Tests auf allen Branches laufen, aber Deployments nur auf dem stabilen Branch.

Eine wichtige Optimierung für den CI-Kontext: Der Composer-Cache und der Node-Cache werden zwischen Pipeline-Runs gespeichert. Ohne Cache installiert Composer alle Abhängigkeiten neu, was in großen Symfony-Projekten mehrere Minuten dauert. Mit Cache-Treffern dauert die Abhängigkeitsinstallation Sekunden. Deployer PHP unterstützt lokales Bauen vor dem Deployment: Assets und CSS können im CI gebaut und dann per Rsync auf den Server übertragen werden, statt auf dem Server eine vollständige Node.js-Umgebung vorzuhalten. Das trennt Build-Umgebung und Produktionsserver sauber voneinander.


# .github/workflows/deploy.yml — GitHub Actions deployment pipeline
name: Deploy Symfony to Production

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          coverage: none
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Run tests
        run: php bin/phpunit --no-coverage

  deploy:
    needs: test  # Only deploy if tests pass
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - name: Install Deployer dependencies
        run: composer install --no-interaction --prefer-dist

      # Configure SSH key for server access
      - name: Setup SSH agent
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${ { secrets.DEPLOY_SSH_KEY } }

      # Add server to known_hosts to prevent interactive prompt
      - name: Add known host
        run: ssh-keyscan -H ${ { secrets.DEPLOY_HOST } } >> ~/.ssh/known_hosts

      # Run Deployer — dep is the CLI tool from vendor/bin
      - name: Deploy to Production
        run: vendor/bin/dep deploy production --no-interaction -vvv
        env:
          DEPLOY_HOST: ${ { secrets.DEPLOY_HOST } }

9. Deployer vs. Capistrano vs. manuelles Deployment

Der Vergleich verschiedener Deployment-Ansätze zeigt, warum Deployer PHP für Symfony-Projekte die pragmatischste Wahl ist. Capistrano, das Vorbild für den Atomic-Symlink-Ansatz, ist in Ruby geschrieben und erfordert eine Ruby-Laufzeitumgebung in der CI-Pipeline. Das ist für PHP-Teams ein unnötiger Technologie-Bruch. Manuelles Deployment per SSH ohne Automatisierung ist fehleranfällig und skaliert nicht: Jeder Entwickler führt die Schritte leicht anders aus, Dokumentation veraltet, und ein Rollback erfordert manuelles Eingreifen mit SSH-Zugriff auf den Server.

Kriterium Manuell (SSH) Capistrano Deployer PHP
Zero Downtime Nein (Dateien ersetzen) Ja (Atomic Symlink) Ja (Atomic Symlink)
Technologie Bash Ruby PHP (kein Bruch)
Rollback Manuell, fehleranfällig cap production deploy:rollback dep rollback production
Symfony-Rezept Selbst implementieren Capistrano-symfony gem recipe/symfony.php inklusive
CI/CD-Integration Bash-Skripte nötig Ruby in Pipeline nötig PHP-Pipeline — kein Extra-Setup

Die Entscheidung für Deployer PHP in Symfony-Projekten ist vor allem eine Technologie-Kohärenz-Entscheidung: Die Deployment-Konfiguration ist PHP-Code, der im Repository liegt, versioniert wird und für das gesamte Team verständlich ist. Neue Teammitglieder müssen keine Ruby- oder Bash-Deployment-Skripte verstehen — die Deployer-Konfiguration ist idiomatisches PHP mit klarer Task-Semantik. Das reduziert den Operations-Overhead und macht das Deployment zu einem selbstverständlichen Teil des Symfony-Projekts statt zu einem opaken Black-Box-Prozess.

Mironsoft

Symfony-Deployment, DevOps-Automatisierung und CI/CD-Pipelines

Symfony-Deployment ohne Ausfallzeiten aufbauen?

Wir konfigurieren Deployer PHP für euer Symfony-Projekt — von der Atomic-Symlink-Strategie über sichere Migrations-Automatisierung bis zur vollständigen GitHub-Actions- oder GitLab-CI-Integration.

Deployment-Setup

Deployer PHP konfigurieren, Shared-Dirs einrichten und Zero-Downtime-Flow testen

CI/CD-Integration

GitHub Actions oder GitLab CI mit Deployer verbinden — inkl. SSH-Key-Management

Migrations-Strategie

Backward-Compatible-Migrations einführen und Rollback-Szenario absichern

10. Zusammenfassung

Zero-Downtime-Deployment mit Deployer PHP und Symfony ist kein kompliziertes Setup — es ist ein strukturierter Prozess, der das Deployment von einem riskanten manuellen Schritt zu einem zuverlässigen, wiederholbaren Vorgang macht. Atomic Symlinks stellen sicher, dass kein Request auf halbfertigem Code landet. Shared Dirs sichern Logs, Sessions und Uploads über Releases hinweg. Die Backward-Compatible-Migrationsstrategie entkoppelt Datenbankänderungen vom Code-Deployment. Rollbacks dauern Sekunden, nicht Stunden.

Der operationelle Gewinn ist sofort spürbar: Deployments können zu jeder Tageszeit ohne Wartungsfenster ausgeführt werden, weil kein Nutzer eine Downtime sieht. Das Team kann häufiger deployen, weil das Risiko pro Deployment minimal ist. Und wenn doch ein Problem auftritt, ist das Rollback ein einziger Befehl mit garantiert vorhersehbarem Ergebnis. Deployer PHP macht das Symfony-Deployment zu dem, was es sein sollte: ein langweiliger, zuverlässiger Routinevorgang.

Symfony Zero-Downtime-Deployment — Das Wichtigste auf einen Blick

Atomic Symlink

Der Kern des Zero-Downtime-Deployments. Der neue Release ist vollständig bereit, bevor der Symlink umgesetzt wird — atomar, kein halbfertiger Stand.

Rollback in Sekunden

dep rollback production setzt den Symlink auf das vorherige Release zurück. Keine Dateien kopieren, kein Rebuild nötig — Sekunden statt Minuten.

Backward-Compatible Migrations

Migrationen in Drei-Phasen aufteilen: hinzufügen, deployen, aufräumen. So funktioniert alter und neuer Code gleichzeitig mit der Datenbank.

CI/CD-Integration

Deployer PHP läuft nativ in PHP-Pipelines. GitHub Actions und GitLab CI brauchen nur SSH-Key und vendor/bin/dep deploy production.

11. FAQ: Symfony Zero-Downtime-Deployment mit Deployer PHP

1Was ist Zero-Downtime-Deployment?
Neue Version aktivieren, ohne dass Nutzer eine Unterbrechung sehen. Der atomare Symlink-Swap stellt sicher, dass kein Request auf halbfertigem Code landet.
2Wie funktioniert der Atomic Symlink?
Deployer baut den neuen Release vollständig auf, dann setzt current per ln -sfn auf das neue Verzeichnis. Atomar — kein Moment ohne gültiges Ziel.
3Rollback durchführen?
dep rollback production — setzt current auf das vorherige Release. Keine Dateien kopieren, kein Rebuild. Dauert Sekunden.
4Datenbank beim Rollback?
Migrationen werden nicht zurückgerollt. Backward-Compatible-Migrations in drei Phasen verhindern Fehler: hinzufügen, deployen, aufräumen.
5Was sind Shared Dirs?
Verzeichnisse, die über alle Releases geteilt werden: var/log, var/sessions, public/uploads. Deployer legt Symlinks aus dem Release in das shared/-Verzeichnis.
6Mehrere Server?
Mehrere Hosts in deploy.php definieren. Parallel oder sequenziell deployen. Migrationen nur einmal laufen lassen, Symlink-Swap auf allen Hosts parallel.
7GitHub Actions Integration?
SSH-Key als Secret, webfactory/ssh-agent hinzufügen, vendor/bin/dep deploy production ausführen. Nur auf main-Branch deployen.
8Wie viele Releases aufbewahren?
5 Releases sind ideal: 4 Rollback-Punkte ohne zu viel Speicherplatz. set('keep_releases', 5) in deploy.php.
9Deployer PHP vs. Capistrano?
Beide nutzen Atomic Symlinks. Capistrano braucht Ruby in der Pipeline. Deployer PHP läuft nativ in PHP-Pipelines mit fertigem Symfony-Rezept.
10PHP-FPM nach Deployment neustarten?
Reload (nicht Restart) nach Symlink-Swap empfohlen. OPcache invalidieren, laufende Requests noch mit altem Code abschließen, neue mit neuem Code bedienen.