# Deployment Zwei Wege, beide unterstützt. Das Abbild ist umgebungsneutral — es gibt keine Werte mehr, die beim Bauen eingebacken werden —, ein Wechsel ist also jederzeit möglich. | | passt, wenn | |---|---| | [Vercel](#vercel) | ihr nichts betreiben wollt; schnellster Weg | | [Docker](#deployment-mit-docker) | es in eure eigene Infrastruktur soll | **In beiden Fällen gleich:** die Umgebungsvariablen aus [Abschnitt 1](#1-env-anlegen), die Umleitungs-URI in der Entra-Registrierung, und dass eine neue Person nach ihrer ersten Anmeldung eine `profiles`-Zeile braucht (siehe [docs/entra-sso.md](docs/entra-sso.md)). ## Vercel Das Repository liegt auf `git.elycon.solutions` — einem selbst betriebenen Git. **Vercels Git-Anbindung kann nur GitHub, GitLab und Bitbucket**, dieses Repository lässt sich dort also nicht verknüpfen. Zwei Möglichkeiten: ### a) Von der Arbeitsstation ausrollen (ohne GitHub) ```bash npx vercel login npx vercel link npx vercel --prod ``` Funktioniert mit jedem Repository. Der Preis: kein automatisches Ausrollen bei einem Push — jede Veröffentlichung ist ein bewusster Befehl. Für zwei Personen ist das eher Vorteil als Nachteil. ### b) Zusätzlich nach GitHub spiegeln ```bash git remote add github git@github.com:/alpenwerk-hr.git git push github feat/sap-om-org-model ``` Danach das GitHub-Repository in Vercel verbinden. Ab dann rollt jeder Push aus. Zwei Fernziele bedeuten aber auch: beide müssen gepflegt werden. ### Danach 1. **Umgebungsvariablen** im Vercel-Projekt setzen (Settings → Environment Variables), dieselben wie in [Abschnitt 1](#1-env-anlegen). `AUTH_URL` ist nicht nötig, Vercel setzt den Host selbst. 2. **Umleitungs-URI** in der Entra-Registrierung ergänzen: `https://.vercel.app/api/auth/callback/microsoft-entra-id` 3. Der nächtliche Lauf ist über `vercel.json` bereits eingerichtet. Zwei Eigenheiten der Plattform, die im Code berücksichtigt sind: `output: "standalone"` entfällt dort automatisch (Vercel baut selbst), und `/api/import` ist auf 60 Sekunden begrenzt — die Obergrenze des kostenlosen Tarifs. Im Pro-Tarif liessen sich 300 setzen, falls eine Importdatei mit vielen tausend Zeilen ansteht. Der Verbindungspool passt zu serverlosen Aufrufen, **weil** `DATABASE_URL` auf den Transaktions-Modus zeigt (Port 6543). Mit dem Sitzungs-Modus wären die 15 Verbindungen des Tarifs nach wenigen gleichzeitigen Aufrufen verbraucht. ## Deployment mit Docker Dieser Guide beschreibt, wie die App stattdessen als Docker-Container auf einem eigenen Linux-Server läuft. ## Was wird containerisiert – und was nicht - **Containerisiert:** nur die Next.js-App selbst (`Dockerfile`). - **Nicht containerisiert:** die Datenbank. Die App verbindet sich über `DATABASE_URL` zu einem beliebigen PostgreSQL ab 15 — heute ein Supabase-Projekt, genauso möglich sind Azure Flexible Server, RDS, Cloud SQL oder eigenes Blech. `supabase/` in diesem Repo ist die Migrations- und Entwicklungsumgebung, kein Teil des Deployments. - **Ebenfalls nicht containerisiert:** die Anmeldung. Sie läuft über Microsoft Entra ID; die App hält nur das Sitzungscookie (Auth.js). Es gibt keinen Anmeldedienst, der mit ausgerollt werden müsste. - **Ersetzt:** der Vercel-Cron-Job aus `vercel.json` (täglich 03:00 Uhr, ruft `/api/cron/apply-pending-changes` auf, um fällige Versetzungen/ Beförderungen/Karenz/Reorg-Änderungen zu übernehmen). Da es außerhalb von Vercel kein Vercel-Cron gibt, übernimmt das im `docker-compose.yml` enthaltene `cron`-Sidecar-Container diese Aufgabe mit demselben Schema und demselben Bearer-Secret, das die Route bereits erwartet. ## Betrieb im Firmennetz — zwei Dinge vorab Die App läuft intern, aber sie ist **nicht** von der Aussenwelt unabhängig. Beides vor der Installation klären, sonst scheitert es am Ende an der Firewall: **1. Der Server braucht ausgehenden Zugang.** Eingehend aus dem Internet nichts, ausgehend zwingend: | Ziel | Wofür | Ohne das | |---|---|---| | `login.microsoftonline.com` (443) | Auth.js tauscht den Anmeldecode **serverseitig** gegen ein Token und lädt die Konfiguration des Ausstellers | keine Anmeldung möglich | | Die Datenbank (Supabase: `*.pooler.supabase.com`, 6543) | jede Abfrage | die App startet, zeigt aber nichts | Dass die Anmeldung im Browser der Person stattfindet, genügt **nicht** — der Tausch von Code gegen Token läuft vom Server aus. Liegt die VM in einem abgeschotteten Netz, ist entweder ein Proxy nötig oder eine PostgreSQL- Instanz im selben Netz statt Supabase. **2. HTTPS ist Pflicht, auch intern.** Entra ID akzeptiert `http` nur für `localhost`. Der praktikable Weg ohne öffentliche Erreichbarkeit: ein **öffentlicher DNS-Name, der auf die private Adresse zeigt** (z. B. `hr.elycon.solutions` → `10.x.x.x`) und ein Zertifikat über die DNS-Challenge. Das ist zulässig, verbreitet, und liefert ein regulär vertrauenswürdiges Zertifikat, ohne dass der Server je aus dem Internet erreichbar ist. Beispielkonfiguration: [`deploy/Caddyfile`](deploy/Caddyfile). Die Alternative — selbst signiertes Zertifikat — bedeutet, es auf jedem Arbeitsplatz als vertrauenswürdig zu hinterlegen. Bei zwei Personen machbar, bei zwanzig nicht. ## Voraussetzungen - Docker + Docker Compose (v2, das im Docker Desktop/Docker Engine enthaltene `docker compose`) auf dem Zielserver. - Ein bestehendes Supabase-Projekt mit den Migrationen aus `supabase/migrations/` bereits eingespielt (`supabase db push` bzw. wie bisher). ## 1. `.env` anlegen ```bash cp .env.example .env ``` Werte eintragen: | Variable | Woher | |---|---| | `DATABASE_URL` | Verbindungsstring der PostgreSQL-Instanz. Die Rolle darf **kein** `BYPASSRLS` haben; hinter einem Pooler den **Transaktions-Modus** (bei Supabase Port 6543) | | `DATABASE_SSL` | nur setzen (`false`), wenn die Datenbank ohne TLS läuft | | `AUTH_SECRET` | selbst generieren: `openssl rand -base64 32` | | `AUTH_MICROSOFT_ENTRA_ID_ID` | Entra-Portal → App-Registrierung → Übersicht | | `AUTH_MICROSOFT_ENTRA_ID_SECRET` | Entra-Portal → Zertifikate & Geheimnisse (nur einmal sichtbar!) | | `AUTH_MICROSOFT_ENTRA_ID_ISSUER` | `https://login.microsoftonline.com//v2.0` | | `CRON_SECRET` | selbst generieren: `openssl rand -hex 32` | Details zur Entra-Registrierung: [`docs/entra-sso.md`](docs/entra-sso.md). Wichtig zum Verständnis: - **Nichts davon wird in das Image eingebacken.** Es gibt keine `NEXT_PUBLIC_*`-Variablen mehr; alle Werte liest die Anwendung zur Laufzeit über `env_file`. Eine Änderung braucht deshalb nur einen Neustart, keinen neuen Build — und dasselbe Image läuft in Test und Produktion. - `.env` steht schon in `.gitignore` – nicht committen. ## 2. Bauen und lokal testen ```bash docker compose build docker compose up -d docker compose logs -f app ``` App ist danach unter `http://localhost:3000` erreichbar. Healthcheck prüft `GET /login`; Status siehe `docker compose ps`. Cron-Sidecar prüfen: ```bash docker compose logs -f cron ``` ## 3. Auf einem Server deployen Einfachste Variante – Repo direkt auf dem Server bauen: ```bash git clone && cd manner-app cp .env.example .env # Werte eintragen docker compose up -d --build ``` Alternative für CI/CD (Image einmal bauen, überall pullen): Image in einer Registry bauen und pushen, auf dem Server nur `docker compose pull && docker compose up -d` ausführen. Dafür in `docker-compose.yml` zusätzlich `image: /:` setzen. Build-Argumente braucht es dabei **keine**: das Abbild enthält keine umgebungsabhängigen Werte mehr, alles kommt zur Laufzeit aus `.env`. Dasselbe Abbild läuft damit in Test und Produktion. ## 4. Reverse Proxy + HTTPS Next.js selbst sollte laut den offiziellen Docs **nicht** direkt exponiert werden – ein Reverse Proxy übernimmt TLS, Rate-Limiting und Request- Validierung. Fertige Konfiguration: [`deploy/Caddyfile`](deploy/Caddyfile) — mit DNS-Challenge, weil der Server aus dem Internet nicht erreichbar ist (siehe [oben](#betrieb-im-firmennetz--zwei-dinge-vorab)). ```bash sudo cp deploy/Caddyfile /etc/caddy/Caddyfile sudo systemctl reload caddy ``` `docker-compose.yml` veröffentlicht Port 3000 bewusst nur auf `127.0.0.1` — die App ist also ausschliesslich über den Proxy erreichbar, nicht daneben unverschlüsselt. Zusätzlich in die `.env`: ``` AUTH_URL=https://hr.elycon.solutions ``` Ohne diesen Wert baut Auth.js seine Rückruf-Adresse aus dem, was der Container sieht — und das ist hinter dem Proxy nicht der Name, den der Browser benutzt hat. ## 5. Updates ausrollen ```bash git pull docker compose build docker compose up -d ``` Kurzer Downtime-Moment beim Neustart des `app`-Containers ist bei dieser Single-Instance-Compose-Konfiguration normal. Für Zero-Downtime-Deployments wäre eine zweite Instanz + Load Balancer nötig (siehe Abschnitt 6). Datenbank-Migrationen (`supabase/migrations/*.sql`) werden weiterhin über die Supabase CLI gegen das Supabase-Projekt gefahren, unabhängig vom App-Deployment: ```bash supabase db push ``` ## 6. Hinweis bei mehreren Replicas Läuft die App skaliert (mehrere `app`-Container hinter einem Load Balancer), muss `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` explizit gesetzt und auf allen Instanzen identisch sein – sonst schlagen Server Actions (`actions/*.ts`, z. B. Mitarbeiter- und Positions-Mutationen) mit "Failed to find Server Action" fehl, wenn eine Anfrage auf einer anderen Instanz landet als der, die das Formular gerendert hat. Erzeugen mit: ```bash openssl rand -base64 32 ``` Als zusätzliche Env-Variable in `.env` eintragen. Bei der aktuellen Single-Instance-Compose-Konfiguration ist das nicht nötig. ## Troubleshooting - **Anmeldung endet auf `/login?error=…`:** die Umleitungs-URI in der Entra-Registrierung muss exakt `https:///api/auth/callback/microsoft-entra-id` lauten. Steht die Anwendung hinter einem Reverse Proxy unter einer anderen Adresse, als sie selbst sieht, zusätzlich `AUTH_URL` setzen. - **Angemeldet, aber sofort zurück auf `/login?error=no_hr_access`:** die Anmeldung hat funktioniert, es fehlt die Freischaltung. Es braucht eine `profiles`-Zeile mit `role = 'hr'` und `is_active = true` auf derselben Kennung, die in `app_users` steht. - **Cron läuft nicht:** `docker compose logs cron` – prüft, ob `/etc/crontabs/root` korrekt geschrieben wurde und ob `CRON_SECRET` in `.env` gesetzt ist (leer/fehlend führt serverseitig zu `401`). - **`max clients reached in session mode`:** der Verbindungsstring zeigt auf den Sitzungs-Modus des Poolers. Auf den Transaktions-Modus wechseln (bei Supabase Port 6543). - **Healthcheck rot:** `docker compose logs app` – meist `DATABASE_URL` fehlend oder nicht erreichbar. Der Pool baut die Verbindung erst beim ersten Zugriff auf, der Fehler steht deshalb im Log der Anfrage, nicht im Start-Log.