Meine Produktions-Deploys dauern 15 Minuten. Ich schaue sie mir nicht an. Ich halte nicht den Atem an. Ich mache Tee, und wenn er gezogen ist, ist die neue Version live.
Das war nicht immer so.
Vor zwei Jahren waren Deploys ein Ritual. Eine Stunde blocken. Dem Team Bescheid sagen. Das Skript manuell ausführen. Die Logs beobachten. Beten. Das kaputte Ding reparieren. Wieder ausführen. Fester beten. Der ganze Prozess dauerte 2-3 Stunden und hinterließ alle erschöpft. Wir haben freitags deployed, weil wir uns selbst hassten.
Wenn das vertraut klingt, fährst du Deploys auf Adrenalin statt auf Systemen. Ab März 2026 gibt es keinen Grund dafür. Hier ist, wie ich ein 3-Stunden-Angst-Festival in 15 langweilige Minuten verwandelt habe. ⚙️
Das Problem: Elf Schritte im Kopf
Deploy — der Prozess, neuen Code von deinem Computer auf den Server zu bringen, wo Benutzer ihn tatsächlich sehen — bedeutete 11 verschiedene Dinge gleichzeitig. Code ziehen. Abhängigkeiten installieren (externe Bibliotheken, die deine App benötigt). Migrationen ausführen (die Datenbankstruktur auf den neuen Code aktualisieren). Assets bauen. Dienst neu starten. Cache leeren. Reverse Proxy aktualisieren. Gesundheits-Endpunkt prüfen.
All das stand auf einer Wiki-Seite, die zu 40% veraltet war. Neue Teammitglieder hatten Angst. Die Senior-Mitglieder waren einfach müde.
Die Lösung war kein ausgefallenes Tool. Es war Disziplin.
Schritt 1: Ein Skript, ein Befehl
Ich habe ein einziges Bash-Skript geschrieben — ein kleines Programm, das Befehle nacheinander ausführt, wie ein Rezept, das der Computer Zeile für Zeile befolgt. Ich nannte es deploy.sh. Es erledigt alle 11 Dinge in der Reihenfolge. Wenn ein Schritt fehlschlägt, stoppt das Ganze und sagt dir, welcher Schritt kaputt gegangen ist und warum.
#!/bin/bash
set -euo pipefail
APP=$1
DEPLOY_LOG="/var/log/deploys/${APP}-$(date +%Y%m%d-%H%M%S).log"
echo "Deploying ${APP}..." | tee "$DEPLOY_LOG"
# Each step: name, command, rollback
deploy_step "Pull code" "git -C /srv/${APP} pull origin main"
deploy_step "Install deps" "cd /srv/${APP} && pip install -r requirements.txt"
deploy_step "Run migrations" "cd /srv/${APP} && python manage.py migrate"
deploy_step "Build assets" "cd /srv/${APP} && npm run build"
deploy_step "Restart service" "systemctl restart ${APP}"
deploy_step "Health check" "curl -sf http://localhost:8080/health"
echo "Deploy complete: ${APP}" | tee -a "$DEPLOY_LOG"
Die deploy_step-Funktion umschließt jeden Befehl mit Logging, Timing und Fehlerbehandlung. Wenn der Gesundheits-Check fehlschlägt, wird automatisch die vorherige Version neu gestartet — ein Rollback, was bedeutet "rückgängig machen und zurückgehen zu dem, was funktionierte".
Gesamtaufbauzeit: 45 Minuten. Diese 45 Minuten haben seitdem Hunderte Stunden gespart. Die zentrale Idee kommt direkt aus der Twelve-Factor App Methodology — ein Code-Base, ein Build, ein Deploy-Befehl.
Schritt 2: CI/CD — Der Roboter, der für dich deployt
CI/CD steht für Continuous Integration / Continuous Deployment. Einfach ausgedrückt: ein System, das deinen Code automatisch testet, wenn du Änderungen pushst (das ist der CI-Teil), und wenn die Tests bestehen, es automatisch auf den Server deployt (das ist der CD-Teil). Du pushst Code, der Roboter kümmert sich um den Rest.
Für ein kleines Team brauchst du kein Jenkins (ein schwergewichtiges Automatisierungs-Server), ArgoCD (ein Kubernetes-Deployment-Tool) oder einen Kubernetes-Cluster (ein Container-Orchestrierungssystem — wenn diese Wörter dir nichts sagen, gut, du brauchst es nicht). Du brauchst GitHub Actions und einen Server mit SSH-Zugang. GitHub Actions wurde 2019 eingeführt und hat sich zu der Standard-CI/CD-Wahl für Teams entwickelt, die bereits auf GitHub sind — stabil, gut dokumentiert und kostenlos für kleine Workloads.
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest --tb=short
deploy:
needs: test
runs-on: ubuntu-latest
if: success()
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SSH_KEY }}
script: bash /srv/myapp/deploy.sh myapp
Das ist die gesamte Pipeline — eine Abfolge von automatisierten Schritten, die der Code von deinem Laptop bis zur Produktion durchläuft. Push auf main (der primäre Zweig deines Codes). Tests laufen auf den Servern von GitHub. Wenn sie bestehen, verbindet sich GitHub über SSH (ein sicheres Remote-Verbindungsprotokoll) mit deinem Server und führt deploy.sh aus.
Keine Container. Keine Orchestrierer. Keine YAML-Dateien, die sich über 200 Zeilen erstrecken. Das kostenlose Kontingent von GitHub Actions bietet dir 2.000 Minuten pro Monat — mehr als genug für ein kleines Team, das 2-3 Mal pro Tag deployed.
Schritt 3: Ein Gesundheits-Check, der tatsächlich die Gesundheit prüft
Die meisten Gesundheits-Checks sind eine /health URL, die {"status": "ok"} zurückgibt. Das sagt dir, dass der Webserver läuft. Es sagt dir nichts darüber, ob die App tatsächlich funktioniert. Es ist, als würde man jemanden fragen "Bist du am Leben?" wenn man fragen sollte "Kannst du sehen, hören und deine Finger bewegen?"
Meine Gesundheits-Checks überprüfen drei Dinge:
@app.get("/health")
async def health():
checks = {
"database": await check_db_connection(),
"cache": await check_redis_ping(),
"disk": check_disk_space_above(20), # percent
}
all_ok = all(checks.values())
return JSONResponse(
status_code=200 if all_ok else 503,
content={"status": "healthy" if all_ok else "degraded", "checks": checks}
)
Datenbank ausgefallen? Gesundheits-Check schlägt fehl. Redis (ein In-Memory-Cache — eine schnelle temporäre Speicherebene) nicht erreichbar? Schlägt fehl. Festplatte zu 95% voll? Schlägt fehl. Das Deploy-Skript ruft diesen Endpunkt nach dem Neustart auf. Wenn es 503 zurückgibt (den HTTP-Statuscode für "Dienst nicht verfügbar"), wird das Deployment automatisch zurückgesetzt.
Dieser einzelne Endpunkt hat mehr Probleme nach einem Deploy erkannt als alle unsere manuellen Tests zusammen. ⚙️
Schritt 4: Benachrichtigungen, keine Dashboards
Ich sitze nicht da und schaue Überwachungs-Dashboards an. Das Deploy-Skript sendet genau zwei Nachrichten:
- Deploy gestartet — "Deploying myapp um 14:32. Ausgelöst durch Commit abc123."
- Deploy abgeschlossen — "myapp in 14m 22s deployed" oder "myapp-Deploy FEHLGESCHLAGEN bei Schritt: Gesundheits-Check. Zurückgesetzt."
Diese gehen in einen Telegram-Kanal. Wenn ich eine Fehlerbenachrichtigung sehe, untersuche ich. Wenn ich einen Erfolg sehe, trinke ich meinen Tee.
Kein Grafana (ein populäres Dashboard-Tool). Kein PagerDuty (ein Alarmdienst, der dich um 3 Uhr morgens weckt). Für ein kleines Team, das 2-3 Dienste betreibt, ist eine Telegram-Nachricht das richtige Komplexitätsniveau. Die Komplexität sollte der Größe des Problems entsprechen, nicht der Größe deines Ehrgeizes. 🫶
Schritt 5: Deploy am Montagmorgen
Wir haben aufgehört, freitags zu deployen. Wir deployen jetzt montag- und mittwochmorgens.
Warum? Weil wenn am Montag etwas kaputt geht, hast du fünf Tage, um es mit frischem Verstand zu reparieren. Deploys am Freitag bedeuten Debugging am Wochenende. Debugging am Wochenende führt zu Burnout. Burnout führt zu Fehlern. Fehler führen zu mehr Debugging am Wochenende.
Das ist eine negative Rückkopplungsschleife — ein Kreislauf, bei dem jedes schlechte Ergebnis das nächste verursacht. Das ist keine Deploy-Strategie.
Der langweilige Deploy-Zeitplan: Montag- und Mittwochmorgen. Nichts nach 15 Uhr. Nichts freitags. Nichts während des Mittagessens. Wenn es nicht für das Montag-Zeitfenster bereit ist, wartet es. Warten ist billig. Burnout ist teuer.
Worauf du verzichtest
Ehrlichkeitszeit. Dieses Setup hat Kompromisse.
Keine Zero-Downtime-Deploys. Wenn systemctl restart läuft, gibt es eine Lücke von 2-5 Sekunden, in der die App nicht verfügbar ist. Für einen Dienst, der Millionen von Anfragen bearbeitet, ist das wichtig. Für die meisten kleinen Teams fällt es niemand auf. Wenn du das überholst, schaue dir Blue-Green-Deployments an (zwei Kopien deiner App betreiben und den Traffic zwischen ihnen umschalten) — aber starte nicht dort.
Ein Server, ein Single Point of Failure. Wenn der Server ausfällt, fällt alles aus. Redundanz kostet Geld. Für die meisten Projekte mit weniger als 10.000 $ monatlichem Umsatz ist ein einziger gut gepflegter Server mit täglichen Backups die richtige Antwort. Skalierungsprobleme sind gute Probleme.
Keine Containerisierung. Docker (ein Tool, das deine App mit all ihren Abhängigkeiten in eine isolierte Einheit verpackt) ist wunderbar für komplexe Umgebungen. Für eine Python-App mit Datenbank und Redis auf einem Server ist es Overhead. Du kannst es immer später hinzufügen.
Die DORA Metriken Forschung von Google zeigt, dass Elite-Teams on-demand mit Vorlaufzeiten unter einer Stunde deployen. Aber sie zeigen auch, dass du schrittweise dorthin gelangst — nicht indem du jedes Tool auf einmal einführst.
Das Ergebnis
Vorher: 3-Stunden-Deploys, 1-2 Mal pro Woche, Team von 3 Personen blockiert. Nachher: 15-Minuten-Deploys, 2-3 Mal pro Woche, null Personen zuschauend.
Monatlich gesparte Zeit: ungefähr 24 Stunden kollektive Teamzeit. Jährliche Kosten des CI/CD-Setups: 0 $ (GitHub Actions kostenlose Stufe plus ein Bash-Skript). Aufbauzeit: ein Wochenende.
Mach es langweilig
Das Ziel guter Ops ist es nicht, Deploys spannend zu machen. Es ist, sie so langweilig zu machen, dass du vergisst, dass sie passieren.
Wenn du ohne Adrenalin deployst, deployst du öfter. Wenn du öfter deployst, ist jedes Deploy kleiner. Wenn jedes Deploy kleiner ist, gehen weniger Dinge kaputt. Wenn weniger Dinge kaputt gehen, schläfst du besser.
Das ist die ganze Philosophie. Ein Skript. Eine Pipeline. Ein Gesundheits-Check. Ein Benachrichtigungskanal. Ein ruhiger Morgen pro Woche.
Mach es langweilig. Mach es klein. Mach es automatisch. Geh, mach Tee. 🫶





