# 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//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@` | | `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://: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. ## 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 '';" 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. ## 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`.