A push to master now builds and restarts the application on the server: a Gitea Actions workflow on a self-hosted runner writes .env from the repository secrets, applies pending migrations, rebuilds the compose stack against the host's Docker daemon, and waits for the container's healthcheck before calling the run green. Without that last step a deploy counts as successful the moment the container *starts*, even if the app inside it dies immediately. Switching migrations on automatically turned up something that had to be fixed first: supabase_migrations.schema_migrations did not exist at all. Every one of the 65 migrations was unrecorded, because they have been applied by hand all along. An automatic `db push` would therefore have replayed all 65 against the live database — initial_schema and the OM cutover included. The database was checked against a spread of migrations first (it is at head), then baselined: all 65 recorded as applied without executing them. The runner is scripts/migrate.mjs rather than the Supabase CLI. It needs only `pg`, which the project already ships, instead of downloading a CLI whose version drifts independently of this repository; and it does one thing — the missing files, in order, each in its own transaction — where `db push` also diffs schemas and may do more than that. Bookkeeping goes in the same table in the same shape the CLI uses, so `supabase db push` from a workstation still works and still skips what already ran. The workflow lives in .github/workflows, not .gitea/. Gitea reads .gitea/workflows and falls back to .github/workflows only when the former is absent — creating .gitea/ would have silently switched off ci.yml, with the run simply never appearing. Verified: both workflow files parse; the secret check names what is missing and refuses; values starting with "-" or containing "=" survive being written to .env; and the runner was exercised against the real database with a throwaway migration — applied once, skipped on a second run, and on a deliberate syntax error rolled back whole, recording nothing. Both probes were removed; the count is back to 65. Not verified: nothing has run on an actual Gitea runner — none is registered yet. DEPLOYMENT.md §5 covers registering one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
407 lines
17 KiB
Markdown
407 lines
17 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 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. Automatisch ausrollen bei einem Push (Gitea Actions)
|
||
|
||
Ein Push auf `master` baut und startet die Anwendung auf dem Server neu.
|
||
Zuständig ist [`.github/workflows/deploy.yml`](.github/workflows/deploy.yml).
|
||
|
||
> **Warum `.github/` und nicht `.gitea/`:** Gitea Actions liest
|
||
> `.gitea/workflows` — und nur wenn dieses Verzeichnis **fehlt**, ersatzweise
|
||
> `.github/workflows`. Ein neu angelegtes `.gitea/workflows/` würde also
|
||
> `ci.yml` stillschweigend abschalten. Solange beide Dateien unter `.github/`
|
||
> liegen, sieht Gitea beide.
|
||
|
||
### a) Runner registrieren
|
||
|
||
Der Runner läuft als Container **auf demselben Server** wie die Anwendung und
|
||
bekommt den Docker-Socket des Hosts eingehängt — damit baut und startet er die
|
||
Container des Hosts, statt welche in sich selbst zu erzeugen.
|
||
|
||
Registrierungs-Token holen: Gitea → Repository → *Settings* → *Actions* →
|
||
*Runners* → *Create new runner*.
|
||
|
||
```bash
|
||
mkdir -p /opt/gitea-runner && cd /opt/gitea-runner
|
||
|
||
docker run -d --name gitea-runner --restart unless-stopped \
|
||
-e GITEA_INSTANCE_URL=https://git.elycon.solutions \
|
||
-e GITEA_RUNNER_REGISTRATION_TOKEN=<token> \
|
||
-e GITEA_RUNNER_NAME=alpenwerk-deploy \
|
||
-e GITEA_RUNNER_LABELS=self-hosted,ubuntu-latest \
|
||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||
-v /opt/gitea-runner:/data \
|
||
gitea/act_runner:latest
|
||
```
|
||
|
||
Zu den Labels: `self-hosted` verlangt der Deploy-Workflow, `ubuntu-latest` die
|
||
bestehende `ci.yml`. Fehlt eines, bleibt der jeweilige Lauf ohne Fehlermeldung
|
||
in der Warteschlange — **das ist der häufigste Grund, warum „nichts passiert"**.
|
||
|
||
> Der eingehängte Docker-Socket gibt dem Runner faktisch Wurzelrechte auf dem
|
||
> Host. Das ist bei einem Deploy-Runner der Zweck der Übung, aber es heisst
|
||
> auch: wer in dieses Repository schreiben darf, darf auf diesem Server alles.
|
||
> Bei zwei Personen vertretbar; bei einem grösseren Kreis gehört der Runner auf
|
||
> eine eigene Maschine, die sich per SSH verbindet.
|
||
|
||
### b) Secrets hinterlegen
|
||
|
||
Gitea → Repository → *Settings* → *Actions* → *Secrets*. Dieselben Werte wie in
|
||
[Abschnitt 1](#1-env-anlegen); der Workflow baut daraus zur Laufzeit die `.env`
|
||
und löscht sie danach wieder.
|
||
|
||
| Secret | Anmerkung |
|
||
|---|---|
|
||
| `DATABASE_URL` | Transaktions-Pooler (Supabase: 6543) — den benutzt die Anwendung |
|
||
| `SUPABASE_DB_URL` | **Direkter** Zugang (Supabase: 5432) — nur für die Migrationen |
|
||
| `AUTH_SECRET` | |
|
||
| `AUTH_URL` | z. B. `https://hr.elycon.solutions` |
|
||
| `AUTH_MICROSOFT_ENTRA_ID_ID` | |
|
||
| `AUTH_MICROSOFT_ENTRA_ID_SECRET` | |
|
||
| `AUTH_MICROSOFT_ENTRA_ID_ISSUER` | |
|
||
| `CRON_SECRET` | |
|
||
|
||
Warum zwei Verbindungsstrings: der Transaktions-Pooler bricht bei einer
|
||
Migration ab, die mehrere Anweisungen in einer Transaktion bündelt. Migrationen
|
||
gehören über die direkte Verbindung, der laufende Betrieb über den Pooler.
|
||
|
||
Zusätzlich unter *Variables* (kein Secret, nur eine Einstellung):
|
||
|
||
| Variable | Vorgabe | Wofür |
|
||
|---|---|---|
|
||
| `COMPOSE_PROJECT_NAME` | `alpenwerk-hr` | Muss zum bestehenden Stapel passen — sonst entsteht ein zweiter daneben |
|
||
|
||
Den bestehenden Namen zeigt auf dem Server:
|
||
|
||
```bash
|
||
docker compose ls
|
||
```
|
||
|
||
### c) Was der Lauf tut
|
||
|
||
1. `.env` aus den Secrets schreiben (fehlt eines, bricht er mit Namen ab).
|
||
2. **Migrationen anwenden** — erst `--dry-run` fürs Protokoll, dann echt.
|
||
3. `docker compose build` und `up -d` gegen den Docker-Dienst des Hosts.
|
||
4. Auf `healthy` warten (bis zu zwei Minuten). Wird die Anwendung nicht gesund,
|
||
schlägt der Lauf fehl und hängt die letzten 80 Logzeilen an.
|
||
5. `.env` wieder löschen.
|
||
|
||
Der Workflow ist **nicht** an `ci.yml` gekoppelt: ein Push auf `master` rollt
|
||
aus, auch wenn die Tests parallel noch laufen. Soll erst nach grünen Tests
|
||
ausgerollt werden, gehört der `deploy`-Job mit `needs: [check]` in `ci.yml` —
|
||
dann allerdings braucht der Runner auch das Label, mit dem `check` läuft.
|
||
|
||
## 5a. Migrationen
|
||
|
||
Eingespielt werden sie von [`scripts/migrate.mjs`](scripts/migrate.mjs) —
|
||
demselben Läufer, den auch der Deploy benutzt:
|
||
|
||
```bash
|
||
node --env-file=.env scripts/migrate.mjs --dry-run # was stünde an
|
||
node --env-file=.env scripts/migrate.mjs # anwenden
|
||
```
|
||
|
||
Buch geführt wird in `supabase_migrations.schema_migrations`, derselben Tabelle
|
||
in derselben Form, die die Supabase-CLI benutzt — `supabase db push` von der
|
||
Arbeitsstation bleibt damit möglich und überspringt, was hier schon lief.
|
||
|
||
> **Einmalig, im August 2026 bereits erledigt:** Die Migrationen wurden bis
|
||
> dahin von Hand eingespielt, die Buchführungstabelle existierte gar nicht. Ein
|
||
> automatischer Lauf hätte deshalb alle 65 Dateien erneut gegen die
|
||
> produktive Datenbank gespielt — inklusive `initial_schema` und der
|
||
> OM-Umstellung. Vor dem Einschalten wurde einmal
|
||
> `node scripts/migrate.mjs --baseline` gefahren: das verbucht alles Vorhandene
|
||
> als angewendet, **ohne es auszuführen**. Wer eine weitere Umgebung aufsetzt,
|
||
> deren Datenbank schon steht, braucht denselben Schritt.
|
||
|
||
## 5b. Updates von Hand ausrollen
|
||
|
||
Falls der Runner nicht läuft oder ein Stand ausser der Reihe gebraucht wird:
|
||
|
||
```bash
|
||
git pull
|
||
node --env-file=.env scripts/migrate.mjs
|
||
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).
|
||
|
||
Der Migrationsschritt steht bewusst **vor** dem Neubau: der neue Code erwartet
|
||
das neue Schema, und umgekehrt liefe die neue Anwendung kurz gegen das alte.
|
||
|
||
## 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
|
||
|
||
- **Push gemacht, aber kein Lauf startet:** meist die Labels. Der Lauf steht
|
||
dann in Gitea unter *Actions* als „Waiting" — ohne Fehlermeldung, weil kein
|
||
Runner das verlangte Label anbietet. `deploy.yml` verlangt `self-hosted`,
|
||
`ci.yml` verlangt `ubuntu-latest`. Welche Labels registriert sind, zeigt
|
||
Gitea → *Settings* → *Actions* → *Runners*; ändern lassen sie sich durch
|
||
erneutes Registrieren.
|
||
- **Deploy läuft, aber es entsteht ein zweiter Stapel:** `COMPOSE_PROJECT_NAME`
|
||
passt nicht zum bestehenden. Auf dem Server `docker compose ls` — der dort
|
||
gelistete Name gehört als Variable ins Repository. Bis dahin läuft die alte
|
||
Instanz weiter und beansprucht Port 3000; die neue scheitert daran.
|
||
- **`Cannot connect to the Docker daemon` im Deploy:** dem Runner-Container
|
||
fehlt der Socket. Er braucht `-v /var/run/docker.sock:/var/run/docker.sock`.
|
||
- **Migration schlägt im Deploy fehl:** der Lauf bricht ab, bevor Container
|
||
angefasst werden — die alte Version läuft also weiter. Die fehlgeschlagene
|
||
Datei wurde vollständig zurückgerollt und **nicht** verbucht; nach der
|
||
Korrektur reicht ein erneuter Push.
|
||
|
||
- **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.
|