Mein erster Deploy: Was zwischen local und production liegt

Pius Kastovsky
Von Pius Kastovsky

📅17. September 2026

Um 13:25 Uhr lief zum ersten Mal Code von mir in Produktion. Kein Feuerwerk, nur eine Zeile im Log.

Gebaut haben wir in meinem Praktikum bei KiWebBoost einen Dienst für Fluggastrechte: mehr als drei Stunden Verspätung, Anspruch auf eine Ausgleichszahlung. Vier Dinge haben mir dabei am meisten beigebracht — und keines davon meldet sich von selbst.

LokalentwickelnBranchpushTestsSQLite → OracleImagebauenProductionläuft1Bug ohne Absturz2grün ≠ fertig3SQLite ≠ Oracle4steht nicht im Code

Zwei der vier sitzen dort, wo man sie sucht — in den Tests. Die beiden anderen nicht.

1. Der Bug, der nicht abstürzt

Wie viel Geld zusteht, hängt an der Entfernung. Keine reine Leiter:

def amount_for_distance_km(distance_km, intra_eu=False):
    if distance_km <= TIER_SHORT_KM:        # 1500 km
        return AMOUNT_SHORT                 # 250 EUR
    if intra_eu or distance_km <= TIER_MEDIUM_KM:
        return AMOUNT_MEDIUM                # 400 EUR
    return AMOUNT_LONG                      # 600 EUR

Sechs Zeilen. Auf die vierte kommt es an.

Das oder entscheidet: Innerhalb der EU sind 400 Euro das Maximum, egal wie weit. Wien–Teneriffa, 3.613 Kilometer, trotzdem 400. Wer nur nach Kilometern staffelt, bekommt keinen Absturz und keinen roten Test — sondern eine saubere Zahl, die um 200 Euro zu hoch ist, in einem Schreiben an eine Fluggesellschaft.

2. Grün heißt nicht fertig

Die Suite war grün. Acht E-Mails pro Lauf verließen sie trotzdem, an eine Adresse aus einer Testfixtur, die es nicht gibt. Der Versand war im Test nicht abgefangen — und Mailversand ist bewusst als „best effort“ gebaut, jeder Fehler wird geschluckt. Genau das machte den Fehler unsichtbar.

Der Schutz blockiert nicht nur — er sieht danach nach, ob jemand es versucht hat:

@pytest.fixture(autouse=True)
def kein_echter_mailversand(request, monkeypatch):
    versuche = []

    # ... requests.post wird blockiert, sobald die URL
    #     auf den Mailanbieter zeigt, und in "versuche"
    #     vermerkt ...

    yield

    # Die zweite Hälfte ist die wichtige: die Sender
    # schlucken jeden Fehler, also würde ein blockierter
    # Versand sonst spurlos verschwinden.
    assert not versuche

Ein Schutz, der still versagt, ist kein Schutz.

Ein grünes Häkchen sagt: keine Behauptung verletzt. Nicht: nichts passiert.

3. SQLite ist nicht Oracle

Tests laufen auf SQLite, weil das schnell ist. Produktion läuft auf Oracle. Beim ersten Lauf gegen eine echte Oracle-Datenbank fielen acht von 348 Tests um: SQLite hält sich nicht an Feldlängen, ein vierstelliger Flughafencode in einem Feld für drei Zeichen ging klaglos durch. Keiner der acht war ein Bug im Produkt — die Tests waren nur auf einer Datenbank richtig, die wir gar nicht benutzen.

4. Der Tag des Deploys

Der Rest lag außerhalb des Codes. Der Webserver vor der Anwendung erlaubte Uploads bis ein Megabyte, die Anwendung selbst zehn. Ein Foto einer Buchungsbestätigung liegt dazwischen — es wäre abgewiesen worden, bevor mein Code es zu sehen bekommt. Das findet kein Test, weil es nicht im Programm steckt.

Danach bin ich den Weg einmal als Kunde gegangen. Kein Ticket hat das verlangt; ein Testlauf prüft Zusicherungen, nicht ob der Ablauf als Ganzes stimmt.

Glasfaserkabel, die in einem Patchfeld zusammenlaufen

Was zusammenläuft

Ein Fall setzt sich aus drei Richtungen zusammen. Planzeiten und tatsächliche Ankunft holen wir von zwei unabhängigen Diensten, die nichts voneinander wissen.

KundeStrecke, Datum, FallartFlugplandatenPlanzeiten, FluglinieFlugverfolgungtatsächliche AnkunftAnwendungprüft, rechnet, verschicktDatenbankein Fall, ein VerlaufE-MailFallnummer, Status, LinksFallaktefür die Rechtsvertretung

Die beiden hervorgehobenen Quellen liefern dieselbe Zahl: die Ankunftszeit, an der der ganze Anspruch hängt. Wir speichern beide Antworten getrennt, statt uns für eine zu entscheiden — weichen sie voneinander ab, wird der Fall zur Prüfung markiert.

Arbeitsteilung mit einer KI

Ein großer Teil dieses Projekts ist mit einem KI-Assistenten entstanden, und es wäre unehrlich, das kleinzureden: Er hat den Großteil des Codes geschrieben und den Großteil der Fehler gefunden.

Der Aufwand hat sich damit verlagert: mitlesen. Verstehen, warum etwas so gebaut wurde und nicht anders, und merken, wenn eine Entscheidung zwar funktioniert, an dieser Stelle aber nicht passt. Das ist keine Nebentätigkeit, das ist die Arbeit — er klingt genauso überzeugt, wenn er danebenliegt.

Beleuchtetes Cockpit bei Nacht

Der Autopilot fliegt die Strecke. Gelesen werden die Instrumente trotzdem.

Fazit

Keines der vier meldet sich von selbst. Kein Absturz, kein roter Test, keine Fehlermeldung — man findet sie nur, wenn man hinter das grüne Häkchen schaut.

Dass ich an etwas mitbauen durfte, das in den Betrieb gehen kann, und dabei echte Entscheidungen treffen konnte, macht für mich den Unterschied zwischen einem Praktikum und einer Übungsaufgabe. Dafür bedanke ich mich.

TechStack

  • Backend: Django (Python), Django REST Framework
  • Frontend: Next.js, TypeScript, Tailwind CSS
  • Datenbank: Oracle Cloud ADB
  • Testing: PyTest (Backend), Playwright (Frontend)
  • Versionierung & CI/CD: GitHub & GitHub Actions
  • Containerisierung: Docker, NGINX als Reverse Proxy
  • Externe Daten: Flugplan- und Flugverfolgungsdienste