L10b Betrieb & Aufräumen: Deploy – craftvia-worker, CI-Testjob, DEPLOY.md, Certvia-Doku archiviert

- docker-compose.coolify(.prebuilt).yml: Service craftvia-worker (Target worker, Chromium,
  shm_size 1gb, gleiche Härtung), Craftvia-Variablen für app und worker.
- Dockerfile: worker-Stage mit HOME=/home/app (Chromium-Profil für non-root); lokaler
  docker build der Targets runner und worker erfolgreich, PDF-Erzeugung im Image geprüft.
- .env.example/.env.prod.example/.env.coolify.example: alle Craftvia-Variablen inkl. RLS,
  KI-Provider, PDF_CHROMIUM_PATH, OFFLINE_MAX_DAYS, API_RATE_LIMIT_*, AI_GENERATION_RETENTION_DAYS,
  AI_MONTHLY_TOKEN_LIMIT.
- CI (.github, .gitea): Job gate mit Postgres (pgvector) und Redis als Service: migrate deploy,
  seed, Passwort für craftvia_app, tsc, lint, build, npm run test.
- docs/craftvia/DEPLOY.md (aus den Certvia-Deploy-Docs abgeleitet): Architektur, Domains, Secrets,
  Worker, Migrationen, RLS-Aktivierung, Backup/Restore, KI, Rate Limits, Aufbewahrung, Smoke,
  Update/Rollback. build-and-push-images.sh baut craftvia-worker.
- Certvia-/ISMS-Dokumente aus docs/ nach docs/_certvia-archiv/ (mit README); Verweise in README.md
  und Skript-Kommentaren angepasst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 18:19:19 +02:00
co-authored by Claude Opus 5
parent 21d6dc016a
commit cadaedc6cc
38 changed files with 902 additions and 76 deletions
@@ -0,0 +1,217 @@
# KONZEPT: Objektspeicher-Migration MinIO → Garage
**Stand:** 2026-08-20 · **Zielgruppe:** mehrköpfiges Entwicklerteam + PM · **Status:** Entwurf zur Abnahme
> **Randbedingung (2026-08-20):** Es existieren **nur Test-Instanzen** — **keine produktiven Daten**. Daher **keine Datenmigration** (kein rclone-Sync), sondern ein **kompletter Neu-Deploy** mit frischer Garage. Das eliminiert die frühere Migrations-Lane und das Wartungsfenster; Cutover = MinIO-Service durch Garage ersetzen, provisionieren, neu deployen (optional DB-Reset wie gehabt).
---
## 1. Ausgangslage & Motivation
certvia nutzt aktuell **MinIO** als S3-kompatiblen Objektspeicher (Dokument-Uploads + Backup-Artefakte). Anlass für die Prüfung: der Hinweis, MinIO werde „nicht mehr weiterentwickelt".
**Faktenlage (verifiziert 2026-08-20):**
- **Mai 2025** — MinIO entfernt die Admin-Konsole/Verwaltungs-GUI aus der Community Edition (nur noch rudimentärer Object-Browser; Bucket-/User-/Policy-Verwaltung nur noch im kommerziellen AIStor).
- **Okt 2025** — MinIO publiziert keine Container-Images mehr auf Docker Hub/Quay (auch nicht für einen kritischen CVE-Fix).
- **Dez 2025** — Community-Repo im **Maintenance-Mode**: keine neuen Features, keine PRs, Security-Fixes nur „case-by-case"; Repo als **„no longer maintained"** markiert. Fokus liegt auf **AIStor** (Abo-Produkt). Der Code bleibt AGPLv3 verfügbar.
**Bewertung:** MinIO ist nicht „von heute auf morgen tot", aber die **Community Edition ist faktisch im End-of-Life-/Wartungsmodus**. Für ein selbstgehostetes ISMS-/Compliance-Produkt (das genau Betriebs-, Wartungs- und Supply-Chain-Sicherheit demonstrieren soll) ist der proaktive Wechsel auf einen aktiv gepflegten Store gerechtfertigt und gut begründbar (u. a. relevant für die eigene Lieferanten-/Komponentenbewertung).
**Warum Garage:** aktiv entwickelt (Deuxfleurs, AGPLv3, Rust), bewusst **minimalistisch & leichtgewichtig**, S3-kompatibel, für Selbsthosting/kleine bis mittlere Deployments und geo-verteilte Replikation ausgelegt. Passt zum internen Coolify-Testserver **und** zum Contabo-Prod-VPS.
---
## 2. Zielbild
Ein **API-kompatibler Austausch** des Storage-Backends: Der Anwendungscode spricht weiterhin S3 (AWS SDK v3, `forcePathStyle`), lediglich der Container-Dienst, die Provisionierung von Bucket/Key und die Daten werden migriert. **Keine Änderung an Fachlogik, UI oder Datenmodell.**
Was gleich bleibt:
- S3-Protokoll, AWS SDK v3, `forcePathStyle: true`, die Env-Kontrakte `S3_ENDPOINT / S3_ACCESS_KEY / S3_SECRET_KEY / S3_BUCKET / S3_REGION`.
- Die Storage-Abstraktionen `src/server/storage/adapter.ts` (Uploads) und `src/server/storage/backup-store.ts` (Backups) inkl. Local-/Stub-Fallback.
- Key-Schema (`<tenantId>/uploads/<uuid>-<name>`), Mandanten-Isolation über Key-Präfix.
---
## 3. Ist-Analyse certvia (Code-Stand dev)
Zwei S3-Konsumenten, beide identischer S3-Dialekt:
| Konsument | Datei | Zweck | Ops |
|-----------|-------|-------|-----|
| Upload-Storage | `src/server/storage/adapter.ts` | hochgeladene Richtlinien + Audit-Nachweise | Put/Get, HeadBucket→CreateBucket |
| Backup-Store | `src/server/storage/backup-store.ts` | verschlüsselte Backup-Artefakte (`.cvb`), DSGVO-ZIP | Put/Get/List/Delete, HeadBucket→CreateBucket |
Gemeinsam:
- `new S3Client({ endpoint, region, forcePathStyle: true, credentials })` — **path-style**, exakt was Garage erwartet.
- Verwendete S3-Operationen: `PutObject`, `GetObject`, `HeadBucket`, `CreateBucket`, `ListObjectsV2`, `DeleteObjects`. **Alle außer `CreateBucket` sind Garage-Standard.**
- Region-Default `us-east-1`.
- Backend-Auswahl rein über Env → sauberer Graceful-Fallback (Stub/Local), kein Hardcoding auf MinIO.
**Zu provisionierende Buckets (Neu-Deploy, keine Altbestände zu übernehmen):**
- `S3_BUCKET` = `isms-documents` (Uploads).
- Backup-Bucket: nur falls der Backup-Store auf S3 statt lokal (`BACKUP_LOCAL_DIR`) betrieben wird — dann eigenen Bucket provisionieren.
### Der einzige echte Reibungspunkt: Bucket-/Key-Anlage
Garage verwaltet **Buckets und Access-Keys sowie deren Rechte** über die **Garage-Admin-API / `garage`-CLI**, **nicht** über die S3-Operation `CreateBucket`. Konsequenz:
- `HeadBucket`, `PutObject`, `GetObject`, `ListObjectsV2`, `DeleteObjects` → funktionieren gegen Garage unverändert.
- `CreateBucket` (unsere Selbstheilung „Bucket fehlt → anlegen") → **wird von Garage über die S3-API nicht bedient**. Buckets/Keys müssen **vorab out-of-band** provisioniert werden.
> Hinweis: `backup-store.ts` **schluckt** einen `CreateBucket`-Fehler bereits (Bucket gilt als „vorhanden angenommen"), `adapter.ts` **wirft** dagegen bei nicht-„already exists"-Fehlern. Nach Vorab-Provisionierung greift ohnehin nur der `HeadBucket`-Erfolgspfad — trotzdem soll `ensureBucket()` explizit Garage-tauglich gemacht werden (siehe Lane C), damit wir uns nicht auf verschlucktes Fehlerverhalten verlassen.
---
## 4. Entscheidungen (D) — vom Team/PO zu bestätigen
| # | Entscheidung | Empfehlung | Begründung |
|---|--------------|-----------|------------|
| **D1** | Zielspeicher | **Garage, self-hosted — ENTSCHIEDEN 2026-08-20** | Aktiv gepflegt, S3-kompatibel, leichtgewichtig, DSGVO/Datenresidenz in eigener Hand. Alternativen (SeaweedFS, Ceph/RGW, extern R2/Hetzner) in §12 abgewogen. |
| **D2** | Region-Handhabung | Garage-`s3_region` = **`us-east-1`** setzen | App-Env bleibt unverändert (Default `us-east-1`) → keine Code-/Config-Drift. |
| **D3** | `ensureBucket()` | **Vorab-Provisionierung** + `ensureBucket` prüft nur (HeadBucket), kein S3-CreateBucket | Deterministisch; klare Fehlermeldung „Bucket nicht provisioniert" statt stiller Selbstheilung. |
| **D4** | Cutover-Strategie | **Neu-Deploy, kein Wartungsfenster** (MinIO→Garage im Compose ersetzen, provisionieren, deployen; optional DB-Reset) | **Keine produktiven Daten** → keine rclone-Migration nötig. |
| **D5** | Topologie | **Single-Node** Garage pro Environment | Passt zur aktuellen 1-Host-Topologie; Multi-Node/Replikation als spätere Ausbaustufe dokumentieren. |
| **D6** | Deployment | Garage als **Compose-Service** in `docker-compose.coolify.yml` (ersetzt `minio`) | Gleiches Betriebsmodell wie bisher (Coolify/Traefik). |
| **D7** | Reihenfolge | Aktuell nur **Test-Instanz(en)** — dort umsetzen; Prod später nach gleichem Muster | Es gibt derzeit keine Prod-Instanz; Runbook bleibt für spätere Prod gültig. |
Strategische Vorfrage (self-hosted vs. externer managed S3): **entschieden am 2026-08-20 zugunsten self-hosted Garage** — Datenresidenz + Betriebshoheit für ein DSGVO-/ISMS-Produkt. Externe Optionen (Hetzner OS/Cloudflare R2) bleiben nur als dokumentierte Alternative in §12.
---
## 5. Zielarchitektur
```
┌───────────────────────── Coolify-Stack (pro Environment) ─────────────────────────┐
app ──S3──► │ garage (Container) │
worker ─S3► │ ├─ garage.toml (rpc_secret, s3_api.s3_region=us-east-1, api_bind_addr :3900, │
backup- ─S3► │ │ admin_token, metadata_dir, data_dir) │
worker │ ├─ Volume: garage_meta → /var/lib/garage/meta (KRITISCH: Metadaten/Layout) │
│ └─ Volume: garage_data → /var/lib/garage/data (Objekt-Bytes) │
│ garage-provision (Init-Job, restart:no): Layout + Bucket + Key + Rechte via CLI │
└────────────────────────────────────────────────────────────────────────────────────┘
```
- **Endpoint:** `S3_ENDPOINT=http://garage:3900` (S3-API-Port; Standard 3900). Admin-API auf 3903 (nur intern).
- **Zwei Ports beachten:** 3900 = S3, 3902 = Web (optional), 3903 = Admin. Nur S3 wird von der App genutzt; Admin bleibt clusterintern.
- **Secrets (pro Environment via Coolify-Env, echte Werte NIE im Chat/Repo):** `GARAGE_RPC_SECRET` (32-byte hex), `GARAGE_ADMIN_TOKEN`, plus der erzeugte Access-Key/Secret, die als `S3_ACCESS_KEY/S3_SECRET_KEY` in die App gehen.
- **Backup der Garage-Metadaten:** `garage_meta` enthält Bucket-/Key-/Layout-Definitionen — muss in die Host-Backup-Strategie (analog `pgdata`). Ohne Meta sind die Daten nicht adressierbar.
---
## 6. Workstreams / Lanes für das Team
Vier parallelisierbare Lanes + PM-Koordination (die frühere rclone-Migrations-Lane entfällt, da Neu-Deploy). Abhängigkeiten in Klammern.
### Lane A — Infra & Deployment (Owner: DevOps)
- Garage-Service in `docker-compose.coolify.yml` ergänzen (Image pinnen, Ports, 2 Volumes, `security_opt`/`cap_drop` analog bestehender Services, Healthcheck auf Admin-API `/health`).
- `garage.toml` als Config (über Env/Coolify-Mount): `rpc_secret`, `s3_region=us-east-1`, `metadata_dir`, `data_dir`, `admin_token`.
- Single-Node-Layout initial (Node einer Zone mit Kapazität zuweisen — ohne Layout kein Schreibzugriff).
- `minio`-Service erst **nach** abgenommenem Garage-Betrieb entfernen (bis dahin als Rollback-Sicherheitsnetz stehen lassen).
- Garage-`meta`-Volume in Host-Backup aufnehmen.
- **Liefergegenstand:** lauffähiger Garage-Container auf der Test-Instanz, Admin-API erreichbar, Layout „ready".
### Lane B — Provisioning-Automatisierung (Owner: DevOps/Backend) — *(braucht A)*
- Init-Job `garage-provision` (restart:no, analog `migrate`): idempotent
1. Layout anwenden (falls noch nicht),
2. Bucket(s) anlegen (`isms-documents`, ggf. Backup-Bucket),
3. Access-Key erzeugen **oder** vorhandenen importieren,
4. Key→Bucket-Rechte (read/write/owner) setzen.
- Umsetzung über `garage`-CLI **oder** Admin-API (HTTP) — Entscheidung dokumentieren; idempotent (mehrfach ausführbar ohne Fehler).
- Ausgabe des Access-Key/Secret **nur** in Coolify-Secrets, nicht in Logs.
- **Liefergegenstand:** ein Skript/Job, der aus „leerer Garage" reproduzierbar den betriebsbereiten Zustand herstellt (dokumentiert in `docs/DEPLOY-*`).
### Lane C — App-Code-Anpassung (Owner: Backend) — *(unabhängig, klein)*
- `ensureBucket()` in **beiden** Stores Garage-tauglich: `HeadBucket` zur Verifikation; bei „missing" **kein** S3-`CreateBucket`, sondern klarer Konfigurationsfehler „Bucket nicht provisioniert — Provisioning-Job ausführen". (Provisionierung liegt bei Lane B.)
- Optional: gemeinsame S3-Client-Factory extrahieren (DRY über `adapter.ts`/`backup-store.ts`) — nur wenn ohne Risiko.
- Region/Endpoint-Doku in `.env.coolify.example` + `.env.example` aktualisieren (MinIO-Kommentare → Garage; `forcePathStyle`-Begründung bleibt gültig).
- **Tests:** bestehende `scripts/test-backup-*.ts` + Upload/Download-Pfad gegen einen lokalen Garage-Container grün; neuer Smoke-Test „Bucket fehlt → sprechender Fehler".
- **Liefergegenstand:** PR mit Code + aktualisierten Tests, `tsc/lint/build` grün.
### Lane D — Test, Cutover & Abnahme (Owner: QA/DevOps + PM) — *(integriert A–C)*
- End-to-End-Durchstich auf der **Test-Instanz** (Neu-Deploy): Upload, Download, Backup-Export (`.cvb`), DSGVO-ZIP, Restore, Mandanten-Isolation.
- **Cutover-Runbook** (siehe §7) an der Test-Instanz **einmal durchspielen** (MinIO raus, Garage rein, provisionieren, deployen, ggf. DB-Reset).
- **Rollback-Runbook** (siehe §8).
- Abnahmekriterien (§9) abhaken.
- **Liefergegenstand:** abgenommene Test-Instanz auf Garage + freigegebenes Runbook (auch für spätere Prod).
**PM:** Reihenfolge/Abhängigkeiten (A→B, C parallel, D integriert), Abnahme je Lane, Freigabe des Runbooks für spätere Prod (D7).
---
## 7. Cutover-Runbook (Neu-Deploy, kein Wartungsfenster nötig)
1. Im Compose `minio`-Service durch `garage` + `garage-provision` (Init-Job) ersetzen; Volumes `garage_meta`/`garage_data` anlegen.
2. Coolify-Env setzen: `S3_ENDPOINT=http://garage:3900`, `S3_REGION=us-east-1`, `GARAGE_RPC_SECRET`, `GARAGE_ADMIN_TOKEN` (literal, echte Werte NICHT aus dem Chat). `S3_ACCESS_KEY/S3_SECRET_KEY` = der beim Provisioning erzeugte Garage-Key.
3. Deploy: Garage startet, Layout „ready", `garage-provision` legt Bucket(s) + Key + Rechte an.
4. Optional **DB-Reset** (wie in bisherigen Deploys), falls alte Objekt-Referenzen im Datenbestand stören — bei frischer/geseedeter Test-Instanz meist unnötig.
5. **Smoke-Tests** (§9) aktiv durchführen.
6. `minio`-Service + Volumes entfernen; Garage-`meta`-Volume in Host-Backup bestätigen.
## 8. Rollback
- Da es **keine produktiven Daten** gibt, ist Rollback unkritisch: Compose zurück auf `minio` (oder frischer Re-Deploy). Optional den alten `minio`-Service während der ersten Testphase noch nicht löschen (Schritt 6 verzögern), bis Garage abgenommen ist.
---
## 9. Validierung / Abnahmekriterien
Gegen Garage müssen grün sein:
- **Upload:** Richtlinie hochladen → Objekt in Garage, `<tenantId>/uploads/...`-Präfix korrekt.
- **Download:** Datei abrufen (`/files/[...key]`), Content-Disposition/Filename-Metadatum stimmt.
- **Backup-Export:** `.cvb`-Artefakt erzeugt (CVB1-Header), Persistenz + Download.
- **DSGVO-ZIP:** erzeugt und lesbar.
- **Restore:** Backup → Wiederherstellung, Datenintegrität, **Mandanten-Isolation** gewahrt.
- **List/Delete:** Backup-Historie listet, Aufräumen entfernt Prefix.
- **Fehlerfall:** fehlender Bucket → sprechender Konfigfehler (kein stiller CreateBucket-Versuch).
- **Automatisiert:** `scripts/test-backup-*.ts` grün gegen Garage; `tsc/lint/build` grün.
---
## 10. Risiken & Gegenmaßnahmen
| Risiko | Gegenmaßnahme |
|--------|---------------|
| `CreateBucket` (S3) gegen Garage nicht verfügbar | Vorab-Provisionierung (Lane B) + `ensureBucket` nur prüfend (Lane C). |
| Garage-`meta`-Volume nicht gesichert → Buckets/Keys „weg" | Meta-Volume in Host-Backup; im Runbook explizit verifiziert. |
| Region-/Endpoint-Mismatch (Coolify-Env vs. `garage.toml`) | D2 fixiert `us-east-1` beidseitig; in Abnahme geprüft. |
| Access-Key/Secret landet in Logs | Provisioning schreibt nur in Coolify-Secrets; Log-Redaction. |
| Layout nicht angewendet → Schreibfehler „no capacity" | Provisioning-Job setzt Layout idempotent; Healthcheck. |
| Coolify-Interpolations-/„managed"-Fallen (bekannt) | Neue Env (`GARAGE_*`) literal setzen, nicht über `${…}` referenzieren; im Container-Env verifizieren. |
| Presigned URLs / spezielle S3-Features | certvia nutzt aktuell keine Presigned-URLs (Downloads laufen serverseitig) — vor Ausbau prüfen. |
---
## 11. Grobe Aufwandsschätzung
| Lane | Aufwand (Personentage, grob) |
|------|------------------------------|
| A Infra/Deployment | 2–3 |
| B Provisioning | 2–3 |
| C App-Code | 1–2 |
| D Test/Cutover/Abnahme | 1–2 |
| PM/Koordination | durchgehend |
| **Summe** | **~6–10 PT** (ohne Datenmigration), gut parallelisierbar |
---
## 12. Alternativen (zur Vollständigkeit für D1)
| Option | Pro | Contra |
|--------|-----|--------|
| **Garage** (empfohlen) | aktiv, leicht, S3, self-hosted, DSGVO in eigener Hand | Bucket/Key via Admin-API (einmaliger Provisioning-Aufwand); kein Erasure-Coding (Replikation) |
| SeaweedFS | performant, S3-Gateway, aktiv | größerer Funktionsumfang/komplexer als nötig |
| Ceph/RGW | Enterprise-Standard, sehr robust | schwergewichtig, hoher Betriebsaufwand — für 1-Host überdimensioniert |
| Externer managed S3 (Hetzner OS / Cloudflare R2) | kein Storage-Ops, hohe Verfügbarkeit | Datenresidenz/Abhängigkeit extern; Kosten; DSGVO-AV nötig |
---
## 13. Quellen (MinIO-Status / Garage-Provisioning)
- MinIO users complain after admin UI removed from Community Edition — blocksandfiles.com (2025-06)
- MinIO Faces Fallout for Stripping Functions from Open Source Version — futuriom.com (2025-06)
- MinIO Ends Community Development, Positions AIStor as the Future — faun.dev / devopslinks
- MinIO in Maintenance Mode: Open Source Alternatives — bizety.com (2025-12)
- minio/minio Discussion #21326 „It's not a feature issue, it's a trust one" — github.com
- Garage — Administration API / Features / Bucket & Key Operations — garagehq.deuxfleurs.fr, deepwiki.com
*(Quellen-Status zeitkritisch; vor Projektstart kurz gegenprüfen.)*