Zum Inhalt

0004 — Docker Compose Deploy mit DB-/Reports-Volume und SN-XML-Sonderpfad

Status accepted
Datum 2026-04-10
Refs CLAUDE.md "Deployment", docker-compose.yml, Issue #5, project_sn_xml_export

Kontext

Der GWÖ-Antragsprüfer läuft als Docker-Container gwoe-antragspruefer auf einem VServer hinter Traefik (Let's Encrypt SSL, Domain gwoe.toppyr.de). Code wird via Git aus repo.toppyr.de gezogen.

Drei Subsysteme haben unterschiedliche Lebenszyklen:

  1. Code (app/) — wird bei jedem Deploy neu kopiert.
  2. SQLite-Daten (data/gwoe-antraege.db, data/embeddings.db) — Source of Truth, MUSS persistent über Deploys hinweg.
  3. PDF-Reports (reports/*.pdf) — sind generierte Artefakte, könnten theoretisch regeneriert werden, sind aber teuer (LLM-Calls + WeasyPrint), also auch persistent.

Issue #5 hatte einen frühen Schmerzpunkt: der Container-Build hat die DB überschrieben, weil data/ nicht aus dem Build-Context exkludiert war.

Optionen

Option A — Alles im Image, manuelle Backups

Code, DB, Reports zusammen im Image. Backups via docker cp vor jedem Deploy.

Nachteile: Image wird gigantisch, jeder Deploy ist ein Risk-Event, kein automatischer Persistenz-Mechanismus.

Option B — Docker-Volumes für DB und Reports

docker-compose.yml mountet ./data und ./reports vom Host als Volumes. Build kopiert nur app/ und requirements.txt.

Vorteile: Host-Volumes überleben Container-Restart und Image-Rebuild. Backups via Standard-Linux-Tools auf dem Host.

Nachteile: Build-Context muss data/, reports/, .env exkludieren (siehe .dockerignore), sonst werden sie versehentlich ins Image kopiert und überschreiben das gemountete Volume bei Container-Start.

Option C — Externe DB (Postgres)

Postgres als separater Container, gwoe-antragspruefer verbindet per asyncpg.

Vorteile: standard, robust, Backups via pg_dump.

Nachteile: Migration aller existierenden Queries, neue Abhängigkeit, mehr Operational Surface. SQLite reicht für die aktuelle Last (~30 Anträge, selten parallele Writes).

Entscheidung

Option B. Docker-Compose mit Host-Volumes für data/ und reports/. .dockerignore exkludiert beide aus dem Build-Context.

Standard-Deploy

ssh vserver 'cd /opt/gwoe-antragspruefer && git pull && docker compose up -d --build'

Manueller Tar-Upload (falls Git-Workflow blockiert ist)

cd webapp
tar czf /tmp/gwoe-webapp.tar.gz \
  --exclude='venv' --exclude='__pycache__' \
  --exclude='data' --exclude='reports' --exclude='.env' .
scp /tmp/gwoe-webapp.tar.gz vserver:/tmp/
ssh vserver 'cd /opt/gwoe-antragspruefer && tar xzf /tmp/gwoe-webapp.tar.gz && docker compose up -d --build'

Beachte: --exclude='data' ist zwingend, sonst überschreibt der Tar die Live-DB.

SN-XML-Sonderpfad

Sachsen hat keinen scrape-baren Endpoint und liest stattdessen wöchentlich manuell exportierte XML-Dumps aus EDAS. Workflow:

  1. User exportiert XML aus EDAS (manuell, im Browser).
  2. cp dokumente_export.xml gwoe-antragspruefer:/app/data/sn-edas/
  3. scp aus dem Container ins Host-Volume — kein Container-Restart nötig.
  4. SNEdasXmlAdapter liest die XML beim nächsten Search-Call.

Details in ~/.claude/projects/<projekt>/memory/project_sn_xml_export.md.

Container-Zeitzone

Der Container läuft UTC, nicht CEST. DB-Timestamps in assessments.created_at sind UTC (kein TZ-Suffix, aber UTC). Beim Korrelieren mit Commit-Zeiten (lokal CEST = UTC+2) muss konvertiert werden, sonst fließen falsche Schlussfolgerungen ein. Detailliert in ~/.claude/projects/<projekt>/memory/reference_container_utc.md.

Konsequenzen

Positiv

  • DB überlebt jeden Deploy — verifiziert seit Issue #5 (Fix in .dockerignore-Update).
  • Backups sind trivial: tar czf gwoe-data-$(date +%F).tar.gz data/ auf dem Host.
  • Build ist schnell (~30s für nicht-cached Layers), weil das Image nur Code + Dependencies enthält.

Negativ

  • .dockerignore ist ein Foot-Gun — wenn jemand vergisst, data/ neu hinzuzufügen nach einem Refactor, kann es passieren dass der Build die Live-DB überschreibt. Mitigation: ein dedicated Sub-D-style Test, der nach dem Build prüft, dass die DB die erwarteten Tabellen hat — steht noch aus.
  • SN-XML braucht manuelle Pflege wöchentlich. Akzeptiert weil es kein scrape-baren Endpoint gibt.
  • SQLite skaliert nicht über parallele Writes — bei mehr als ~5 gleichzeitigen Analyses würde es Lock-Contention geben. Aktuell läuft alles seriell durch den Background-Task-Mechanismus, also kein Problem. Bei Wachstum auf >50 Analyses/Tag wäre ein Postgres-Migration ein separater ADR.

Folgen für andere ADRs

  • ADR 0002 (Adapter-Architektur) ist davon unabhängig — Adapter sind reine Code-Klassen ohne State.
  • Ein zukünftiger Postgres-Migration-ADR würde diesen ADR partial superseden (DB-Persistenz, nicht Reports).