Files
alpenwerk-hr/DEPLOYMENT.md
Maximilian Stubhan d8a1fdf43b Reinstate the Vercel build settings
Reverts 61ccce5, which reverted ecbda3f. The decision came back to Vercel,
so the two platform accommodations return: output: "standalone" is
conditional on VERCEL again, and /api/import goes back to 60 seconds, the
free tier's ceiling.

The Docker path is unaffected and stays documented — including the internal
network notes and deploy/Caddyfile written in between, which remain correct
for anyone taking that road. DEPLOYMENT.md conflicted at the top and now
carries both introductions instead of one replacing the other.

Verified with VERCEL=1: builds clean and emits no standalone directory.

Stated once and recorded here rather than repeated: Vercel's Hobby plan
excludes commercial use, and this is a company's HR system. Defensible while
the database holds nothing but the 852 invented people from the seed;
Pro at $20/month is the licensed path once real personnel data is in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 08:32:47 +02:00

279 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:<konto>/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://<projekt>.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/<verzeichnis-id>/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 <repo-url> && 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: <registry>/<name>:<tag>` 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://<host>/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.