Files
Speisekarte/DEPLOYMENT.md
T
florian.wildeandClaude Sonnet 5 32ef93ae8e feat: SSO-Login per OpenID Connect (Authentik u.a.)
Dritte Login-Methode neben Passwort und Passkey, konfiguriert über
OIDC_ISSUER/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET (optional, Button bleibt
ohne diese Vars ausgeblendet). Authorization-Code-Flow mit PKCE,
ID-Token-Signaturprüfung gegen das JWKS des Providers via neuer
Abhängigkeit "jose" (RS256 fest gepinnt gegen Algorithm-Confusion),
State/Nonce kurzlebig und einmalig verwendbar in neuer Tabelle
oidc_states (Migration 0003, analog zu webauthn_challenges).

Funktioniert ausschließlich für bereits angelegte Benutzer (Abgleich
per E-Mail) – kein Auto-Provisioning, keine Rollenübernahme aus
Claims, beides bleibt exklusiv über die bestehende Benutzerverwaltung
steuerbar.

app/login/page.tsx zu Server-Component-Wrapper reduziert (prüft
isConfigured(), liest ?error= aus searchParams), bisheriger Inhalt
nach components/LoginForm.tsx verschoben.

Version 1.9.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 00:29:57 +02:00

277 lines
9.9 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 auf einem neuen Server
Schritt-für-Schritt-Anleitung, um Canteen Menu auf einem frischen
(Linux-)Server produktiv zu betreiben. Die App läuft vollständig selbst
gehostet (PostgreSQL + eigene Auth), einzige optionale externe
Abhängigkeit ist DeepL für automatische Übersetzungen.
## Voraussetzungen
- Ein Server (VM oder Root-Server) mit Docker und Docker Compose v2
(`docker compose version` sollte funktionieren; auf den meisten
aktuellen Distributionen per `apt install docker.io
docker-compose-v2` bzw. dem [offiziellen Docker-Install-Skript](https://docs.docker.com/engine/install/)
installierbar).
- Eine Domain, deren DNS-A/AAAA-Eintrag auf die Server-IP zeigt (z. B.
`menu.example.de`). Ohne eigene Domain funktioniert die App auch
unter der reinen Server-IP, dann aber ohne gültiges HTTPS-Zertifikat
und ohne funktionierende Passkeys (siehe unten).
- Mindestens 1 GB RAM frei (App + PostgreSQL sind genügsam, für ~250
Mitarbeiter reicht die kleinste übliche VM-Größe).
## 1. Repository auf den Server holen
```bash
git clone https://github.com/<dein-fork-oder-repo>/canteen-menu.git
cd canteen-menu
```
(Oder per `git pull`/`scp`, falls kein direkter Git-Zugriff vom Server
aus gewünscht ist.)
## 2. Konfiguration anlegen
```bash
cp .env.example .env.local
```
`.env.local` mit einem Editor öffnen und ausfüllen:
| Variable | Was rein muss |
| --- | --- |
| `LOCAL_PG_PASSWORD` | Ein neues, zufälliges Passwort **nur aus Buchstaben/Zahlen** (z. B. `openssl rand -hex 24`) – wird beim ersten Start zum Datenbank-Passwort des mitgelieferten PostgreSQL-Containers. Zeichen wie `#`, `$`, Leerzeichen oder Anführungszeichen können beim Einlesen der `.env`-Datei zu Problemen führen (`#` beginnt z. B. einen Kommentar) |
| `APP_URL` | Die öffentliche URL mit Schema, z. B. `https://menu.example.de` (auch wenn HTTPS erst in Schritt 5 eingerichtet wird – schon jetzt korrekt eintragen, da daraus u. a. die Passkey-Domain abgeleitet wird) |
| `SITE_TITLE` | Optional, Titel im Browser-Tab/Überschrift ohne Logo |
| `ADMIN_EMAIL` | Optional, E-Mail des automatisch angelegten Admin-Erstnutzers. Ohne Angabe: `admin@<Hostname aus APP_URL>` |
| `DEEPL_API_KEY` | Optional, nur für automatische Übersetzung |
`DATABASE_URL` in `.env.local` **nicht** anpassen – die wird nur für
den Betrieb ohne Docker gebraucht (siehe README) und beim
Docker-Compose-Start ignoriert.
Zusätzlich eine leere Datei für die automatisch generierten
Admin-Zugangsdaten anlegen, da sie per Bind-Mount in den Container
eingebunden wird. Der App-Container läuft als nicht-root-Nutzer –
damit er die Datei beschreiben darf, muss sie für alle schreibbar sein:
```bash
touch ZUGANGSDATEN.md
chmod 666 ZUGANGSDATEN.md
```
## 3. App starten
**Wichtig:** `.env.local` muss vollständig ausgefüllt sein, *bevor*
dieser Befehl zum ersten Mal läuft – und danach immer mit
`--env-file .env.local` aufgerufen werden. PostgreSQL setzt sein
Passwort nur beim allerersten Start auf einem leeren Datenverzeichnis;
läuft der Befehl vorher schon einmal ohne (oder mit einem anderen)
`--env-file`, landet ein falsches Passwort dauerhaft im Datenbank-Volume
(Symptom: `password authentication failed for user "postgres"` in den
App-Logs, siehe [Fehlerbehebung](#fehlerbehebung)).
```bash
docker compose --env-file .env.local up -d --build
```
Das baut das Docker-Image, startet PostgreSQL und die App, wendet beim
ersten Start automatisch das Datenbankschema an und legt – da die
Datenbank leer ist – einen einzigen Admin-Benutzer mit zufällig
generiertem Passwort an.
Kurz prüfen, ob beides läuft:
```bash
docker compose ps
docker compose logs app --tail 30
```
In den Logs sollte u. a. `bootstrap-admin – Admin angelegt: ...`
stehen.
## 4. Admin-Zugangsdaten abholen
```bash
cat ZUGANGSDATEN.md
```
Enthält E-Mail-Adresse und Passwort des automatisch angelegten
Admin-Kontos. Direkt nach dem ersten Login unter „Mein Konto“ das
Passwort ändern und optional einen Passkey registrieren.
Die App ist jetzt unter `http://<server-ip>:3000` erreichbar – für den
produktiven Betrieb fehlt noch HTTPS/die Domain (nächster Schritt).
## 5. Reverse-Proxy + HTTPS einrichten
Die App selbst spricht nur HTTP auf Port 3000; TLS/HTTPS übernimmt ein
Reverse-Proxy davor. Am einfachsten mit [Caddy](https://caddyserver.com/)
(automatisches Let’s-Encrypt-Zertifikat, keine manuelle Zertifikats-
verwaltung nötig):
```bash
sudo apt install caddy # oder: siehe caddyserver.com/docs/install
```
`/etc/caddy/Caddyfile`:
```
menu.example.de {
reverse_proxy localhost:3000
}
```
```bash
sudo systemctl reload caddy
```
Caddy holt beim ersten Aufruf automatisch ein Zertifikat und leitet
danach HTTP→HTTPS um. Alternative: nginx + certbot, falls bereits ein
nginx-Setup existiert (Standard-`proxy_pass`-Konfiguration auf
`http://127.0.0.1:3000`, Zertifikat separat per `certbot --nginx`).
Danach in `.env.local` sicherstellen, dass `APP_URL=https://menu.example.de`
gesetzt ist (Schema `https`, keine Portangabe), und die App neu
starten, falls diese Variable nach Schritt 2 noch geändert wurde:
```bash
docker compose --env-file .env.local up -d
```
## 6. Danach
- Passkeys funktionieren erst mit korrekt gesetzter `APP_URL` (RP-Domain
wird direkt daraus abgeleitet) und über HTTPS (Passkeys verlangen eine
sichere Origin, `localhost` ausgenommen).
- Weitere Kantinenmitarbeiter/Admins unter „Benutzer“ (nur als Admin
sichtbar) anlegen.
- Essenszeiten, Gerichte, Allergene/Zusatzstoffe sind zu Beginn leer und
werden im Kantinen-Bereich gepflegt.
- Logo/Favicon/Farben unter „Design“ (nur Admin) anpassen.
## SSO/OpenID Connect (optional)
Zusätzlich zu Passwort und Passkey kann ein SSO-Login gegen einen
beliebigen OpenID-Connect-Provider aktiviert werden (z. B. ein selbst
gehostetes [Authentik](https://goauthentik.io/)). Dazu in `.env.local`
setzen:
```bash
OIDC_ISSUER=https://authentik.example.de/application/o/canteen-menu/
OIDC_CLIENT_ID=<client-id-aus-authentik>
OIDC_CLIENT_SECRET=<client-secret-aus-authentik>
```
Beim Identity Provider als Redirect-URI hinterlegen:
`<APP_URL>/api/auth/oidc/callback` (also z. B.
`https://menu.example.de/api/auth/oidc/callback`).
**Wichtig:** SSO-Login funktioniert ausschließlich für E-Mail-Adressen,
die bereits unter „Benutzer“ angelegt sind – es werden nie neue Konten
angelegt, und die Rolle (Admin/Kantinenmitarbeiter) bleibt ausschließlich
dort steuerbar. Ein Mitarbeiter muss also zuerst ganz normal als
Benutzer angelegt werden, bevor er sich per SSO anmelden kann.
Ohne die drei Variablen bleibt der SSO-Button auf der Anmeldeseite
einfach ausgeblendet, Passwort/Passkey funktionieren unverändert.
## Fehlerbehebung
### `password authentication failed for user "postgres"` in den App-Logs
Ursache: Das Docker-Volume der Datenbank wurde bereits mit einem
anderen Passwort initialisiert, als aktuell in `.env.local` steht (z. B.
weil `docker compose up` einmal ohne `--env-file .env.local` lief, bevor
das Passwort gesetzt war, oder `LOCAL_PG_PASSWORD` nachträglich geändert
wurde – PostgreSQL übernimmt das Passwort nur beim allerersten Start).
Fix, solange noch keine echten Daten in der Datenbank sind (bei einem
frischen Deployment immer der Fall):
```bash
docker compose --env-file .env.local down
docker volume rm canteen-menu_pgdata
docker compose --env-file .env.local up -d --build
docker compose logs app --tail 30
```
Existieren bereits echte Daten, stattdessen das Passwort direkt im
laufenden Datenbank-Container an `.env.local` angleichen, statt das
Volume zu löschen:
```bash
docker compose exec db psql -U postgres -c "ALTER USER postgres WITH PASSWORD '<Wert aus LOCAL_PG_PASSWORD>';"
docker compose --env-file .env.local up -d
```
### `Error: connect ECONNREFUSED ...:5432` beim Start
Normal beim allerersten Start: Der App-Container versucht sich zu
verbinden, bevor PostgreSQL im `db`-Container fertig hochgefahren ist.
Dank `restart: unless-stopped` startet der App-Container automatisch
neu und verbindet sich, sobald die Datenbank bereit ist (kann ein paar
Sekunden bis wenige Minuten dauern). Mit `docker compose logs app -f`
verfolgen – taucht der Fehler dauerhaft (mehrere Minuten) weiter auf,
`docker compose ps` prüfen, ob der `db`-Container tatsächlich dauerhaft
läuft.
### `EACCES: permission denied, open '/app/ZUGANGSDATEN.md'`
Die Datei wurde ohne Schreibrecht für alle angelegt (Schritt 2 oben,
`chmod 666` fehlte). Der Admin-Benutzer wurde in diesem Fall trotzdem
in der Datenbank angelegt, nur die Zugangsdaten konnten nicht in die
Datei geschrieben werden – sie stehen dann stattdessen im Log
(`docker compose logs app` nach `bootstrap-admin – Admin-Passwort:`
durchsuchen). Für zukünftige Deployments zusätzlich die Berechtigung
korrigieren:
```bash
chmod 666 ZUGANGSDATEN.md
```
Falls das Passwort auch im Log nicht mehr auffindbar ist (z. B. weil
Logs schon rotiert wurden), Admin-Konto neu anlegen lassen:
```bash
docker compose exec db psql -U postgres -d canteen_menu -c "delete from users;"
chmod 666 ZUGANGSDATEN.md
docker compose restart app
docker compose logs app --tail 20
cat ZUGANGSDATEN.md
```
## Updates einspielen
```bash
git pull
docker compose --env-file .env.local up -d --build
```
Neue Datenbank-Migrationen (`db/migrations/*.sql`) werden beim Start
automatisch und idempotent angewendet, bestehende Daten bleiben
erhalten.
Alternativ übernimmt `./update.sh` den kompletten Ablauf: Stack stoppen,
lokale Änderungen an `docker-compose.yml` per `git stash` sichern (falls
vorhanden), `git pull`, Änderungen zurückspielen, Stack neu bauen und
starten:
```bash
./update.sh
```
## Backup
Zwei Docker-Volumes enthalten alle persistenten Daten:
- `canteen-menu_pgdata` – die komplette PostgreSQL-Datenbank
- `canteen-menu_branding_data` – hochgeladenes Logo/Favicon
```bash
docker run --rm -v canteen-menu_pgdata:/data -v "$PWD":/backup alpine \
tar czf /backup/pgdata-backup.tar.gz -C /data .
```
Für ein reines Datenbank-Backup eignet sich auch `docker compose exec db
pg_dump -U postgres canteen_menu > backup.sql`.