From 81722f0cb4a2bfbcc9156e145337aac875ea1f6c Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 20 Jul 2026 11:41:24 +0200 Subject: [PATCH] DevOps: Coolify-Testserver-Deployment (Compose, Migrations-Job, Runbook) - docker-compose.coolify.yml: Coolify-taugliche Variante (kein host-port, Env via Coolify-Variablen, migrate-Init-Job vor app, app-Healthcheck) - .env.coolify.example: Referenz der Coolify-Env-Variablen (nur Platzhalter) - docs/DEPLOY-COOLIFY.md: Runbook (Gitea-Deploy-Key, Ressource, Env, Domain, Seed) - .gitignore: .env.coolify.example whitelisten Co-Authored-By: Claude Opus 4.8 --- .env.coolify.example | 39 ++++++++++++++++ .gitignore | 1 + docker-compose.coolify.yml | 95 ++++++++++++++++++++++++++++++++++++++ docs/DEPLOY-COOLIFY.md | 78 +++++++++++++++++++++++++++++++ 4 files changed, 213 insertions(+) create mode 100644 .env.coolify.example create mode 100644 docker-compose.coolify.yml create mode 100644 docs/DEPLOY-COOLIFY.md diff --git a/.env.coolify.example b/.env.coolify.example new file mode 100644 index 0000000..2906b14 --- /dev/null +++ b/.env.coolify.example @@ -0,0 +1,39 @@ +# Referenz für die Coolify-Environment-Variablen (Testserver, intern). +# ECHTE Secrets NUR in Coolify eintragen — diese Datei enthält nur Platzhalter. +# In Coolify: Ressource -> Environment Variables (Bulk-Paste möglich). +# Hostnamen sind die Compose-Service-Namen (postgres/redis/minio), NICHT localhost. + +# --- Datenbank (Service "postgres") --- +POSTGRES_USER=isms +POSTGRES_PASSWORD=CHANGE_ME_db_password +POSTGRES_DB=isms +DATABASE_URL=postgresql://isms:CHANGE_ME_db_password@postgres:5432/isms?schema=public + +# --- Redis (Service "redis") --- +REDIS_URL=redis://redis:6379 + +# --- Objektspeicher (Service "minio") --- +# S3_ACCESS_KEY == MINIO_ROOT_USER und S3_SECRET_KEY == MINIO_ROOT_PASSWORD! +S3_ENDPOINT=http://minio:9000 +S3_ACCESS_KEY=isms +S3_SECRET_KEY=CHANGE_ME_minio_password +S3_BUCKET=isms-documents +MINIO_ROOT_USER=isms +MINIO_ROOT_PASSWORD=CHANGE_ME_minio_password + +# --- Auth (NextAuth) --- +# AUTH_SECRET: openssl rand -base64 32 +# AUTH_URL: exakt die Coolify-Domain des app-Service (http:// für intern) +AUTH_SECRET=CHANGE_ME_openssl_rand_base64_32 +AUTH_URL=http://REPLACE-WITH-COOLIFY-SSLIP-DOMAIN + +# --- KI-Provider (optional, aktuell ungenutzt) --- +AI_PROVIDER=anthropic +AI_API_KEY= + +# --- E-Mail (optional, im Test ungenutzt) --- +SMTP_HOST= +SMTP_PORT=1025 +SMTP_USER= +SMTP_PASSWORD= +SMTP_FROM=isms@example.com diff --git a/.gitignore b/.gitignore index 7b8da95..330ceed 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* !.env.example +!.env.coolify.example # vercel .vercel diff --git a/docker-compose.coolify.yml b/docker-compose.coolify.yml new file mode 100644 index 0000000..b6ed317 --- /dev/null +++ b/docker-compose.coolify.yml @@ -0,0 +1,95 @@ +# Coolify-Deployment (Testserver, intern) — abgeleitet von docker-compose.yml. +# Unterschiede zum lokalen Dev-Compose: +# - keine host "ports": Coolify-Proxy routet die Domain intern auf app:3000 +# - Konfiguration über Coolify-Env-Variablen statt env_file: .env +# - Service "migrate": Init-Job (prisma migrate deploy), läuft einmalig VOR app +# - kein mailhog/worker (Dev bzw. noch nicht lauffähig) +# In Coolify als "Docker Compose Location" -> docker-compose.coolify.yml setzen. +services: + # Einmaliger Migrations-Job. Nutzt die builder-Stage (volle node_modules inkl. + # Prisma-CLI/tsx), da die runner-Stage kein Prisma-CLI enthält. Idempotent. + migrate: + build: + context: . + target: builder + command: sh -c "npx prisma migrate deploy" + environment: + DATABASE_URL: ${DATABASE_URL} + depends_on: + postgres: + condition: service_healthy + restart: "no" + + app: + build: + context: . + target: runner + environment: + DATABASE_URL: ${DATABASE_URL} + REDIS_URL: ${REDIS_URL} + AUTH_SECRET: ${AUTH_SECRET} + AUTH_URL: ${AUTH_URL} + S3_ENDPOINT: ${S3_ENDPOINT} + S3_ACCESS_KEY: ${S3_ACCESS_KEY} + S3_SECRET_KEY: ${S3_SECRET_KEY} + S3_BUCKET: ${S3_BUCKET} + AI_PROVIDER: ${AI_PROVIDER} + AI_API_KEY: ${AI_API_KEY} + SMTP_HOST: ${SMTP_HOST} + SMTP_PORT: ${SMTP_PORT} + SMTP_USER: ${SMTP_USER} + SMTP_PASSWORD: ${SMTP_PASSWORD} + SMTP_FROM: ${SMTP_FROM} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + minio: + condition: service_started + migrate: + condition: service_completed_successfully + healthcheck: + # Dependency-frei (node ist im Image vorhanden); alles < 500 gilt als gesund + test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/',r=>process.exit(r.statusCode<500?0:1)).on('error',()=>process.exit(1))"] + interval: 15s + timeout: 5s + retries: 10 + start_period: 30s + restart: unless-stopped + + postgres: + image: pgvector/pgvector:pg16 + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 5s + timeout: 5s + retries: 10 + restart: unless-stopped + + redis: + image: redis:7-alpine + volumes: + - redisdata:/data + restart: unless-stopped + + minio: + image: minio/minio:latest + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + volumes: + - miniodata:/data + restart: unless-stopped + +volumes: + pgdata: + redisdata: + miniodata: diff --git a/docs/DEPLOY-COOLIFY.md b/docs/DEPLOY-COOLIFY.md new file mode 100644 index 0000000..aed7ea8 --- /dev/null +++ b/docs/DEPLOY-COOLIFY.md @@ -0,0 +1,78 @@ +# 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`, `minio`. +- **Migrationen** laufen als eigener Init-Job `migrate` (`prisma migrate deploy`, builder-Image) **vor** `app` — der App-Container migriert selbst nicht. +- **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`, `minio` (nicht `localhost`). +- `AUTH_SECRET` frisch: `openssl rand -base64 32`. +- `AUTH_URL` = exakt die in Schritt 4 vergebene App-Domain (`http://…`). +- `MINIO_ROOT_USER/PASSWORD` müssen `S3_ACCESS_KEY/S3_SECRET_KEY` entsprechen. + +## 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). Ebenso `miniodata`, `redisdata`. + +## 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` und DB-/MinIO-Passwörter neu/aus Secret-Store; eigene Domain + gültiges TLS. +- **Kein** Demo-Seed; Erst-Superadmin per Bootstrap-Skript. +- Backups (`pg_dump`/Volume-Snapshots) + Restore-Test, Monitoring/Alerting. +- Ggf. ≥ 2 `app`-Replicas hinter dem Coolify-Proxy.