Two settings were wrong for a platform build. output: "standalone" tells Next.js to emit a self-contained server, which is what the Dockerfile copies in — and what Vercel neither needs nor expects, since it builds and packages the app itself. It is now conditional on the VERCEL variable, which every build there sets, so each path gets what it wants. Verified both ways: with VERCEL=1 no standalone directory appears, without it one does. /api/import declared maxDuration = 120. The free tier caps at 60 and refuses anything higher, so the deployment would have failed on a value chosen for a self-hosted server. Lowered, with the reason and the Pro ceiling written next to it. DEPLOYMENT.md now covers both paths, and says plainly that the repository cannot be connected: git.elycon.solutions is self-hosted, and Vercel's git integration only speaks GitHub, GitLab and Bitbucket. Deploying from the workstation with the CLI works with any repository and is the shorter road; mirroring to GitHub is written down as the alternative, with its cost — two remotes to keep in step. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
231 lines
8.6 KiB
Markdown
231 lines
8.6 KiB
Markdown
# 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 beliebigen Server läuft.
|
||
|
||
## Was wird containerisiert – und was nicht
|
||
|
||
- **Containerisiert:** nur die Next.js-App selbst (`Dockerfile`).
|
||
- **Nicht containerisiert:** Supabase (Datenbank + Auth). Die App verbindet
|
||
sich per URL/Key zu einem bestehenden Supabase-Projekt (Cloud oder
|
||
selbst gehostet) – das bleibt unverändert. `supabase/` in diesem Repo ist
|
||
nur die lokale Dev-/Migrations-Umgebung (`supabase start`), kein Teil des
|
||
Deployments.
|
||
- **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.
|
||
|
||
## 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 (GHCR, Docker Hub, …) 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 und
|
||
den Build in der CI-Pipeline mit den `--build-arg`-Werten für
|
||
`NEXT_PUBLIC_*` laufen lassen.
|
||
|
||
## 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. Beispiel mit [Caddy](https://caddyserver.com/) (automatisches
|
||
HTTPS via Let's Encrypt):
|
||
|
||
```caddyfile
|
||
# /etc/caddy/Caddyfile
|
||
hr.example.com {
|
||
reverse_proxy localhost:3000
|
||
}
|
||
```
|
||
|
||
`docker-compose.yml` published Port 3000 aktuell auf den Host – bei
|
||
Verwendung eines Reverse Proxys auf demselben Host kann das Publishing auf
|
||
`127.0.0.1:3000:3000` eingeschränkt werden, damit der Container-Port nicht
|
||
direkt von außen erreichbar ist.
|
||
|
||
## 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.
|