Entwicklung & Technik8 Min. Lesezeit04. Juli 2026
Engineering-Handwerk · Tag 7: Zero-Downtime-Deploys
Wie du ohne Ausfall deployst: rückwärtskompatible Migrationen, richtige Reihenfolge, Rolling Updates, Health-Checks, Auto-Rollback und Feature-Flags.
Willkommen zu Tag 7 unserer Serie Engineering-Handwerk. Heute geht es um den Moment, vor dem sich viele Teams insgeheim fürchten: das Deployment einer neuen Version, während echte Nutzer gerade auf der Anwendung arbeiten. Ein Zero-Downtime-Deployment bedeutet genau das — du bringst neuen Code und geänderte Datenstrukturen live, ohne dass auch nur eine einzige Anfrage ins Leere läuft. Kein Wartungsfenster nachts um drei, keine Statusseite mit „Wir sind gleich wieder da“, kein Bauchgefühl-Deploy am Freitagnachmittag. Klingt nach Magie? Ist es nicht. Es ist Handwerk — mit klaren Regeln, einer festen Reihenfolge und ein paar Sicherheitsnetzen, die dich vor deinem eigenen Änderungsdrang schützen.
Wir bauen bei Forge12 SaaS-Systeme, die rund um die Uhr laufen. Ein Deploy, der Nutzer aussperrt, ist für uns kein Deploy, sondern ein Ausfall mit Ankündigung. In diesem Beitrag zeigen wir dir, was Zero-Downtime wirklich verlangt, warum die meisten Ausfälle nicht vom Code, sondern von der Datenbank kommen, und wie eine sichere Migration Schritt für Schritt aussieht. Inklusive eines echten Beispiels, bei dem dieselbe Änderung einmal in die Katastrophe und einmal ins ruhige Fahrwasser führt.
Was Zero-Downtime-Deployment wirklich bedeutet
Ein Zero-Downtime-Deployment ist kein einzelnes Feature, das man aktiviert. Es ist eine Eigenschaft des gesamten Auslieferungsprozesses — von der Art, wie du deine Datenbank änderst, bis zu der Frage, ob dein Load-Balancer weiß, wann eine neue Instanz wirklich bereit ist. Der Kern lautet: Zu jedem Zeitpunkt läuft mindestens eine funktionsfähige Version deiner Anwendung, die Anfragen korrekt beantwortet.
Das Tückische daran ist ein Zustand, den Einsteiger gerne übersehen: Während des Deploys läuft für einige Sekunden bis Minuten alte und neue Version gleichzeitig. Wenn du deine Server nacheinander austauschst — und das solltest du — dann sprechen in diesem Fenster beide Code-Stände mit derselben Datenbank. Alter Code liest ein Schema, das neuer Code gerade verändert hat. Genau hier entstehen die stillen Datenfehler, die niemand im Testlauf gesehen hat, weil im Testlauf immer nur eine Version lief.
Warum die Datenbank der eigentliche Engpass ist
Anwendungscode ist zustandslos und leicht austauschbar. Du startest eine neue Instanz, sie ist da oder sie ist es nicht — im Zweifel wirfst du sie weg. Die Datenbank ist das Gegenteil: Sie hat einen Zustand, sie ist geteilt, und eine falsche Migration ist nicht mit einem Neustart behoben. Ein versehentlich gelöschtes Feld ist weg. Ein Schema-Lock, der eine große Tabelle blockiert, legt jede Anfrage lahm, die auf diese Tabelle zugreift. Deshalb dreht sich beim Zero-Downtime-Deployment neunzig Prozent der Disziplin um die Frage: Wie ändere ich Daten, ohne den laufenden Betrieb zu stören?
Rückwärtskompatible Migrationen: die goldene Regel
Die wichtigste Regel lautet: Jede Datenbank-Migration muss mit der alten UND der neuen Code-Version kompatibel sein. Solange sich während des Deploys beide Versionen die Datenbank teilen, darf keine von beiden über eine Schema-Änderung stolpern. Daraus folgen ein paar sehr konkrete Do’s und Don’ts.
Was sicher ist
- Eine neue, nullbare Spalte hinzufügen ist sicher. Alter Code ignoriert sie schlicht, neuer Code nutzt sie. Niemand fällt hin.
- Eine neue Spalte mit Default-Wert ist sicher, solange das Hinzufügen die Tabelle nicht komplett neu schreibt. Alter Code liest den Default, neuer Code setzt echte Werte.
- Eine neue Tabelle oder ein neuer Index stört den alten Code nicht — er weiß nichts davon. Bei Indizes auf großen Tabellen gilt: nebenläufig anlegen, damit kein exklusiver Schreib-Lock entsteht.
Was gefährlich ist
- Eine Spalte entfernen im selben Release wie der Code, der sie nicht mehr braucht — das ist der Klassiker unter den Selbstverletzungen. Solange noch alter Code läuft, der diese Spalte liest oder beim Insert erwartet, brechen dessen Anfragen. Spalten entfernt man erst im nächsten Release, wenn garantiert kein Code mehr darauf zugreift.
- Eine Spalte umbenennen ist in Wahrheit „Spalte entfernen und neue anlegen“ — also doppelt gefährlich. Der saubere Weg ist: neue Spalte anlegen, beide eine Weile parallel schreiben, Daten kopieren, Code umstellen, alte Spalte später löschen.
- NOT NULL ohne Default direkt setzen sprengt jede laufende Anfrage des alten Codes, der die Spalte noch leer lässt — und blockiert obendrein die Tabelle, während die Datenbank jede Zeile prüft.
NOT NULL in Schritten — die Erweiterung-vor-Verengung-Regel
Ein NOT-NULL-Constraint ist eine Verengung des erlaubten Zustands. Verengungen zieht man nie in einem Rutsch durch, sondern in Etappen, damit zu keinem Zeitpunkt ein gültiger Zustand plötzlich ungültig wird:
- Schritt eins: Spalte als nullbar hinzufügen. Nichts bricht.
- Schritt zwei: Neuen Code deployen, der die Spalte bei jedem Schreibvorgang füllt. Ab jetzt entstehen keine neuen Lücken mehr.
- Schritt drei: Bestehende Zeilen im Hintergrund in Batches nachfüllen — nicht in einem einzigen riesigen Update, das die Tabelle sperrt.
- Schritt vier: Erst wenn keine Zeile mehr NULL ist, den NOT-NULL-Constraint setzen. Jetzt ist die Prüfung ein reiner Formalakt.
Dasselbe Prinzip — erst erweitern, was erlaubt ist, dann verengen — gilt für Fremdschlüssel, Unique-Constraints und Typ-Änderungen. Die Datenbank muss zu jedem Zeitpunkt mit dem umgehen können, was beide gleichzeitig laufenden Code-Versionen ihr geben.
Die Reihenfolge entscheidet: Migration vor Code
Es gibt eine feste Choreografie, und ihre Reihenfolge ist nicht verhandelbar: Erst die Datenbank-Migration, dann der Code. Weil die Migration rückwärtskompatibel gebaut ist, verträgt der noch laufende alte Code das neue Schema problemlos — er sieht nur eine zusätzliche Spalte, die er ignoriert. Würdest du umgekehrt vorgehen und zuerst den Code deployen, der eine Spalte erwartet, die es noch nicht gibt, dann bricht die neue Version in der Sekunde ihres Starts.
Der komplette Ablauf sieht bei uns so aus:
- Migration ausführen — additiv und rückwärtskompatibel.
- Neue App-Version im Rolling Update ausrollen, Instanz für Instanz.
- Health-Check jeder neuen Instanz abwarten, bevor sie Traffic bekommt.
- Smoke-Test gegen die kritischen Endpunkte fahren.
- Erst dann die alten Instanzen abschalten.
Und die aufräumenden, „verengenden“ Schritte — Spalte löschen, Constraint verschärfen, alten Pfad entfernen — kommen bewusst erst im übernächsten Zug, wenn nirgends mehr alter Code lebt.
Rolling Updates, Health-Checks, Smoke-Tests und Auto-Rollback
Die rückwärtskompatible Migration ist die halbe Miete. Die andere Hälfte ist, wie du den Code selbst austauschst.
Rolling Updates statt Big Bang
Beim Rolling Update ersetzt du deine Instanzen nacheinander, nicht alle auf einmal. Während Instanz A durch die neue Version ersetzt wird, tragen B und C weiter den Traffic. Ist A gesund, kommt B dran. So gibt es nie einen Moment, in dem null Instanzen bereitstehen — die Voraussetzung dafür, dass „Zero“ in Zero-Downtime überhaupt stimmt.
Health-Checks, die die Wahrheit sagen
Ein Load-Balancer darf einer neuen Instanz erst dann Traffic schicken, wenn sie wirklich bereit ist — nicht schon, wenn der Prozess gestartet ist. Der Unterschied ist gewaltig. Ein guter Health-Check prüft nicht nur „läuft der Webserver“, sondern „habe ich eine Datenbankverbindung, erreiche ich Redis, sind meine Worker da“. Trennst du dabei Liveness (lebt der Prozess?) von Readiness (kann er Anfragen bedienen?), verhinderst du, dass Nutzer auf eine halb hochgefahrene Instanz treffen, die zwar antwortet, aber intern noch nicht angebunden ist.
Smoke-Tests direkt nach dem Deploy
Bevor die alte Version verschwindet, prüfst du automatisiert die wenigen Endpunkte, deren Ausfall dich das Geschäft kosten würde: Login, der zentrale Lese-Endpunkt, der wichtigste Schreib-Pfad. Ein Smoke-Test ist kein vollständiges Testset — er ist der schnelle Griff an die Herdplatte, der dir in Sekunden sagt, ob die neue Version grundsätzlich lebt.
Auto-Rollback als Sicherheitsnetz
Fällt ein Health-Check oder Smoke-Test durch, darf das Deployment nicht einfach weiterlaufen. Es muss automatisch stoppen und zur letzten funktionierenden Version zurückkehren. Weil dein Code rückwärtskompatibel zur bereits ausgeführten Migration ist, ist dieser Rollback gefahrlos möglich — die alte Version verträgt das neue Schema ja. Genau das ist der tiefere Grund, warum wir Schema und Code entkoppeln: Ein Code-Rollback darf niemals einen Datenbank-Rollback erzwingen.
Praxisbeispiel: dieselbe Migration, zwei Ausgänge
Nehmen wir eine reale Aufgabe aus einem unserer Multi-Tenant-Systeme. Die Tabelle mit den Rechnungen soll ein Pflichtfeld für die Zahlungsart bekommen. Bisher stand die Zahlungsart irgendwo im Freitext, künftig soll sie eine eigene, verpflichtende Spalte werden. Über die Tabelle laufen ständig Lese- und Schreibvorgänge — Kunden erzeugen live Rechnungen.
Der Weg, der schiefgeht
Ein Entwickler unter Zeitdruck packt alles in eine Migration und ein Release: Er fügt die Spalte in einem Schritt als NOT NULL hinzu, ohne Default. Weil die Tabelle Millionen Zeilen hat, muss die Datenbank für den Constraint jede einzelne Zeile prüfen und hält dafür einen Lock — die Rechnungstabelle ist minutenlang blockiert, jede Kundenanfrage darauf hängt. Gleichzeitig läuft der Deploy als Rolling Update, also spricht für einige Sekunden noch die alte Code-Version mit dem neuen Schema. Diese alte Version kennt die neue Spalte nicht und schickt beim Anlegen einer Rechnung kein Zahlungsart-Feld mit. Die Datenbank lehnt den Insert wegen der NOT-NULL-Regel ab. Ergebnis: fehlgeschlagene Rechnungen, wütende Kunden, und ein Team, das mitten im Deploy hektisch überlegt, ob ein Rollback jetzt noch alles schlimmer macht. Aus einem geplanten Feature ist ein Incident geworden.
Der Weg, der sicher ist
Dieselbe fachliche Änderung, aber in Etappen zerlegt. Release eins fügt die Spalte nullbar und ohne teure Prüfung hinzu — eine schnelle, additive Migration, die niemanden blockiert. Der neue Code, der danach ausrollt, füllt die Zahlungsart bei jeder neuen Rechnung. Alter Code, der im Deploy-Fenster noch mitläuft, ignoriert die Spalte einfach; da sie nullbar ist, geht sein Insert ohne das Feld weiterhin durch. Nichts bricht.
Zwischen den Releases läuft ein Hintergrund-Job, der die Altbestände in kleinen Batches nachfüllt — ein paar tausend Zeilen pro Durchlauf, mit Pausen, damit die Datenbank atmen kann. Kein großer Lock, keine spürbare Last. Release zwei setzt schließlich den NOT-NULL-Constraint. Weil zu diesem Zeitpunkt jede Zeile bereits einen Wert hat und aller laufende Code die Spalte füllt, ist die Prüfung schnell und risikolos. Dieselbe Zielstruktur wie im Katastrophen-Szenario — nur ohne einen einzigen fehlgeschlagenen Request. Der einzige Unterschied ist die Disziplin, die Änderung in erweiternde und verengende Schritte zu zerlegen.
Feature-Flags für riskante Änderungen
Manche Änderungen sind zu groß, um sie mit einem Deploy scharfzuschalten — ein neuer Bezahl-Flow, eine umgeschriebene Suchlogik, ein anderer Berechnungspfad. Hier trennst du das Ausliefern des Codes vom Aktivieren des Verhaltens. Der neue Pfad wird deployt, aber hinter einem Feature-Flag deaktiviert. Erst wenn er im Produktivsystem intern getestet ist, schaltest du ihn frei — zuerst für einen Test-Tenant, dann für ausgewählte Kunden, dann schrittweise für alle.
Der entscheidende Gewinn: Geht etwas schief, drehst du das Flag zurück, ohne neu deployen zu müssen. Deaktivieren dauert Sekunden statt Minuten, und du triffst genau den Bereich, der Probleme macht, statt die ganze Version zurückzurollen. Feature-Flags und rückwärtskompatible Migrationen ergänzen sich perfekt: Die Migration bringt die Datenstruktur risikofrei live, das Flag entscheidet, wann und für wen der neue Code sie tatsächlich nutzt. Nur eine Bitte: Räume alte Flags wieder auf. Ein Flag, das seit Monaten für alle an ist, ist keine Sicherheit mehr, sondern nur noch toter Code und eine Verzweigung, die niemand mehr versteht.
So gehst du vor
Wenn du das Ganze auf eine wiederholbare Routine eindampfen willst, dann fahr für jede Änderung diese Checkliste ab:
- Trenne Schema und Code gedanklich. Frag bei jeder Migration: Verträgt der aktuell laufende alte Code dieses neue Schema? Wenn nein, ist die Migration nicht rückwärtskompatibel und muss zerlegt werden.
- Mach Migrationen additiv. Hinzufügen zuerst, Entfernen später. Neue Spalten nullbar oder mit Default. Löschungen und schärfere Constraints kommen in ein separates, späteres Release.
- Zerlege Verengungen in Etappen. NOT NULL, Unique und Fremdschlüssel nie in einem Schritt — erst Spalte, dann füllen, dann Constraint.
- Halte die Reihenfolge ein. Migration vor Code. Immer.
- Rolle rollierend aus. Instanz für Instanz, mit ehrlichen Readiness-Checks, die Datenbank und Abhängigkeiten wirklich prüfen.
- Verifiziere automatisch. Health-Check plus Smoke-Test auf die kritischen Endpunkte, bevor die alte Version stirbt.
- Baue ein Sicherheitsnetz ein. Auto-Rollback bei rotem Check — gefahrlos, weil der alte Code zum neuen Schema passt.
- Nutze Flags für das Riskante. Code ausliefern, Verhalten getrennt aktivieren, schrittweise ausrollen, altes Flag später entfernen.
Der wichtigste Perspektivwechsel dabei: Zero-Downtime ist keine Sache des Deploy-Tools, sondern deiner Migrationsdisziplin. Das beste Rolling-Update-System der Welt rettet dich nicht, wenn deine Migration die Tabelle sperrt oder den alten Code aussperrt. Umgekehrt kommst du mit sauber zerlegten, rückwärtskompatiblen Änderungen selbst mit einfachem Werkzeug erstaunlich weit.
Fazit
Ein Zero-Downtime-Deployment ist kein Zauberstück, sondern die Summe kleiner, disziplinierter Entscheidungen: rückwärtskompatible Migrationen, die additiv hinzufügen und erst später aufräumen, Verengungen wie NOT NULL in Etappen, die feste Reihenfolge Migration-vor-Code, rollierende Updates mit ehrlichen Health-Checks, Smoke-Tests als schneller Realitätscheck, Auto-Rollback als Netz und Feature-Flags für alles, was zu heikel für einen harten Cutover ist. Wer diese Bausteine verinnerlicht, deployt irgendwann mitten am Dienstagvormittag — ohne Herzklopfen, ohne Wartungsfenster, ohne dass ein Kunde etwas merkt. Genau das ist das Ziel: Auslieferung so unspektakulär, dass sie langweilig wird.
Mehr Beiträge aus dieser Reihe findest du in der Serien-Übersicht. Wenn du eine Anwendung betreibst, die zuverlässig und ohne Ausfälle ausliefern soll, unterstützen wir dich bei der Softwareentwicklung — von der Migrationsstrategie bis zur Deploy-Pipeline. Erzähl uns von deinem System über unser Kontaktformular, und wir schauen gemeinsam, wo dein nächster sicherer Deploy anfängt.