Files
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

9.9 KiB
Raw Permalink Blame History

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 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

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

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:

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).

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:

docker compose ps
docker compose logs app --tail 30

In den Logs sollte u. a. bootstrap-admin – Admin angelegt: ... stehen.

4. Admin-Zugangsdaten abholen

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 (automatisches Let’s-Encrypt-Zertifikat, keine manuelle Zertifikats- verwaltung nötig):

sudo apt install caddy   # oder: siehe caddyserver.com/docs/install

/etc/caddy/Caddyfile:

menu.example.de {
    reverse_proxy localhost:3000
}
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:

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). Dazu in .env.local setzen:

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):

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:

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:

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:

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

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:

./update.sh

Backup

Zwei Docker-Volumes enthalten alle persistenten Daten:

  • canteen-menu_pgdata – die komplette PostgreSQL-Datenbank
  • canteen-menu_branding_data – hochgeladenes Logo/Favicon
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.