TechnologieSoftware

Zero-Downtime-Deployments für Multi-Tenant-Hosting mit Coolify

netcomputing.de Redaktion26. August 202612 Min. Lesezeit

Zusammenfassung

Agenturen, die mehrere Next.js-Websites auf einem Docker-Host mit Coolify betreiben, riskieren pro Deployment einen Ausfall: Ein Container-Neustart kappt Verbindungen, bevor die neue Version antworten kann. Ein Docker-HEALTHCHECK meldet Traefik erst dann eine neue Version als bereit, wenn sie Anfragen beantwortet, und Coolify tauscht den Container danach aus. Ein Test zeigt den Unterschied: ohne Healthcheck bis zu 23 Sekunden Ausfall, mit sauberer Einstellung nur 0,1 bis 0,2 Sekunden.

Systemvoraussetzungen

KomponenteGetestete Mindestversion
Docker Engine25.0
Docker Composev5.4.0 (Compose-Spec, kein version-Feld mehr nötig)
Coolifyv4.1.2
Traefik (in Coolify integriert)v3.7.11
Host-BetriebssystemUbuntu 22.04 LTS oder Debian 12, 64-Bit
Next.js-Anwendungeigener Healthcheck-Endpunkt, etwa /api/health

Die Tabelle listet die Mindestversionen für den in diesem Text beschriebenen Aufbau: mehrere Kunden-Websites auf Next.js, gehostet über Coolify, eine quelloffene, selbst gehostete PaaS (Platform as a Service, eine Software, die Deployments, Netzwerk und Reverse Proxy für mehrere Anwendungen gemeinsam übernimmt). Coolify setzt intern auf Docker als Container-Laufzeit und bringt Traefik als Reverse Proxy mit, also die Komponente, die eingehende Anfragen an den passenden Container weiterleitet. Für den eigenen Betrieb reicht Coolify Docker Engine 24, für die in diesem Artikel gezeigte Option start-interval beim Healthcheck wird jedoch Engine 25 oder neuer gebraucht. Wer eine ältere Engine-Version einsetzt, verliert nur diese eine Feineinstellung, die drei anderen Healthcheck-Parameter funktionieren unverändert.

Systemarchitektur

Eine Agentur, die vierzig Next.js-Websites auf einem einzigen Docker-Host betreibt, teilt sich für alle vierzig denselben Traefik-Prozess und dieselbe Coolify-Instanz. Ein Deployment für eine einzelne Kundin läuft deshalb nie isoliert. Es passiert auf derselben Maschine, über denselben Reverse Proxy, direkt neben den laufenden Containern aller anderen Mandanten. Genau deshalb kostet ein missglückter Rollout nicht nur die eine betroffene Website, sondern das Vertrauen in die gesamte Hosting-Praxis der Agentur: Eine fünfzehn Sekunden lange Fehlerseite bei einer einzigen Kundin reicht für ein Support-Ticket und eine unangenehme Nachfrage zum Hosting-Vertrag, oft noch am selben Nachmittag.

flowchart TD
  Anfrage[Anfrage an tenant.example.com]
  Proxy[Traefik Reverse Proxy]
  AltC[Container Version A, aktuell healthy]
  Trigger[Deployment wird in Coolify ausgeloest]
  NeuC[Container Version B startet im selben Netzwerk]
  Schleife[Dockers Healthcheck-Schleife prueft Version B]
  Starting[Zustand starting waehrend der Anlaufzeit]
  Unhealthy[Zustand unhealthy nach zu vielen Fehlversuchen]
  Healthy[Zustand healthy nach genug Erfolgen]
  Pool[Traefik nimmt Version B in den Routing-Pool auf]
  Stop[Coolify sendet SIGTERM an Version A]
  Drain[Version A beendet offene Antworten in der Auslauffrist]
  Entfernt[Coolify entfernt Version A]

  Anfrage --> Proxy
  Proxy --> AltC
  Trigger --> NeuC
  NeuC --> Schleife
  Schleife --> Starting
  Starting --> Schleife
  Schleife --> Unhealthy
  Unhealthy --> Schleife
  Schleife --> Healthy
  Healthy --> Pool
  Pool --> Stop
  Stop --> Drain
  Drain --> Entfernt

Der Ablauf beginnt, bevor überhaupt ein neues Image existiert: Traefik leitet jede Anfrage an eine Kunden-Domain wie tenant.example.com an den Container mit der aktuell laufenden Version weiter, weil genau dieser Container als healthy gemeldet ist. Löst die Agentur über Coolify ein Deployment aus, baut Coolify zunächst das neue Image und startet daraus einen zweiten Container im selben Docker-Netzwerk, während der erste Container weiterläuft und weiter Traffic bedient. Dieses Prinzip nennt Coolify Start-before-Stop: Die neue Version steht schon, bevor die alte verschwindet, statt umgekehrt. Ab dem Moment, in dem der neue Container läuft, beginnt Dockers eigene Healthcheck-Schleife, ein in festen Abständen ausgeführter Befehl, dessen Rückgabewert einen von drei Zuständen setzt. Starting gilt, solange die im Dockerfile festgelegte Anlaufzeit noch läuft; Fehlschläge zählen in dieser Phase nicht gegen den Container. Unhealthy gilt, sobald der Befehl so oft hintereinander fehlgeschlagen ist wie konfiguriert. Healthy gilt erst, wenn genügend Versuche in Folge erfolgreich waren. Erst der Übergang zu healthy setzt die Kette in Bewegung, die die alte Version ablöst.

An dieser Stelle klärt sich die Frage, die in der Praxis am häufigsten zu Fehlkonfigurationen führt: Braucht Traefik einen eigenen, separat konfigurierten Healthcheck, um auf den Docker-Zustand zu reagieren? Für den Docker-Provider, den Coolify verwendet, lautet die Antwort nein. Traefik hört auf die Health-Status-Ereignisse, die der Docker-Daemon für jeden Container mit einer HEALTHCHECK-Anweisung ausgibt, und schließt Container in den Zuständen starting und unhealthy automatisch aus seinem Routing-Pool aus, ganz ohne eigenes Label. Erst wenn ein Container healthy meldet, nimmt Traefik ihn in die Liste der Ziele auf, an die er Anfragen verteilt. Die separaten Labels der Form traefik.http.services.tenant-acme.loadbalancer.healthcheck.path konfigurieren dagegen einen zweiten, unabhängigen Mechanismus: eine eigene, aktive Prüfung, die Traefik selbst periodisch gegen einen Endpunkt fährt, unabhängig von Dockers HEALTHCHECK. Sinnvoll ist dieser zweite Mechanismus vor allem dort, wo ein Container gar kein eigenes HEALTHCHECK im Image trägt oder wo mehrere Instanzen hinter demselben Servicenamen laufen. Für einen einzelnen Next.js-Container je Mandant mit HEALTHCHECK im Dockerfile reicht die native Beobachtung durch Traefik aus, ein zusätzliches Loadbalancer-Label ist nicht nötig.

Sobald Traefik den neuen Container in seinen Pool aufgenommen hat, sendet Coolify das Signal zum Stoppen an den alten Container. Docker schickt dabei zuerst SIGTERM und wartet eine Auslauffrist von standardmäßig zehn Sekunden, in der ein laufender Node-Prozess seine offenen Antworten noch fertigstellen kann, bevor Docker mit SIGKILL nachhilft. Überschreitet eine einzelne Anfrage diese Frist, reißt Docker die Verbindung trotzdem ab, und die Kundin sieht einen Verbindungsabbruch statt einer fertigen Antwort. Erst nachdem der alte Container tatsächlich beendet und entfernt ist, bedient ausschließlich die neue Version den gesamten Traffic für diesen einen Mandanten. Die anderen neununddreißig Websites auf demselben Host bekommen vom ganzen Vorgang nichts mit, solange Healthcheck und Traefik-Routing wie beschrieben zusammenspielen.

Schritt-für-Schritt-Umsetzung

Der Healthcheck im Dockerfile

Die HEALTHCHECK-Anweisung im Dockerfile bestimmt, welcher Befehl über den Zustand des Containers entscheidet, und mit welchem Rhythmus. Ein Next.js-Container, der intern auf Port 3000 läuft und einen eigenen Endpunkt für den Zustand bereitstellt, bekommt eine Zeile wie diese:

HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/api/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"

Interval legt fest, wie oft Docker den Befehl nach Ablauf der Anlaufzeit ausführt, hier alle zehn Sekunden. Timeout beendet einen einzelnen Versuch als Fehlschlag, wenn er länger als die angegebene Zeit braucht, hier drei Sekunden. Start-period ist die Anlaufzeit: Fehlschläge in diesem Fenster zählen nicht gegen den Container, was einer Next.js-Anwendung die Zeit gibt, Build-Cache und dynamische Routen beim ersten Start aufzuwärmen. Retries gibt an, wie viele Fehlschläge in Folge nötig sind, damit Docker den Container als unhealthy einstuft. Der Befehl selbst nutzt bewusst node statt curl: Ein schlankes Next.js-Image auf Alpine-Basis enthält meist keinen HTTP-Client, aber die Node-Laufzeit ist ohnehin Teil des Images, weil sie den Server selbst startet.

Wie eng interval und start-period zusammen sitzen dürfen, ist keine Geschmacksfrage, sondern messbar. Ein veröffentlichter Lasttest mit genau diesem Aufbau, Next.js-Container hinter Coolify und Traefik, zeigt beide Enden der Skala. Mit einer häufig kopierten Standardkonfiguration aus interval=5s, retries=3 und ganz ohne start-period markierte Docker den neuen Container als unhealthy, noch bevor die Anwendung überhaupt fertig gestartet war, und Coolify brach jedes einzelne Deployment ab, ohne dass der neue Code jemals live ging. Mit einer auf die tatsächlich gemessene Startzeit abgestimmten Konfiguration aus interval=2s, timeout=3s, start-period=20s und retries=3 sank die gemessene Ausfallzeit je Deployment auf 0,1 bis 0,2 Sekunden, also ein bis zwei fehlgeschlagene Anfragen bei mehreren tausend Aufrufen. Die Faustregel dahinter: Start-period muss die tatsächliche, gemessene Bootzeit der Anwendung deutlich übersteigen, nicht die im Idealfall erwartete. Ein zu kurzes Intervall in Kombination mit einer zu knappen Start-period ist der häufigste Grund, warum eine an sich korrekt bootende Next.js-Anwendung als kaputt gilt und ein inhaltlich fehlerfreies Deployment scheitert.

Der Healthcheck in der docker-compose.yml

Wer einen Mandanten als Compose-Ressource statt als native Coolify-Anwendung betreibt, trägt denselben Healthcheck stattdessen im Compose-Format ein:

services:
  tenant-acme:
    image: registry.example.com/tenant-acme:latest
    healthcheck:
      test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"]
      interval: 10s
      timeout: 3s
      start_period: 30s
      retries: 3

Die Felder entsprechen eins zu eins den Dockerfile-Optionen. Wichtig ist eine Einschränkung, die in der Praxis oft übersehen wird: Coolify führt für Ressourcen, die als Docker Compose eingerichtet sind, grundsätzlich keine Rolling Updates durch, unabhängig davon, wie sorgfältig der Healthcheck-Block hier konfiguriert ist. Derselbe veröffentlichte Lasttest, der oben die 0,1 bis 0,2 Sekunden für einen nativ verwalteten Container misst, hat dieselbe Anwendung zusätzlich als Compose-Ressource deployt und kam auf 22,7 bis 23,1 Sekunden vollständigen Ausfall pro Deployment, bei jedem einzelnen der gemessenen Durchläufe. Der Healthcheck in der Compose-Datei bleibt trotzdem sinnvoll, weil Docker selbst und abhängige Services (etwa über depends_on mit der Bedingung service_healthy) davon profitieren. Für Zero-Downtime bei einem einzelnen Kunden-Container führt der Weg aber über Coolifys nativen, Dockerfile-basierten Anwendungstyp, nicht über eine Compose-Ressource.

Traefik-Labels für den Rollout

Damit Traefik einen Container überhaupt kennt und weiß, wohin er Anfragen weiterleitet, braucht der Container drei Labels. Für zusätzliche Robustheit während der kurzen Übergangsphase, in der der neue Container schon läuft, aber sein erster Healthcheck-Erfolg noch aussteht, kommt eine Retry-Middleware dazu:

labels:
  - traefik.enable=true
  - traefik.http.routers.tenant-acme.rule=Host(`acme-kunde.de`)
  - traefik.http.services.tenant-acme.loadbalancer.server.port=3000
  - traefik.http.middlewares.tenant-acme-retry.retry.attempts=3
  - traefik.http.middlewares.tenant-acme-retry.retry.initialinterval=100ms
  - traefik.http.routers.tenant-acme.middlewares=tenant-acme-retry

Das Label traefik.enable macht den Container für Traefik überhaupt sichtbar; Coolifys nativer Anwendungstyp setzt es automatisch, bei einer selbst geschriebenen Compose-Datei muss es explizit stehen. Die rule bestimmt, welche Domain auf diesen Container zeigt, in der für Traefik v3 üblichen Schreibweise mit Backticks um den Hostnamen. Loadbalancer.server.port nennt den internen Container-Port, an den Traefik weiterleitet, nicht den Port auf dem Host. Ein falscher Wert hier führt zu einem trügerischen Bild: Docker meldet den Container als healthy, weil der eigentliche Healthcheck-Befehl im Container selbst läuft und den richtigen Port kennt, während Traefik von außen trotzdem keine Verbindung zusammenbekommt. Die Retry-Middleware lässt Traefik einen fehlgeschlagenen Verbindungsversuch bis zu dreimal wiederholen, mit einer ersten Wartezeit von 100 Millisekunden und wachsenden Abständen danach. Sie greift ausschließlich, wenn Traefik den Zielcontainer auf TCP-Ebene gar nicht erreicht; sobald der Container irgendeine Antwort liefert, egal mit welchem Statuscode, gilt der Versuch als abgeschlossen und die Middleware wiederholt nichts mehr.

Häufige Fehlerquellen

  • Kein HEALTHCHECK im Image oder in der Compose-Datei. Coolify und Traefik haben dann kein Signal, wann der neue Container tatsächlich bereit ist, und verlassen sich nur darauf, dass der Container läuft. Symptom: Nutzerinnen sehen während des Wechsels vereinzelt 502 Bad Gateway, weil Traefik kurz auf einen Container zeigt, der zwar gestartet ist, aber noch keine Anfragen beantworten kann. Fix: ein echtes HEALTHCHECK ergänzen, das denselben Endpunkt prüft, den auch echte Nutzerinnen aufrufen.
  • Interval und Retries enger gesetzt als die reale Bootzeit der Anwendung. Ohne ausreichende start-period markiert Docker einen Container als unhealthy, während Next.js selbst noch mitten im Start ist. Symptom: Coolify bricht jedes Deployment ab und rollt zurück, der neue Code geht nie live, obwohl er fehlerfrei wäre. Fix: die tatsächliche Startzeit einmal messen (Zeit vom Containerstart bis zur ersten erfolgreichen Antwort) und start-period großzügig darüber ansetzen.
  • Der Healthcheck-Endpunkt hängt selbst an einer wackligen Abhängigkeit. Prüft /api/health bei jedem Aufruf zusätzlich eine Datenbankverbindung oder einen externen Dienst, reißt ein kurzer Aussetzer dieser Abhängigkeit den ganzen Container mit in den Zustand unhealthy. Symptom: Deployments schlagen sporadisch und scheinbar grundlos fehl, obwohl die Anwendung selbst lauffähig ist. Fix: einen schlanken Endpunkt nur für den Prozesszustand vom tieferen Abhängigkeitscheck trennen.
  • Ein Mandant läuft als Docker-Compose-Ressource statt als native Anwendung. Coolify führt für Compose-Ressourcen grundsätzlich keine Rolling Updates durch, ein Healthcheck in der Compose-Datei ändert daran nichts. Symptom: Jedes Deployment reißt die Website für rund 20 bis 23 Sekunden komplett herunter, sichtbar als 503 mit einer Meldung wie "no available server", weil Traefiks Pool für diesen Moment leer ist. Fix: den Mandanten auf Coolifys Dockerfile-basierten Anwendungstyp umziehen, Compose nur noch für zustandsbehaftete Satelliten wie Datenbank oder Cache verwenden.
  • Falscher oder fehlender Port im Label loadbalancer.server.port. Zeigt das Label auf einen Port, auf dem die Anwendung gar nicht lauscht, meldet Docker den Container zwar als healthy, Traefik kann ihn aber trotzdem nicht erreichen. Symptom: Der Healthcheck in docker inspect steht auf healthy, Nutzerinnen bekommen trotzdem dauerhaft 502. Fix: den internen Port im Label gegen den tatsächlich im Container offenen Port prüfen, nicht gegen den auf dem Host gemappten.
  • Fester, selbst vergebener Containername statt Coolifys Standardbenennung. Ein fester Name erleichtert zwar den schnellen Zugriff per docker exec, verhindert aber, dass Coolify während eines Deployments einen zweiten Container mit eigenem Namen parallel starten kann. Symptom: Deployment schlägt mit einem Namenskonflikt fehl, oder Coolify stoppt den alten Container, bevor der neue überhaupt läuft. Fix: Coolifys automatische Namensvergabe unangetastet lassen und Container stattdessen über Labels oder Compose-Projektnamen identifizieren.

Häufig gestellte Fragen

Was passiert, wenn der Healthcheck-Endpunkt selbst von einer Datenbankverbindung abhängt, die kurz aussetzt?

Ein Healthcheck-Endpunkt, der bei jedem Aufruf zusätzlich eine Datenbankabfrage ausführt, meldet den gesamten Next.js-Container als unhealthy, sobald nur die Datenbank für ein paar Sekunden nicht erreichbar ist, selbst wenn der Next.js-Prozess selbst einwandfrei läuft und alle anderen Routen bedienen könnte. Das Problem ist eine vermischte Prüfung: Sie beantwortet gleichzeitig zwei verschiedene Fragen, ob der Prozess lebt und ob eine externe Abhängigkeit erreichbar ist, und lässt beide dasselbe Schicksal teilen. Die sauberere Lösung trennt einen schlanken Endpunkt, der nur den eigenen Prozess prüft, von einem tieferen Endpunkt für Abhängigkeiten, und verwendet für das Docker-HEALTHCHECK bewusst nur den schlanken.

Worin unterscheiden sich Liveness- und Readiness-Prüfung, wenn Docker nur ein einziges HEALTHCHECK kennt?

Liveness- und Readiness-Prüfung beantworten unterschiedliche Fragen: Liveness fragt, ob ein Prozess noch läuft und neu gestartet werden sollte, wenn er hängt; Readiness fragt, ob ein laufender Prozess gerade Anfragen annehmen darf. Kubernetes führt beide als zwei getrennte Prüfungen mit eigenen Regeln, Docker dagegen kennt pro Container nur die eine HEALTHCHECK-Anweisung und bildet damit praktisch nur Readiness ab, denn Traefik und Coolify reagieren auf genau diesen einen Zustand. Wer echtes Liveness-Verhalten braucht, etwa einen automatischen Neustart bei einem hängenden Prozess unabhängig vom Routing, muss das zusätzlich über die Docker-Restart-Policy oder eine externe Prozessüberwachung abdecken, denn das HEALTHCHECK allein löst diesen Neustart nicht selbstständig aus.

Warum bleibt ein Kunden-Container ohne Zero-Downtime, obwohl im docker-compose.yml ein Healthcheck definiert ist?

Ein Kunden-Container, der als Docker-Compose-Ressource in Coolify läuft, bleibt beim Deployment ohne Zero-Downtime, selbst wenn seine docker-compose.yml einen vollständigen Healthcheck-Block mit Interval, Timeout und Start-period enthält, weil Coolify für Compose-basierte Ressourcen grundsätzlich keine Rolling Updates durchführt. Der Healthcheck wirkt dort nur innerhalb von Docker Compose selbst, etwa für Startreihenfolgen zwischen Diensten, aber nicht als Signal an Coolifys Deployment-Mechanismus. Wer für einen bestimmten Mandanten echte Zero-Downtime-Deployments braucht, muss diesen einen Dienst als Coolifys nativen, Dockerfile-basierten Anwendungstyp einrichten und darf Compose höchstens noch für begleitende, zustandsbehaftete Dienste wie eine Datenbank verwenden.

Quellen

  1. Coolify-Dokumentation: Rolling Updates (Start-before-Stop), Coolify, abgerufen am 2026-08-26
  2. Coolify-Dokumentation: Health Checks, Coolify, abgerufen am 2026-08-26
  3. Dockerfile-Referenz: HEALTHCHECK-Anweisung, Docker Inc., abgerufen am 2026-08-26
  4. Traefik-Dokumentation: Docker-Provider-Referenz (Health-Status), Traefik Labs, abgerufen am 2026-08-26
  5. Coolify Zero-Downtime Deployments: Measured and Fixed, learnwithhasan.com, abgerufen am 2026-08-26

Der Content wurde mit Hilfe von KI erstellt, vor allem in der Recherche und Vorformulierung. Prüfung und Abnahme durch unsere Redaktion.