# 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 (`/uploads/-`), 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, `/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.)*