EDEvangelos Dimitriadis
  • Über mich
  • Blog
  • Leistungen
  • Projekte
  • OpenSource
  • Uses
  • Kontakt
← Zurück zum Blog

Zero-Downtime-Deploys für Laravel auf ECS

ED
Evangelos Dimitriadis
29. Juni 2026
Zero-Downtime-Deploys für Laravel auf ECS

Foto von Paul Krüger auf Unsplash

Ein Deploy dauert vielleicht fünfzehn Sekunden. In diesen fünfzehn Sekunden kann jeder Request, der gerade läuft, sterben, jeder Job, der gerade verarbeitet wird, abbrechen, und jeder Nutzer, der gerade eine Seite lädt, einen Fehler sehen. Zero-Downtime heißt nicht, dass nichts passiert. Es heißt, dass der Nutzer nichts davon merkt.

ECS macht das Rolling-Update einfach. Ein Häkchen, neue Tasks ersetzen alte, fertig. Aber Zero-Downtime ist mehr als dieses Häkchen. Die eigentliche Arbeit liegt in den Migrationen, in den Requests und Jobs, die beim Deploy gerade unterwegs sind, und in der Frage, was passiert, wenn der neue Code kaputt ist. Dieser Artikel baut auf dem Fargate-Setup auf und nutzt die Alarme aus dem Observability-Artikel fürs automatische Rollback.

Versionsstand ist Laravel 13, Stand Juni 2026, Region eu-central-1 als Referenz.

Was Zero-Downtime tatsächlich bedeutet

Drei Garantien stecken hinter dem Wort: kein abgebrochener Request, kein verlorener Job, kein gebrochenes Frontend. Schwierig wird es, weil alle drei in demselben kurzen Moment auf die Probe gestellt werden.

Dieser Moment ist das Rolling-Fenster. Für ein paar Sekunden bis Minuten laufen alter und neuer Code gleichzeitig, gegen dieselbe Datenbank, hinter demselben Load Balancer. Alles, was in diesem Fenster bricht, ist ein Incident. Genau hier scheitern die meisten Deploys, die "eigentlich Zero-Downtime" sein sollten.

Die mentale Verschiebung, die alles einfacher macht: Die Frage ist nicht "wie deploye ich neuen Code", sondern "wie sorge ich dafür, dass alter und neuer Code für einen Moment friedlich nebeneinander laufen". Wer so denkt, löst die richtigen Probleme.

Die ECS-Rolling-Mechanik

ECS ersetzt beim Rolling-Update die alten Tasks schrittweise durch neue. Zwei Parameter steuern, wie. minimumHealthyPercent legt fest, wie viel Kapazität mindestens gesund bleiben muss, maximumPercent, wie weit über die Soll-Kapazität ECS während des Deploys hochfahren darf. 100 und 200 bedeutet: erst alle neuen Tasks hochfahren, dann die alten runter. Volle Kapazität die ganze Zeit, dafür kurzzeitig doppelte Kosten.

Damit das funktioniert, muss ECS wissen, wann ein neuer Task wirklich bereit ist. Das ist der Health-Check. Der /up-Endpoint ist der natürliche ALB-Target-Group-Check, und er braucht eine ausreichende startPeriod, damit der Task booten kann, bevor der Check zu zählen beginnt. Ein zu kurzes Fenster, und ECS routet Traffic auf einen Task, der noch hochfährt.

Am anderen Ende steht die Deregistration Delay, auch Connection Draining genannt. Bevor ein alter Task stirbt, nimmt ihn der Load Balancer aus der Rotation und lässt die laufenden Requests auslaufen. Der Default liegt bei 300 Sekunden, was für die meisten Web-Apps zu lang ist und oft auf 30 bis 60 Sekunden gesenkt wird.

Und falls etwas schiefgeht, greift der Deployment Circuit Breaker. Kommt ein neuer Task nach einer Schwelle von Versuchen nicht hoch, gilt der Deploy als gescheitert und rollt automatisch zurück:

{
  "deploymentConfiguration": {
    "minimumHealthyPercent": 100,
    "maximumPercent": 200,
    "deploymentCircuitBreaker": {
      "enable": true,
      "rollback": true
    }
  }
}

Das Migrations-Problem

Hier liegt die härteste Stelle. Im Rolling-Fenster muss das Datenbankschema zu beidem passen, dem alten und dem neuen Code. Eine Migration, die das Schema so ändert, dass der alte Code bricht, nimmt die ganze App herunter, bevor der neue Code überhaupt überall läuft.

Die goldene Regel der rückwärtskompatiblen Migration

Jede Migration muss durchlaufen, während der alte Code noch läuft, und ihn nicht brechen. Das bedeutet additiv, nie destruktiv im selben Deploy. Eine Spalte hinzufügen ist sicher, der alte Code ignoriert sie einfach. Eine Spalte umbenennen oder löschen ist gefährlich, weil der alte Code sie noch erwartet.

Und die Migration läuft als einmaliger migrate --force RunTask vor dem Service-Update, nicht im Container-Entrypoint. Wer migrate in den Entrypoint legt, hat bei N neuen Tasks N gleichzeitige Migrationen und damit eine Race Condition, wie im Fargate-Artikel beschrieben.

Expand-Contract am Beispiel

Nehmen wir den Klassiker, der naiv Downtime garantiert: eine Spalte umbenennen, sagen wir name zu full_name. In einem Schritt bricht das jeden alten Task, der noch name liest. Das Muster, das es löst, heißt Expand-Contract oder Parallel Change, und es zerlegt die Änderung in vier Schritte über mehrere Deploys.

Zuerst die Expand-Phase, die die neue Spalte additiv hinzufügt:

// Deploy 1: expand
return new class extends Migration {
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('full_name')->nullable();
        });
    }
};

Dann deployt man Code, der in beide Spalten schreibt und aus der neuen liest, mit Fallback auf die alte. Danach ein Backfill, der die alten Werte in die neue Spalte umzieht. Und erst in einem späteren, separaten Deploy folgt die Contract-Phase, wenn kein laufender Code mehr die alte Spalte braucht:

// Deploy 2 (später): contract
return new class extends Migration {
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('name');
        });
    }
};

Vier Schritte statt einem, und keine Sekunde Ausfall. Das ist mehr Arbeit als eine einzelne Migration, aber es ist der Unterschied zwischen einem ruhigen Release und einem Incident.

In-Flight-Requests und Jobs

Beim Web-Service erledigt die Deregistration Delay die Arbeit. Der Load Balancer schickt keine neuen Requests mehr an den sterbenden Task und lässt die offenen auslaufen. FPM beendet sie sauber, Octane schließt die in-flight Requests beim Reload ab.

Bei den Workern ist es der Graceful Shutdown. ECS schickt beim Stoppen ein SIGTERM, und nach stopTimeout ein SIGKILL. queue:work beendet den gerade laufenden Job, bevor es stoppt, und Horizon den Master sauber. Damit das klappt, muss stopTimeout über der typischen Job-Laufzeit liegen. Der Default von 30 Sekunden reicht nicht, wenn ein Job länger braucht. Die Details stehen im Fargate-Artikel.

Über allem steht die Idempotenz. Weil ein Job bei hartem Abbruch erneut laufen kann, müssen Jobs idempotent sein. Das ist keine theoretische Vorsicht, sondern bei jedem Deploy relevant, denn jeder Deploy ist ein potenzieller Abbruchpunkt.

Frontend, Sessions und Cache

Das Frontend hat ein eigenes Rolling-Problem: Ein Nutzer, der vor dem Deploy eine Seite geladen hat, hält HTML im Browser, das auf die alten Assets verweist. Nach dem Deploy darf dieses HTML nicht ins Leere zeigen. Die Lösung sind versionierte Assets. Das Vite-Manifest gibt jedem Build gehashte Dateinamen, sodass alte HTML ihre alten Assets weiter lädt und neue HTML die neuen. Beide Versionen koexistieren, nichts bricht.

Sessions und Cache haben dasselbe Prinzip im Hintergrund. Sessions gehören in einen geteilten Store, also Redis oder die Datenbank, nicht ins lokale Dateisystem eines Tasks. Sonst verliert ein Nutzer beim Wechsel auf einen neuen Task seine Session, weil die auf dem alten Task lag. Dasselbe gilt für den Cache. Lokaler Cache pro Task führt zu inkonsistentem Verhalten, solange alte und neue Tasks parallel laufen. Und config:cache gehört in den Image-Build, damit kein Task beim Start die Config neu kompiliert.

Octane: nicht doppeln

Hier machen Teams mit Octane einen verbreiteten Fehler. Octane mit FrankenPHP, Swoole oder RoadRunner bringt seinen eigenen Graceful Reload mit. Ein Reload-Signal lässt die persistenten Worker ihre laufenden Requests beenden und mit dem neuen Code neu starten. Das ist bereits ein Zero-Downtime-Mechanismus.

Die Konsequenz: Man darf die Mechanismen nicht doppeln. Wer auf ECS rollend deployt und Octane sauber neu starten lässt, hat genug. Ein zusätzlicher Zero-Downtime-Layer, wie ihn manche Tools für klassisches FPM über Symlink-Switches bauen, stört Octanes eigenen Reload und führt zu seltsamem Verhalten. Auf ECS ersetzt das Rolling-Update plus Octane-Reload genau das, was auf einem klassischen Server der Symlink-Switch war. Mehr braucht es nicht, und mehr schadet.

Rollback, wenn der neue Code kaputt ist

Der beste Deploy-Prozess hilft nicht, wenn der neue Code fehlerhaft ist und niemand zurück kann. ECS bringt das Rollback in zwei Stufen eingebaut.

Deployment Circuit Breaker

Der Circuit Breaker fängt den offensichtlichen Fall: ein Task, der gar nicht erst hochkommt, etwa weil eine Umgebungsvariable fehlt oder der Container sofort crasht. Nach einer Schwelle von Versuchen markiert ECS den Deploy als gescheitert und rollt automatisch auf die letzte funktionierende Task-Definition zurück. Das ist der Schutz gegen den kaputten Build.

Alarm-gestütztes Rollback

Der subtilere Fall ist gefährlicher: Der Task kommt hoch, der Health-Check ist grün, aber die Fehlerrate steigt, oder die Latenz explodiert. Der Circuit Breaker sieht das nicht, weil der Task technisch läuft. Hier greifen CloudWatch-Alarme auf echten Business-Metriken, nicht nur CPU und RAM. Application Signals liefert die Latenz- und Fehlerraten, auf die der Alarm zeigt:

resource "aws_cloudwatch_metric_alarm" "deploy_error_rate" {
  alarm_name          = "laravel-deploy-error-rate"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "5xxErrorRate"
  namespace           = "ECS/ApplicationSignals"
  period              = 60
  statistic           = "Average"
  threshold           = 5
  alarm_description    = "Loest Deploy-Rollback bei erhoehter Fehlerrate aus"
}

Mit Native Blue/Green bleibt nach dem Traffic-Switch die alte Version eine konfigurierbare Bake-Time lang lauffähig. Schlägt in dieser Zeit ein Alarm an, ist das Rollback sofort, weil die alte Flotte noch steht.

Rolling, Native Blue/Green oder CodeDeploy

ECS bietet 2026 drei Wege, und die Wahl hängt davon ab, wie viel Kontrolle man über den Traffic-Shift braucht.

Das Rolling Update ist der einfache Default. Neue Tasks ersetzen alte schrittweise. Für die meisten Setups reicht das vollkommen, solange die Migrationen rückwärtskompatibel sind.

Native Blue/Green ist 2026 generell verfügbar und für die meisten Teams der empfohlene Weg. Eine komplette grüne Flotte fährt parallel hoch, der Traffic schaltet auf einmal um, danach läuft die Bake-Time mit sofortigem Rollback bei Alarm, dazu kommen Lifecycle Hooks. Das alles ohne externes Tooling, nativ in ECS.

CodeDeploy braucht man nur noch für feinkörnige Traffic-Shifts, also Canary, bei dem erst ein kleiner Prozentsatz den neuen Code bekommt, oder Linear, bei dem der Traffic in Schritten umzieht. Mehr Kontrolle, mehr Komplexität.

BedarfWahl
Standard-Deploy, rückwärtskompatible MigrationenRolling Update
Sauberer Cut mit Bake-Time und schnellem RollbackNative Blue/Green
Schrittweiser Traffic-Shift, Canary oder LinearCodeDeploy
Risikoreiches Release, das man genau beobachten willNative Blue/Green plus Business-Alarme

Die vollständige Deploy-Sequenz

Die Reihenfolge hält alles zusammen, und sie ist nicht verhandelbar:

  1. Image bauen, mit config:cache und versionierten Assets
  2. Nach ECR pushen
  3. migrate --force als RunTask, nur additive Migrationen
  4. Services rollend oder per Blue/Green auf das neue Image aktualisieren
  5. Bake-Time und Alarme beobachten, bei Bedarf greift das automatische Rollback
  6. Aufräum-Migrationen, also die Contract-Phase, erst in einem späteren, separaten Deploy

Schritt drei vor Schritt vier ist der Kern: Die Migration läuft, bevor der neue Code kommt, und weil sie additiv ist, stört sie den alten Code nicht. Schritt sechs getrennt von Schritt drei ist die zweite Hälfte: Aufgeräumt wird erst, wenn garantiert kein Code mehr die alte Struktur braucht. Die Mechanik der Pipeline selbst steht im CI/CD-Artikel.

Anti-Patterns

Destruktive Migration im selben Deploy wie der Code. Bricht den alten Code im Rolling-Fenster. Umbenennen und Löschen gehören in die Contract-Phase eines späteren Deploys.

migrate im Container-Entrypoint. Race über N Tasks. Gehört in einen einmaligen RunTask vor dem Service-Update.

Kein Health-Check oder zu kurze startPeriod. ECS routet Traffic auf einen Task, der noch bootet, und der Nutzer sieht den Fehler.

Keine Deregistration Delay. Laufende Requests werden hart abgeschnitten, wenn der alte Task stirbt.

Lokale Sessions oder lokaler Cache. Der Nutzer verliert seine Session beim Wechsel auf einen neuen Task. Beides gehört in einen geteilten Store.

Kein Rollback-Plan. Wenn der neue Code kaputt ist, fehlt der Weg zurück. Circuit Breaker und Alarm-Rollback kosten fast nichts und retten den schlechten Tag.

Zero-Downtime-Layer doppeln bei Octane. Octane macht seinen eigenen Graceful Reload. Ein zusätzlicher Layer stört ihn.

Alle Migrationen auf einmal, inklusive Aufräumen. Expand und Contract gehören in getrennte Deploys, sonst ist die Rückwärtskompatibilität dahin.

Fazit

Zero-Downtime auf ECS ist kein einzelnes Feature, sondern das Zusammenspiel von rückwärtskompatiblen Migrationen, Connection Draining, Graceful Shutdown und einem Rollback, das automatisch greift. Jedes Stück für sich ist überschaubar, und zusammen ergeben sie einen Deploy, den der Nutzer nicht bemerkt.

Die härteste Disziplin ist Expand-Contract. Wer Migrationen so plant, dass alter und neuer Code für einen Moment friedlich nebeneinander laufen, hat das Kernproblem gelöst. Den Rest nimmt einem ECS weitgehend ab. Mit Native Blue/Green und dem Circuit Breaker ist das Rollback 2026 eingebaut, und die Aufgabe des Teams reduziert sich darauf, die richtigen Alarme zu setzen und die Migrationen sauber zu schneiden.


Deployen Sie auf ECS, aber ein Release fühlt sich jedes Mal nach angehaltenem Atem an? Kontaktieren Sie mich für einen Deploy-Audit, der Migrationen, Health-Checks, Draining und Rollback-Strategie in wenigen Tagen prüft und absichert.

An einem IaC-Kit für Production-Laravel auf AWS arbeite ich gerade, inklusive Deploy-Pipeline mit Expand-Contract-Disziplin und Alarm-gestütztem Rollback. Wer früh Bescheid wissen will, kann sich unverbindlich auf die Liste setzen lassen. Kein Newsletter-Dauerfeuer, nur eine Nachricht, wenn es so weit ist.

Über michBlogProjekteKontaktImpressumDatenschutzerklärung

Made in Gerlingen, 2026