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:
- Code (
app/) — wird bei jedem Deploy neu kopiert. - SQLite-Daten (
data/gwoe-antraege.db,data/embeddings.db) — Source of Truth, MUSS persistent über Deploys hinweg. - 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:
- User exportiert XML aus EDAS (manuell, im Browser).
cp dokumente_export.xml gwoe-antragspruefer:/app/data/sn-edas/scpaus dem Container ins Host-Volume — kein Container-Restart nötig.- 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¶
.dockerignoreist 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).