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:
@@ -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.)*
|
||||
Reference in New Issue
Block a user