# 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

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

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

```bash
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).
