- 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>
124 lines
7.7 KiB
Markdown
124 lines
7.7 KiB
Markdown
# 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://:<pw>@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`.
|