Technologie

Fünf Container.
Mehr braucht es nicht.

Die Plattform lief einmal auf über zehn Diensten in vier getrennten Stacks. In wenigen Tagen entstanden dabei rund zehn Infrastrukturprobleme — und kein einziger Datenimport. Die Diagnose war unbequem: Die Architektur war für Phase 5 gebaut, das Produkt stand in Phase 0.

5Dienste in einer Compose-Datei
87Migrationen, beim Start selbst angewendet
16Jobs, jeder idempotent und einzeln auslösbar
1 GBDatenbankgröße im laufenden Betrieb

Film 02 · 5 Minuten

Wie lokalis funktioniert

Alle acht Filme
§ 01
Aufbau

Die Plattform in fünf Schichten

Von der amtlichen Quelle bis zum Bildschirm. Der Umschalter stellt den Entwurf von Mai 2026 der heute laufenden Architektur gegenüber — durchgestrichen ist, was nie gebaut oder wieder ausgebaut wurde.

Schicht 05

Zugang

Was ein Mensch zu sehen bekommt.

Next.js 14Kunden-Dashboard · Port 3020
Marketing-SiteExpress · eigene Application
Import-Studiointerner Import-Arbeitsplatz
Mobile AppEntwurf, iOS zuerst
MetabaseSelf-Service-BI
Airflow-UIOrchestrierungs-Oberfläche
Schicht 04

Auslieferung

Zugang, Verschlüsselung, Berechtigung.

TraefikCoolify-Proxy · Netz gepinnt
TLS / Let’s Encryptautomatisch erneuert
NginxSecurity-Header · CSP self
FastAPI + Uvicornasynchron · OpenAPI
JWT + RefreshBlacklist in Redis
RBAC5 Rollen · Org-Kontext
Keycloakeigener Identity-Server
Schicht 03

Dienste

Rechnen, verdichten, erzeugen.

16 API-Routerauth · kpis · berichte · admin …
APScheduler-Worker16 Jobs · ein Container
Werte-AuflösungKreis / Land / Bund an einer Stelle
AggregationKreis → Land → Bund, abgeglichen
Score-Berechnungnach jedem Import
spaCy NLPKldB-2020 · WZ-2008
Berufs-Wörterbuch4.352 Benennungen, ohne Modell
WeasyPrintHTML → PDF
PhotonGeocoder, optional
CeleryTask-Queue
Prophet / SARIMAPrognose-Engine
SHAPScore-Erklärung
Schicht 02

Ingestion

Beschaffen, prüfen, verbuchen.

KonnektorenBA · GENESIS · Jobbörse · Register
Import-LedgerDatei-Hash · Periode · Los
Upload-PanelsBatch · Wahlen · INKAR
Dedup (MinHash)Anzeigen entdoppeln
ValidatorBereichs- und FK-Prüfung
dbt-Modelleraw → staging → mart
Apache AirflowDAG-Orchestrierung
Scrapy + PlaywrightPortal-Crawling
Great ExpectationsDatenqualitäts-Suite
Miniflux RSSNachrichten-Feeds
Schicht 01

Speicher

Die einzige Quelle der Wahrheit.

PostgreSQL 15TimescaleDB · PostGIS
Redis 7.2Cache · Token-Blacklist
Auto-Migration87 Skripte · Advisory-Lock
pg_dump täglich03:15 · 14 Tage Vorhaltung
KafkaEvent-Streaming
ElasticsearchVolltextsuche
Schicht 00

Quellen

Alles amtlich, alles frei zugänglich.

BA-StatistikHefte · SDI · Entgelte · Pendler
BA-JobbörseREST v6 · Stellenanzeigen
BBSR / INKARRaumbeobachtung, jährlich
Destatis / GENESISRegionaldatenbank
OffeneRegisterUnternehmensdaten
Indeed / Stepstonekommerzielle Portale
Alle Quellen mit Takt, Umfang und Beschaffungsweg
§ 02
Verzicht

Was wir abgeschafft haben —
und wie es zurückkäme

Jede Streichung ist ein Versprechen an den Betrieb und ein Risiko für die Zukunft. Deshalb ist keine davon endgültig: das Datenmodell trägt die Haken, an denen die Komponente wieder andockt — ohne Datenmigration.

Apache Airflow → APScheduler

Ein Job ist bei uns eine reine Funktion mit Ergebnisobjekt: keine interne Zustandshaltung, idempotent, einzeln auslösbar. Für 16 Läufe im Monatstakt braucht das keine DAG-Maschine mit eigener Datenbank und eigener Oberfläche.

Zurück, sobald Verzweigungen und Retry-Bäume nötig werden — die Jobs sind portierbar

Celery → ein Worker-Container

Ein Broker, ein Result-Backend, Worker-Pools und Flower für Aufgaben, die planbar sind und nicht auf Nutzerklicks reagieren. Redis blieb — als Cache, nicht als Warteschlange.

Zurück bei nutzerausgelösten Langläufern; Report-Jobs haben bereits eine Status-Tabelle

Kafka → Postgres

Es gibt keinen Ereignisstrom. Die Quellen veröffentlichen monatlich, quartalsweise, jährlich — als Datei. Ein Streaming-Bus hätte nichts zu transportieren.

Zurück bei Echtzeit-Zuflüssen; Rohtabellen tragen bereits eine JSONB-Nutzlast

Elasticsearch → Postgres-Volltext

Ein zweiter Index will befüllt, synchronisiert und überwacht werden. Für Regions- und Berufssuche über einige Tausend Einträge reicht Postgres mit Trigram-Index.

Der Sync-Job existiert und schaltet sich selbst ein, sobald eine Suchindex-Adresse gesetzt ist

Metabase → Berichte im Produkt

Self-Service-BI beantwortet die Frage „welche Kennzahlen sehe ich diesen Monat?“ nicht besser als eine Berichtsdefinition, die der Kunde selbst zusammenstellt — und die mandantensicher ist.

Zurück für interne Analysen; die Datenbank ist ohnehin die einzige Quelle

Scrapy & Playwright → offizielle Schnittstellen

Portale zu crawlen ist rechtlich heikel und technisch brüchig. Die BA-Jobbörse hat eine dokumentierte Schnittstelle mit nativem Umkreis-Parameter — das ist der belastbarere Weg.

Bleibt draußen. Abdeckungslücken werden über kommerzielle Anbieter geschlossen, nicht über Crawler
Der eigentliche Wert steckt im Datenmodell, nicht im Container

Kennzahlen liegen im Long-Format: eine Zeile je Messwert, mit Gebiet, Periode, Schlüssel und Berechnungsversion. Eine neue Kennzahl ist damit ein neuer Schlüsselwert, keine Schema-Änderung. Ändert sich die Berechnungsmethode, laufen alte und neue Werte nebeneinander — historische Berichte bleiben reproduzierbar. Rohdaten behalten die vollständige Original-Antwort als JSONB; ein neues Feld der Quelle kostet keine Migration.

§ 03
Betrieb

Fünf Fehler,
die wir nur einmal gemacht haben

Jeder davon hat Stunden gekostet und steht heute als Regel im Code oder in der Dokumentation. Wir führen sie hier auf, weil eine Architektur nicht daran gemessen wird, was sie verspricht, sondern daran, was sie überlebt.

01

Das Datenbank-Volume hing am falschen Pfad

Das verwendete Postgres-Abbild legt seine Daten nicht dorthin, wo das offizielle Abbild sie ablegt. Das Volume zeigte ins Leere, die Daten lagen in der Container-Schicht — und wurden bei jedem Deploy gelöscht. Neustarts überlebten sie, was den Fehler monatelang kaschiert hat.

Bei jedem Wechsel des Datenbank-Abbilds den Datenpfad des neuen Abbilds prüfen. Den Mount nie „zurück korrigieren“.

02

Alle Container gesund — und trotzdem 504

API und Frontend hängen an mehreren Docker-Netzen. Ohne ausdrückliche Angabe wählt der Proxy das Zielnetz zufällig. Erwischt er eines ohne Proxy-Anbindung, läuft jede Anfrage der Domain in einen Zeitfehler, während die Container direkt einwandfrei antworten. Der Ausfall erscheint nach einem Neuaufbau „aus dem Nichts“.

Das Proxy-Netz je Dienst festnageln. Diagnose in dieser Reihenfolge: Container gesund → Direktabfrage im Container → Labels prüfen → Proxy-Log.

03

Zwei Migrationen mit derselben Nummer

Die laufende Nummer ist ein Nadelöhr, sobald zwei Menschen gleichzeitig arbeiten: beide greifen zur nächsten freien Zahl, nur eine läuft. Die andere bleibt liegen, obwohl sie im Repository steht — und eine Spalte fehlt, die der Code bereits abfragt.

Dateinamen tragen einen Zeitstempel statt einer Nummer. Eine doppelte Version bricht den Start ab und nennt beide Dateien, statt still eine zu überspringen.

04

Das Backup meldete „ok“ und schrieb nichts

Zwei Fehler, die sich gegenseitig verdeckten: das Backup-Verzeichnis gehörte root, der Datenbanknutzer durfte nicht hineinschreiben — und der Fehlerabbruch galt innerhalb der aufgerufenen Funktion nicht, sodass die Erfolgsmeldung nach dem gescheiterten Schritt trotzdem lief.

Jeder Schritt wird einzeln geprüft, ein leerer Dump zählt als Fehler. Und: Fehlerabbruch gilt in einer aufgerufenen Funktion nicht — das betrifft jeden solchen Wrapper.

05

Ein Schrägstrich zu viel — und alle Nutzer waren abgemeldet

Der Proxy entfernt den abschließenden Schrägstrich, die API antwortet mit einer Umleitung auf ihre interne Adresse. Der Browser folgt ihr über die Ursprungsgrenze hinweg und verwirft dabei den Berechtigungs-Header. Ergebnis: Zugriff verweigert, Zwangsabmeldung — ausgelöst vom Öffnen der Karte.

Client-Pfade nie mit abschließendem Schrägstrich; Router-Routen als leerer Pfad registrieren.

§ 04
Grenzen

Alles, was von selbst wächst,
hat eine Grenze

Drei Dinge wachsen im Betrieb, ohne dass jemand etwas tut. Jedes davon ist gedeckelt — zwei davon waren es bis August 2026 nicht.

Was wächstGrenzeWarum genau diese
Container-Protokolle20 MB × 5 je DienstDocker rotiert von sich aus nicht. Im Normalbetrieb sind es wenige MB am Tag — das Problem ist der Rand: eine Neustartschleife schreibt Gigabyte pro Stunde.
Datenbank-Sicherungen14 TageTäglich um 03:15, außerhalb des Docker-Volumes auf dem Host — bewusst getrennt vom Datenpfad, der den Fehler aus Lehre 01 verursacht hat.
Hochgeladene Importdateien7 Tage nach AbschlussEin INKAR- oder SDI-Satz sind bis zu 60 Dateien à 25 MB. Aufgeräumt wird beim Anlegen des nächsten Auftrags; laufende bleiben unangetastet.
Gemeinsamer Speicher der Datenbank1 GBFest gesetzt, weil der Standardwert für die Aggregations-Abfragen über 2,3 Mio. Zeilen zu klein war.
Beobachtet wird das im Produkt, nicht im Terminal

Der Reiter Datenbetrieb → Lage zeigt Platte, gemeinsamen Speicher und Datenbankgröße laufend an und warnt unter 10 % frei, mit Alarm unter 5 %. Daneben steht der Systemstatus nach dem Modell Komponente × Prüfung × Stufe.

Kapazität ist auf Jahre kein Thema

Rund 2,3 Mio. Kennzahl-Zeilen wiegen etwa 1 GB. Die Beobachtungsschwelle liegt bei 5–10 Mio. Zeilen; dann kommen Zeitreihen-Kompression und die zwei teuren Abfragepfade dran. Ein Datenbankwechsel steht nicht zur Debatte.

§ 05
Ausfallsicherheit

Ehrlich zum Ist-Zustand,
gestuft zum Ziel

Heute läuft alles als eine Compose-Application auf einem Server. Das ist für den jetzigen Stand angemessen und wird hier nicht schöngeredet: Host und Sicherungen liegen auf derselben Platte. Der Ausbau ist in vier Stufen geplant, jede additiv und ohne Datenmigration.

als Nächstes

Stufe 0 · Gehärteter Einzel-Host

Datenverlust
≤ 15 min
Ausfallzeit
2–4 h
Mehrkosten
~5 €/Mon

Sicherungen ausgelagert, Transaktionsprotokoll fortlaufend archiviert. Schließt das Katastrophenrisiko.

Stufe 1 · Warmer Zweitserver

Datenverlust
≤ 5 min
Ausfallzeit
15–60 min
Mehrkosten
~30–60 €/Mon

Vorgesehen vor dem ersten zahlenden Kunden. Umschaltung von Hand.

Stufe 2 · Automatische Umschaltung

Datenverlust
nahe 0
Ausfallzeit
1–5 min
Mehrkosten
~100–150 €/Mon

Erst bei zugesagter Verfügbarkeit, etwa 99,9 %.

Stufe 3 · Aktiv/Aktiv

Datenverlust
0
Ausfallzeit
Sekunden
Mehrkosten
200 €+/Mon

Auf absehbare Zeit überdimensioniert — genau die Falle, aus der wir kommen.

Abgestufter Rückfall statt Totalausfall

Cache weg → langsamer, nicht kaputt. Worker weg → Karte und Auswertungen laufen weiter, ein Hinweis meldet die pausierte Aktualisierung. Geocoder weg → Rückfall auf die Postleitzahl-Auflösung.

Idempotenz ist Pflicht, nicht Kür

Umschaltung heißt: Läufe passieren doppelt oder brechen mittendrin ab. Migrationen, Importe und Kennzahl-Berechnung sind darauf ausgelegt — für jeden neuen Job gilt dieselbe Anforderung.

Eine nie zurückgespielte Sicherung existiert nicht

Vierteljährlicher Rückspiel-Test auf einer Wegwerf-Instanz, mit Stoppuhr und Protokoll. Erst damit sind die Zielwerte oben Tatsachen statt Hoffnung.

Weiter

Was auf dieser Architektur läuft