# Deployment: Testserver via Coolify (intern) > Zielumgebung: interner Coolify-Host (`192.168.1.207`), Docker-Compose-Stack, > HTTP über eine `*.sslip.io`-Domain (kein öffentliches TLS). > Ergänzt `docs/HANDOVER-DEVOPS.md`. Deploy-Datei: `docker-compose.coolify.yml`. ## Architekturüberblick (Coolify) - Coolify deployt den **Compose-Stack** aus diesem Repo und übernimmt **Reverse-Proxy + Routing** — die App wird **nicht** per Host-Port exponiert. - Services: `migrate` (Init-Job) → `app` (Next.js standalone) + `postgres` (pgvector), `redis`, `garage` (Objektspeicher) + `garage-provision` (Init-Job). - **Migrationen** laufen als eigener Init-Job `migrate` (`prisma migrate deploy`, schlanke **`migrate`-Stage** ohne `next build`) **vor** `app` — der App-Container migriert selbst nicht. - **Objektspeicher Garage** (ersetzt MinIO, Community EOL): S3-kompatibel, Single-Node. Buckets/Keys legt NICHT die S3-API an, sondern der Init-Job `garage-provision` (Admin-API, idempotent) **vor** `app`. Details/Cutover: eigener Abschnitt unten. - **Redis** läuft mit `requirepass` (F-18): `REDIS_PASSWORD` als Coolify-Env setzen und in die `REDIS_URL` einsetzen (`redis://:@redis:6379`). - **Netzsegmentierung** (F-18): `postgres`/`redis`/`garage`/`migrate` liegen im internen Netz (`internal: true`, kein Egress); nur `app` ist zusätzlich im default-Netz (Coolify-Proxy). Garage wird nicht per Traefik/Host-Port exponiert. - **Secrets/Config** kommen aus den **Coolify-Env-Variablen** (Referenz: `.env.coolify.example`), nicht aus einer committeten `.env`. ## 1. Gitea mit Coolify verbinden (Deploy Key) SSH ist erreichbar (`git@…sslip.io:22`), daher Deploy-Key: 1. **Coolify → Keys & Tokens → Private Keys**: SSH-Key generieren (oder beim Anlegen der Ressource „Private Repository (deploy key)“ automatisch). Öffentlichen Key kopieren. 2. **Gitea → Repo `msolarczek/ISMS-Tool` → Settings → Deploy Keys → Add Deploy Key**: Key einfügen, nur Lesezugriff. 3. Repo-URL in Coolify (SSH): `git@gitea-vkbhbn2qdkz5ppk9q4qgb0tn.192.168.1.207.sslip.io:msolarczek/ISMS-Tool.git` ## 2. Ressource anlegen 1. Projekt/Environment wählen → **+ New → Private Repository (deploy key)**. 2. Repo `ISMS-Tool`, **Branch `devops/coolify-testserver`** (nach erfolgreichem Test → `main`). 3. **Build Pack: Docker Compose**, **Compose Location: `docker-compose.coolify.yml`**. ## 3. Environment-Variablen Werte aus `.env.coolify.example` in Coolify eintragen. Kritisch: - Hostnamen = Compose-Service-Namen: `postgres`, `redis`, `garage` (nicht `localhost`); `S3_ENDPOINT=http://garage:3900`. - `AUTH_SECRET` frisch: `openssl rand -base64 32`. - `AUTH_URL` = exakt die in Schritt 4 vergebene App-Domain (`http://…`). - **Garage** (siehe Abschnitt unten): `GARAGE_RPC_SECRET` + `GARAGE_ADMIN_TOKEN` (je `openssl rand -hex 32`), sowie `S3_ACCESS_KEY` = `GK`+24 Hex (`echo "GK$(openssl rand -hex 12)"`) und `S3_SECRET_KEY` = `openssl rand -hex 32`. Alle **literal** setzen (nicht via `${…}` — Coolify-Interpolationsfalle). ## 4. Domain & HTTP - Beim Service `app` unter **Domains** die `*.sslip.io`-Domain übernehmen, Schema **`http://`**. - Dieselbe Domain als `AUTH_URL` setzen (sonst schlägt der NextAuth-Login fehl). ## 5. Persistenz Named Volumes müssen als Persistent Storage erkannt sein — v. a. **`pgdata`** (sonst Datenverlust bei Redeploy) und **`garage_meta`** (Bucket-/Key-/Layout-Definitionen — ohne meta sind die Objektdaten in `garage_data` nicht adressierbar). Ebenso `garage_data`, `redisdata`, `backups`. **`garage_meta` in die Host-Backup-Strategie aufnehmen** (analog `pgdata`). ## 6. Deploy **Deploy** starten. Reihenfolge: `postgres` (healthy) → `migrate` (läuft einmalig durch) → `app` (startet erst nach erfolgreichem Migrate). Logs von `migrate`/`app` bei Fehlern prüfen. ## 7. Demo-Admin anlegen (einmalig, Testserver) Der Demo-Seed läuft nicht automatisch. Im Coolify-**Terminal** des `migrate`-Containers (builder-Image, hat `tsx`): ``` npx tsx prisma/seed.ts ``` Login danach: `admin@demo.example` / `Demo1234!`. > Produktiv: **kein** Demo-Seed. Stattdessen einmaliger Bootstrap des Plattform-Admins > (`provisionTenant({ admin.isPlatformAdmin: true })`) — Skript noch zu erstellen > (siehe `docs/HANDOVER-DEVOPS.md` §4). ## 8. Smoke-Test Über die App-Domain: Login, Admin-Konsole, eine Modul-Seite. ## Redeploy / Migrationen-Nachziehen Jeder Coolify-Redeploy baut neu und lässt `migrate` erneut laufen (`migrate deploy` ist idempotent — nur neue Migrationen werden angewandt). App startet erst nach erfolgreichem Migrate. ## Von Test zu Produktiv (Delta) - `AUTH_SECRET`, DB-Passwörter und die **Garage-Secrets** (`GARAGE_RPC_SECRET`, `GARAGE_ADMIN_TOKEN`, `S3_ACCESS_KEY`/`S3_SECRET_KEY`) neu/aus Secret-Store; eigene Domain + gültiges TLS. - **Kein** Demo-Seed; Erst-Superadmin per Bootstrap-Skript. - Backups (`pg_dump`/Volume-Snapshots inkl. **`garage_meta`**) + Restore-Test, Monitoring/Alerting. - Ggf. ≥ 2 `app`-Replicas hinter dem Coolify-Proxy. ## Objektspeicher (Garage) — Cutover, Provisioning & Rollback Ablösung von MinIO (Community EOL) durch **Garage** (S3-kompatibel, aktiv gepflegt, Single-Node). Konzept: `docs/KONZEPT-garage-migration.md`. Es gibt **keine produktiven Daten** → **Neu-Deploy statt Datenmigration** (kein rclone, kein Wartungsfenster). **Konfig-Bausteine** - `deploy/garage.toml` (eingecheckt, **secret-frei**): `replication_factor=1`, `s3_region=us-east-1`, S3-API `:3900`, Admin-API `:3903`. `rpc_secret`/`admin_token` liest der Daemon aus der Env (`GARAGE_RPC_SECRET`/`GARAGE_ADMIN_TOKEN`). - Volumes: `garage_meta` (**kritisch**, ins Host-Backup) + `garage_data`. - Init-Job `garage-provision` (`scripts/garage-provision.ts`, Admin-API, **idempotent**): Layout → Bucket `isms-documents` → Access-Key **importieren** (aus `S3_ACCESS_KEY/…`) → Rechte read/write. Läuft bei jedem Deploy; „already exists“ = Erfolg. Ohne `GARAGE_ADMIN_TOKEN` No-op. Loggt **keine** Secrets. **Cutover-Runbook (Test-Instanz)** 1. Compose enthält bereits `garage` + `garage-provision`; der alte `minio`-Service ist auskommentiert (Rollback-Netz) und wird erst nach Abnahme entfernt. 2. Coolify-Env **literal** setzen (nicht `${…}`): `S3_ENDPOINT=http://garage:3900`, `S3_REGION=us-east-1`, `S3_BUCKET=isms-documents`, `GARAGE_RPC_SECRET`, `GARAGE_ADMIN_TOKEN`, `S3_ACCESS_KEY` (`GK`+24 Hex), `S3_SECRET_KEY` (64 Hex). 3. Deploy. Reihenfolge: `garage` (healthy: `/garage status`) → `garage-provision` (legt Layout/Bucket/Key/Rechte an) → `app`. Optionaler DB-Reset nur, falls alte Objekt-Referenzen stören (bei frischer/geseedeter Instanz unnötig). 4. Abnahme (§ KONZEPT §9): Upload → Download (`/files/[...key]`) → Backup-Export `.cvb` + Download → DSGVO-ZIP → Restore mit Mandanten-Isolation → Backup-Historie List/ Aufräumen → Negativfall „fehlender Bucket“ = sprechender Fehler. 5. **`garage_meta`** in der Host-Backup-Strategie bestätigen. Erst danach den `minio`-Block **und** das Volume `miniodata` aus `docker-compose.coolify.yml` entfernen. **Rollback** (unkritisch, keine Prod-Daten): `garage`/`garage-provision` wieder aus-, den auskommentierten `minio`-Block wieder einkommentieren und die alten `S3_*`/ `MINIO_*`-Env setzen; neu deployen. Solange `minio` + `miniodata` noch existieren, ist das ein reiner Compose-/Env-Wechsel. **Provisioning erneut anstoßen / debuggen**: Im Coolify-Terminal eines Containers mit `tsx` (`migrate`-Image) `npx tsx scripts/garage-provision.ts` erneut laufen lassen (idempotent). „Container weg ohne Logs“ (Coolify entfernt fehlgeschlagene Container sofort): Provisioning-Ausgabe zusätzlich in eine Datei spiegeln, z. B. `npx tsx scripts/garage-provision.ts 2>&1 | tee /tmp/garage-provision.log`.