Kamal 2.x in Produktion: Zero-Downtime-Deploys, Secrets und wo der einfache Weg endet

#kamal 2 in produktion
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Wenn Du den früheren Beitrag zu Kamal vs Kubernetes gelesen hast, weißt Du bereits: Kamal verdient seinen Platz, wenn Du containerbasierte Deployments willst, ohne den operativen Overhead einer vollen Orchestrierungsplattform. Dieser Vergleich beantwortet die strategische Frage. Dieser Beitrag beantwortet die mechanische: Was braucht es tatsächlich, um Kamal 2.x zuverlässig in Produktion zu betreiben?

Die kurze Antwort: weniger, als Du vielleicht erwartest, aber mehr, als das README vermuten lässt. Kamal 2.x brachte spürbare Änderungen daran, wie Secrets, Accessories und die Proxy-Schicht behandelt werden. Einige Gewohnheiten aus Kamal 1.x gelten nicht mehr. Dieser Guide arbeitet sich durch Zero-Downtime Rolling Deploys, Secrets-Management ohne Vault-Cluster, einen Symfony-spezifischen Deploy-Flow mit Doctrine-Migrationen und die Proxy-Konfiguration, die das Ganze zusammenhält. Am Ende benennt er die Decke - den Punkt, an dem Kamal nicht mehr die ehrliche Antwort ist.

Was sich zwischen Kamal 1.x und Kamal 2.x geändert hat

Kamal 2.x hat den Traefik-basierten Proxy durch kamal-proxy ersetzt, einen eigens dafür gebauten HTTP-Proxy, den das 37signals-Team pflegt. Das ist keine kosmetische Änderung. Der alte traefik-Konfigurationsblock in Deiner deploy.yml ist weg und wurde durch einen proxy-Block mit anderer Semantik ersetzt.

Auch das Accessories-Modell wurde überarbeitet. In Kamal 1.x waren Accessories eher zweitklassige Bürger - sie liefen auf den Ziel-Hosts, waren aber nicht so eng in den Deploy-Lebenszyklus integriert. In 2.x haben Accessories sauberere Lifecycle-Befehle (kamal accessory boot, kamal accessory reboot) und ihre Konfiguration wird beim Deploy strikter validiert.

Die andere wesentliche Änderung: Kamal 2.x setzt stärker auf .kamal/secrets als kanonische Secrets-Datei, mit Anbindung an kamal-secrets zum Abrufen von Werten aus externen Stores. Das ersetzt das ältere Muster, Umgebungsvariablen direkt zu injizieren oder sich auf .env-Dateien zu verlassen - beides Wege, auf denen leicht etwas schiefging.

Zero-Downtime Rolling Deploys konfigurieren

Zero-Downtime hängt in Kamal 2.x von zwei Dingen ab, die zusammenspielen müssen: einem Health-Check-Endpoint, den kamal-proxy abfragen kann, und einem stop_wait_seconds-Wert, der Deiner App Zeit gibt, laufende Requests abzuschließen, bevor der Container beendet wird.

Ein minimaler, aber produktionstauglicher proxy-Block in der deploy.yml sieht so aus:

proxy:
  ssl: true
  host: your-app.example.com
  app_port: 8080
  healthcheck:
    path: /up
    interval: 3
    threshold: 5

Das threshold bedeutet hier: kamal-proxy wartet auf fünf aufeinanderfolgende erfolgreiche Antworten, bevor er Traffic an den neuen Container leitet. Bei einem Drei-Sekunden-Intervall sind das etwa 15 Sekunden Aufwärmzeit - genug für eine Symfony-App, um beim ersten Request ihren DI-Container zu kompilieren und den Opcode-Cache aufzuwärmen.

Auf der Drain-Seite setzt Du stop_wait_seconds in Deiner Service-Konfiguration:

servers:
  web:
    hosts:
      - 203.0.113.10
    options:
      stop-wait-seconds: 15

Das sagt Docker, nach dem SIGTERM 15 Sekunden zu warten, bevor SIGKILL folgt. Kombiniert mit dem Health-Check-Threshold bekommst Du ein Fenster, in dem der alte Container seine laufenden Requests beendet, während der neue hochfährt - und kamal-proxy schaltet den Traffic erst um, wenn der neue Container nachweislich gesund ist.

Ein praktisches Detail: Dein /up-Endpoint sollte eine Nicht-200-Antwort liefern, wenn die Anwendung noch nicht bereit ist, echten Traffic zu bedienen - etwa wenn ein Cache-Warmup noch läuft oder ein benötigter Upstream-Service nicht erreichbar ist. Bedingungslos 200 zurückzugeben hebelt den Health Check aus.

Secrets-Management ohne Vault-Cluster

Kamal 2.x bringt das Konzept einer .kamal/secrets-Datei mit sowie ein kamal-secrets-Binary, das Werte aus 1Password, AWS Secrets Manager oder einfachen Umgebungsvariablen holen kann. Für Teams, die keinen dedizierten Secrets-Management-Cluster betreiben, ist der env-Adapter kombiniert mit einer nie ins Versionskontrollsystem eingecheckten Secrets-Datei die pragmatische Basis.

Deine .kamal/secrets-Datei (in der .gitignore) könnte so aussehen:

DATABASE_URL=$(cat ~/.secrets/database-url)
APP_SECRET=$(cat ~/.secrets/app-secret)
SENTRY_DSN=$(op read "op://Production/Sentry/dsn")

Die $(...)-Syntax bedeutet: Jede Zeile wird beim Deploy auf der Maschine ausgewertet, die kamal deploy ausführt. Der Wert wird dem Container als Umgebungsvariable übergeben. Bei Symfony-Anwendungen sind DATABASE_URL und APP_SECRET die typischen Secrets, die über diesen Mechanismus fließen sollten, statt in Images eingebacken zu werden.

Für Teams, die bereits 1Password nutzen, liefert die op://-Syntax saubere Audit-Trails und Rotation, ohne zusätzliche Infrastruktur aufzubauen. Für AWS-Umgebungen unterstützt kamal-secrets aws-secrets-manager://-Referenzen. Der Punkt ist: Keiner dieser Wege braucht einen Vault-Cluster - die Deploy-Maschine holt das Secret zur Deploy-Zeit und injiziert es in die Container-Umgebung.

Eines solltest Du vermeiden: Lege keine Secrets in die deploy.yml selbst, auch nicht im env.clear-Block. Die deploy.yml gehört in die Versionskontrolle. Alles, was in env.secret steht, wird aus .kamal/secrets geholt und bleibt außerhalb des Repositories - das ist die richtige Grenze.

Eine Symfony-App mit Doctrine-Migrationen deployen

Symfony bringt eine Komplikation mit, die generische Kamal-Dokumentation überspringt: Du willst doctrine:migrations:migrate typischerweise ausführen, bevor neue Container Traffic bedienen, nicht danach. Migrationen nach dem Deploy laufen zu lassen bedeutet ein Zeitfenster, in dem neuer Code gegen ein altes Schema läuft - das bricht bei jeder Migration, die eine Spalte entfernt oder umbenennt.

Kamal-2.x-Hooks lösen das sauber. Lege eine Datei unter .kamal/hooks/pre-deploy an:

#!/usr/bin/env bash
set -e

echo "Running Doctrine migrations..."
kamal app exec --reuse --interactive -- php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration

Mache sie mit chmod +x .kamal/hooks/pre-deploy ausführbar. Dieser Hook läuft auf der Deploy-Maschine, führt den Migrationsbefehl in einem laufenden Container auf dem Ziel-Host aus und blockiert den Deploy, wenn die Migration fehlschlägt. Das --reuse-Flag sorgt dafür, dass ein bereits laufender Container genutzt wird statt ein temporärer - die Migration läuft damit in derselben Umgebung wie Deine Anwendung.

Damit der Hook zuverlässig funktioniert, müssen Deine Migrationen rückwärtskompatibel zur vorherigen Version der Anwendung sein. Das heißt: Spalten zuerst als nullable hinzufügen, bevor sie verpflichtend werden, und niemals eine Spalte in derselben Migration entfernen, die auch den referenzierenden Code entfernt. Das ist keine Kamal-Einschränkung - es ist eine Anforderung jeder Zero-Downtime-Deployment-Strategie.

Der post-deploy-Hook ist der richtige Ort für Cache-Warming oder Suchindex-Updates, die laufen können, nachdem der Traffic bereits umgeschaltet wurde:

#!/usr/bin/env bash
set -e
kamal app exec --reuse -- php bin/console cache:warmup --env=prod

Die Proxy-Konfiguration: kamal-proxy in der Praxis

kamal-proxy übernimmt die TLS-Terminierung via Let's Encrypt automatisch, sobald Du ssl: true im Proxy-Block setzt, managt den rollierenden Wechsel zwischen altem und neuem Container und läuft als eigener Docker-Container, den Kamal selbst verwaltet. Für die meisten Deployments braucht er keine Konfiguration über das oben Gezeigte hinaus.

Manche Teams greifen stattdessen zu Caddy oder Nginx - meist weil sie einen davon ohnehin für andere Services auf demselben Host betreiben oder komplexere Routing-Regeln brauchen (mehrere Anwendungen hinter derselben Domain, eigenes Header-Rewriting). Wenn Du diesen Weg gehst, kannst Du Kamal mit proxy: false anweisen, seinen verwalteten Proxy zu überspringen, und Deinen eigenen Reverse Proxy auf den Port zeigen lassen, den Dein Anwendungscontainer exponiert.

Der Preis dafür: Du verlierst den automatischen, Health-Check-gesteuerten Cutover auf Proxy-Ebene. Kamal startet den neuen Container weiterhin und stoppt den alten in Sequenz, aber der Übergang auf Netzwerkebene liegt dann in der Verantwortung Deines Reverse Proxys. Für die meisten Anwendungen ist kamal-proxy die einfachere Wahl und die mit weniger laufendem Wartungsaufwand.

Deploy-Timing: Was Du erwarten kannst

Auf einem einzelnen VPS mit 2 vCPUs und einer moderaten Symfony-Anwendung (rund 40 MB Docker-Image, warmer Opcode-Cache) sieht ein typischer kamal deploy so aus:

PhaseDauer
Image bauen und pushen45-90s
Image auf Ziel-Host ziehen10-20s
Pre-Deploy-Hook (Migrationen)5-15s
Neuen Container starten, Health-Check-Threshold15-20s
kamal-proxy-Cutover< 1s
Alten Container stoppen (Drain-Fenster)15s
Gesamt~90-160s

Der längste Posten ist fast immer die Image-Build-Zeit. Die Dockerfile-Layer-Reihenfolge auf maximale Cache-Treffer zu optimieren - Systemabhängigkeiten und Composer-Pakete installieren, bevor der Anwendungscode kopiert wird - ist die wirksamste verfügbare Verbesserung. Ein gut strukturiertes Dockerfile kann die Build-Zeit bei warmem Cache von 90 Sekunden auf unter 10 Sekunden drücken.

Wo Kamal nicht mehr die richtige Antwort ist

Kamal ist ehrlich über seinen Geltungsbereich, und das solltest Du auch sein. Die folgenden Szenarien sind legitime Gründe, zu Kubernetes oder einer anderen Orchestrierungsplattform zu wechseln.

Multi-Region-Deployments mit intelligentem Routing. Kamal kennt Hosts, keine Regionen. Wenn Du Traffic zum nächstgelegenen geografischen Cluster routen willst, latenzbasiertes Failover oder Active-Active über Rechenzentren hinweg brauchst, benötigst Du eine Plattform mit Topologie-Bewusstsein.

GPU-Workloads. Kamal hat kein Konzept für Ressourcenanforderungen über das hinaus, was Docker selbst bietet. Wenn Deine Anwendung Inferenz oder Training macht, brauchst Du einen Orchestrator, der Container auf Nodes mit spezifischer Hardware einplanen kann.

Verteilte zustandsbehaftete Systeme. Eine Datenbank als Kamal-Accessory zu betreiben funktioniert auf einem einzelnen Host gut. Verteilte zustandsbehaftete Systeme - geclusterte Datenbanken, verteilte Caches mit Co-Location-Anforderungen - brauchen Persistent-Volume-Management und Scheduling-Primitive, die Kamal nicht bietet.

Große Engineering-Teams mit Self-Service-Bedarf. Wenn 50 Entwickler unabhängig voneinander eigene Services deployen, wirst Du irgendwann eine Plattformschicht mit Quota-Management, Leitplanken und einer UI wollen, die keinen direkten Serverzugriff voraussetzt. Kamal ist ein CLI-Werkzeug für Teams, die diesen Zugriff haben.

Für ein fokussiertes SaaS-Produkt oder ein Beratungsprojekt mit Deployment auf dedizierte Infrastruktur trifft keine dieser Einschränkungen zu. Kamal ist das richtige Werkzeug, und der Griff zu Kubernetes als Absicherung gegen imaginäre zukünftige Skalierung ist genau die Art von Tech-Stack-Komplexität, die die Auslieferung verlangsamt, ohne Dich vor irgendetwas Realem zu schützen.

Die Konfiguration richtig hinbekommen

Die Muster oben sind nicht exotisch - sie zeigen, wie ein produktives Kamal-2.x-Setup tatsächlich aussieht, nachdem man die rauen Kanten durchgearbeitet hat. Der Proxy-Block, die Secrets-Datei, der Pre-Deploy-Migration-Hook und das Stop-Wait-Fenster decken das meiste ab, was einen fragilen Deploy-Prozess von einem zuverlässigen trennt.

Wenn Du Kamal für ein neues Symfony-Projekt evaluierst oder von einem manuellen Deploy-Prozess migrierst, ist das Setup machbar. Betreibst Du aber Anwendungen mit komplexem Migrationsverhalten, Legacy-Codebasen mit unvorhersehbaren Nebenwirkungen beim Deploy oder Kundenumgebungen, in denen ein fehlgeschlagener Deploy echte Konsequenzen hat, lohnt sich ein Review von jemandem, der das in Produktion betrieben hat.

Melde Dich unter hello@wolf-tech.io oder über wolf-tech.io.