v1.5: Docker/Compose/.env.example auf selbst gehostetes Postgres umgestellt

Dockerfile: keine NEXT_PUBLIC_*-Build-Args mehr (Branding-URLs sind jetzt
relativ, kein Anon-Key-Konzept mehr – das Image wird dadurch
umgebungsunabhängig), db/migrate.mjs + scripts/ werden zusätzlich in die
Runner-Stage kopiert (nicht Teil von Next's Standalone-Trace), CMD wendet
Migrationen an und legt bei Bedarf den Admin an, bevor der Server startet.

docker-compose.yml: db-Dienst ist jetzt die primäre Datenbank (kein
Supabase-Dump-Mount mehr), neues Volume branding_data für Logo/Favicon,
Bind-Mount für ZUGANGSDATEN.md.

.env.example: Supabase-Variablen durch DATABASE_URL + optionales
ADMIN_EMAIL ersetzt. package.json: @supabase/ssr und @supabase/supabase-js
entfernt. README.md komplett auf die neue Architektur aktualisiert.
This commit is contained in:
2026-07-18 11:02:16 +02:00
parent a9a9cf7ba4
commit b512093a7e
7 changed files with 105 additions and 243 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ node_modules
.env* .env*
!.env.example !.env.example
ZUGANGSDATEN.md ZUGANGSDATEN.md
supabase/local data/branding
Dockerfile Dockerfile
docker-compose.yml docker-compose.yml
README.md README.md
+9 -11
View File
@@ -1,25 +1,23 @@
# Vorlage für .env.local (Entwicklung) bzw. die Umgebungsvariablen # Vorlage für .env.local (Entwicklung) bzw. die Umgebungsvariablen
# beim Hoster (Live-Betrieb). Echte Werte niemals committen! # beim Hoster (Live-Betrieb). Echte Werte niemals committen!
# Supabase-Projekt-URL (Dashboard -> Project Settings -> API) # Verbindung zur (selbst gehosteten) PostgreSQL-Datenbank.
NEXT_PUBLIC_SUPABASE_URL=https://<projekt-ref>.supabase.co DATABASE_URL=postgres://postgres:<passwort>@localhost:5432/canteen_menu
# Öffentlicher anon-Key (darf im Browser landen, RLS schützt die Daten)
NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key>
# Service-Role-Key (GEHEIM! Nur serverseitig, für die Benutzerverwaltung)
SUPABASE_SERVICE_ROLE_KEY=<service-role-key>
# Öffentliche URL, unter der die App erreichbar ist (Schema + Domain). # Öffentliche URL, unter der die App erreichbar ist (Schema + Domain).
# Wird für den CSRF-Origin-Check hinter Reverse-Proxies genutzt und ist # Wird für den CSRF-Origin-Check hinter Reverse-Proxies genutzt und ist
# zugleich die Passkey-Domain: nach einer Änderung einmalig # zugleich die Passkey-Domain (RP-ID/Origin werden direkt daraus
# SUPABASE_ACCESS_TOKEN=<token> node scripts/configure-passkeys.mjs # abgeleitet, kein separater Konfigurationsschritt nötig).
# ausführen, damit Passkeys auf der neuen Domain funktionieren.
APP_URL=http://localhost:3000 APP_URL=http://localhost:3000
# Titel im Browser-Tab und Überschrift, solange kein Logo hochgeladen ist. # Titel im Browser-Tab und Überschrift, solange kein Logo hochgeladen ist.
SITE_TITLE=Canteen Menu SITE_TITLE=Canteen Menu
# Optional: E-Mail-Adresse für den automatisch angelegten Admin-Erstnutzer
# (nur relevant, solange die users-Tabelle noch leer ist). Ohne Angabe
# wird admin@<Hostname aus APP_URL> verwendet.
ADMIN_EMAIL=
# Optional: DeepL-API-Key für automatische Übersetzung von Gerichten nach # Optional: DeepL-API-Key für automatische Übersetzung von Gerichten nach
# Englisch/Polnisch (kostenloser Free-Tier unter deepl.com/de/pro-api, # Englisch/Polnisch (kostenloser Free-Tier unter deepl.com/de/pro-api,
# Free-Keys enden auf ":fx"). Ohne Key bleibt "Automatisch übersetzen" # Free-Keys enden auf ":fx"). Ohne Key bleibt "Automatisch übersetzen"
+7 -7
View File
@@ -15,12 +15,6 @@ WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
# NEXT_PUBLIC_*-Variablen werden zur Build-Zeit ins Client-Bundle eingebacken
ARG NEXT_PUBLIC_SUPABASE_URL
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
RUN npm run build RUN npm run build
# ---------- Runtime ---------- # ---------- Runtime ----------
@@ -35,7 +29,13 @@ RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Von Next's Standalone-Output nicht automatisch mitgetraced (werden nicht
# von App-Routen importiert, sondern eigenständig vor dem Start ausgeführt).
COPY --from=builder --chown=nextjs:nodejs /app/db ./db
COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts
RUN mkdir -p /data/branding && chown nextjs:nodejs /data/branding
USER nextjs USER nextjs
EXPOSE 3000 EXPOSE 3000
CMD ["node", "server.js"] CMD ["sh", "-c", "node db/migrate.mjs && node scripts/bootstrap-admin.mjs && exec node server.js"]
+69 -82
View File
@@ -4,8 +4,11 @@ Digitale Speisekarte für eine Firmenkantine (ca. 250 Mitarbeiter) mit
öffentlicher Wochenansicht, Kantinen-Bereich zur Pflege der Gerichte und öffentlicher Wochenansicht, Kantinen-Bereich zur Pflege der Gerichte und
Admin-Bereich für Benutzerverwaltung und Branding. Admin-Bereich für Benutzerverwaltung und Branding.
**Stack:** Next.js 16 (App Router, TypeScript, Tailwind CSS) · Supabase **Stack:** Next.js 16 (App Router, TypeScript, Tailwind CSS) · selbst
(PostgreSQL, Auth inkl. Passkeys, Storage, Row Level Security) gehostetes PostgreSQL (via `pg`, eigene Migrationen) · eigene Session-Auth
(Passwort via `node:crypto` scrypt, Passkeys via `@simplewebauthn`) ·
DeepL als einzige externe API (optional). Läuft vollständig selbst
gehostet, keine Cloud-Abhängigkeit außer optional DeepL.
Änderungen je Version stehen in [CHANGELOG.md](CHANGELOG.md), Git-Tags Änderungen je Version stehen in [CHANGELOG.md](CHANGELOG.md), Git-Tags
markieren die jeweiligen Versionsstände (`git tag`). markieren die jeweiligen Versionsstände (`git tag`).
@@ -14,14 +17,14 @@ markieren die jeweiligen Versionsstände (`git tag`).
| Bereich | Funktionen | | Bereich | Funktionen |
| --- | --- | | --- | --- |
| Öffentlich (ohne Login) | Wochenplan Mo–Fr mit den Essenszeiten Brotzeit (09:00–10:00), Mittagessen, Mittagessen vegetarisch und Suppe (je 11:30–13:30); Navigation vorherige/nächste Woche/Heute; auf schmalen Bildschirmen umschaltbare Tages-/Wochenansicht (Tag ist Standard); Logo zentriert über der Karte; Allergen-/Zusatzstoff-Kürzel je Gericht, Legende unterhalb der Karte; Sprachumschalter Deutsch/Englisch/Polnisch; Rezeptvorschläge einreichen | | Öffentlich (ohne Login) | Wochenplan Mo–Fr mit frei konfigurierbaren Essenszeiten; Navigation vorherige/nächste Woche/Heute; auf schmalen Bildschirmen umschaltbare Tages-/Wochenansicht (Tag ist Standard); Logo zentriert über der Karte; Allergen-/Zusatzstoff-Kürzel je Gericht, Legende unterhalb der Karte; Sprachumschalter Deutsch/Englisch/Polnisch; Rezeptvorschläge einreichen |
| Kantinen-Team (`/intern`) | Gerichte anlegen/bearbeiten/löschen (inkl. Live-Suche, Mehrfachauswahl zum Batch-Löschen, englischem/polnischem Namen mit automatischer DeepL-Übersetzung) mit Allergenen/Zusatzstoffen je Gericht; Gerichte Tagen/Essenszeiten zuweisen (mehrere pro Zelle) und wieder entfernen; globale Allergen-/Zusatzstofflisten pflegen (ebenfalls mehrsprachig); eingereichte Rezeptvorschläge sichten, übernehmen oder ablehnen; eigenes Konto verwalten (Name, E-Mail, Passwort, Passkeys, Passwort-Anmeldung an/aus) | | Kantinen-Team (`/intern`) | Essenszeiten anlegen/bearbeiten/löschen/neu anordnen; Gerichte anlegen/bearbeiten/löschen (inkl. Live-Suche, Mehrfachauswahl zum Batch-Löschen, englischem/polnischem Namen mit automatischer DeepL-Übersetzung) mit Allergenen/Zusatzstoffen je Gericht; Gerichte Tagen/Essenszeiten zuweisen (mehrere pro Zelle) und wieder entfernen; globale Allergen-/Zusatzstofflisten pflegen (ebenfalls mehrsprachig); eingereichte Rezeptvorschläge sichten, übernehmen oder ablehnen; eigenes Konto verwalten (Name, E-Mail, Passwort, Passkeys, Passwort-Anmeldung an/aus) |
| Admin (`/admin`) | Kantinenmitarbeiter anlegen/bearbeiten/löschen (inkl. Passwort-Reset mit Wiederholung und Rollenvergabe); Farbgebung der Speisekarte anpassen; Logo und Favicon hochladen/entfernen | | Admin (`/admin`) | Kantinenmitarbeiter anlegen/bearbeiten/löschen (inkl. Passwort-Reset mit Wiederholung und Rollenvergabe); Farbgebung der Speisekarte anpassen; Logo und Favicon hochladen/entfernen |
Anmeldung mit E-Mail + Passwort oder alternativ per **Passkey** Anmeldung mit E-Mail + Passwort oder alternativ per **Passkey**
(Registrierung im eigenen Konto nach dem ersten Login). Die (Registrierung im eigenen Konto nach dem ersten Login). Die
Passwort-Anmeldung kann pro Konto deaktiviert werden, sobald mindestens Passwort-Anmeldung kann pro Konto deaktiviert werden, sobald mindestens
ein Passkey registriert ist. ein Passkey registriert ist (serverseitig erzwungen).
Die gesamte Oberfläche ist auf Deutsch, Englisch und Polnisch verfügbar Die gesamte Oberfläche ist auf Deutsch, Englisch und Polnisch verfügbar
(Sprachumschalter, Cookie-basiert). Gerichte, Essenszeiten, Allergene und (Sprachumschalter, Cookie-basiert). Gerichte, Essenszeiten, Allergene und
@@ -39,18 +42,29 @@ app/ Next.js-Routen
intern/ Kantinen-Bereich (Rolle: staff oder admin) intern/ Kantinen-Bereich (Rolle: staff oder admin)
vorschlaege/ Rezeptvorschläge sichten/übernehmen vorschlaege/ Rezeptvorschläge sichten/übernehmen
admin/ Verwaltung (Rolle: admin) admin/ Verwaltung (Rolle: admin)
api/admin/users/ Benutzerverwaltung (serverseitig, Service-Role-Key) branding/[filename]/ Logo/Favicon ausliefern (öffentlich)
api/translate/ DeepL-Anbindung (serverseitig) api/auth/ Login, Logout, Passkeys
api/account/ Eigenes Konto (Name, E-Mail, Passwort, Toggle)
api/admin/users/ Benutzerverwaltung (nur Admin)
api/admin/branding/ Logo-/Favicon-Upload (nur Admin)
api/dishes/, api/sections/, api/allergens/, api/additives/,
api/menu-entries/, api/dish-suggestions/, api/settings/
CRUD-Routen für den Kantinen-/Admin-Bereich
api/translate/ DeepL-Anbindung (serverseitig)
api/locale/ Sprach-Cookie setzen api/locale/ Sprach-Cookie setzen
components/ Client-Komponenten (WeekEditor, DishManager, …) components/ Client-Komponenten (WeekEditor, DishManager, …)
components/i18n/ Sprachkontext, Sprachumschalter components/i18n/ Sprachkontext, Sprachumschalter
lib/i18n/ Wörterbücher (de/en/pl), Locale-Helfer lib/i18n/ Wörterbücher (de/en/pl), Locale-Helfer
lib/supabase/ Supabase-Clients (Browser / Server / Admin) lib/auth/ Sessions, Passwort-Hashing, WebAuthn-Konfiguration
lib/db/ pg-Pool, Query-Helfer, gemeinsame Ladefunktionen
lib/branding.ts Lokale Datei-Ablage für Logo/Favicon
lib/ Typen, Datums-/Formatierungshelfer lib/ Typen, Datums-/Formatierungshelfer
proxy.ts Session-Refresh + Schutz von /intern und /admin proxy.ts Session-Prüfung + Schutz von /intern, /admin, /konto
supabase/migrations/ Schema & Seed-Daten (Referenz/Doku) db/migrations/ Datenbankschema (reines SQL, nummeriert)
supabase/local/ Skripte + Dumps für die lokale PostgreSQL-Kopie db/migrate.mjs Wendet neue Migrationen an (idempotent, bei jedem Start)
scripts/ Hilfsskripte (DB-Migration, lokaler Sync) scripts/bootstrap-admin.mjs
Legt bei leerer Datenbank automatisch einen
Admin mit Zufallspasswort an (siehe ZUGANGSDATEN.md)
``` ```
## Entwicklung ## Entwicklung
@@ -66,25 +80,25 @@ docker compose --env-file .env.local --profile dev up dev
# http://localhost:3000 (Hot-Reload, Quellcode wird gemountet) # http://localhost:3000 (Hot-Reload, Quellcode wird gemountet)
``` ```
**Variante B – lokal installiertes Node.js:** **Variante B – lokal installiertes Node.js + PostgreSQL:**
```bash ```bash
npm install npm install
npm run dev # http://localhost:3000 (bindet an 0.0.0.0, auch im LAN erreichbar) npm run dev # http://localhost:3000 (bindet an 0.0.0.0, auch im LAN erreichbar)
``` ```
## Umgebungsvariablen (Live-Betrieb) In beiden Varianten wird die Datenbank beim Start automatisch auf den
neuesten Stand migriert und – falls noch kein Benutzer existiert – ein
Admin-Konto mit Zufallspasswort angelegt (siehe [Zugangsdaten](#zugangsdaten)).
Beim Webserver/Hoster müssen genau diese drei Variablen hinterlegt werden ## Umgebungsvariablen (Live-Betrieb)
(Werte: Supabase-Dashboard → Project Settings → API Keys):
| Variable | Bedeutung | Geheim? | | Variable | Bedeutung | Geheim? |
| --- | --- | --- | | --- | --- | --- |
| `NEXT_PUBLIC_SUPABASE_URL` | URL des Supabase-Projekts, z. B. `https://hvzikwjoyhoakedlywhj.supabase.co` | nein | | `DATABASE_URL` | Verbindung zur PostgreSQL-Datenbank, z. B. `postgres://postgres:<passwort>@db:5432/canteen_menu` | **ja** |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Öffentlicher anon-Key (RLS schützt die Daten) | nein | | `APP_URL` | Öffentliche URL der App, z. B. `https://menu.example.de` (Origin-Check hinter Reverse-Proxy; zugleich Passkey-RP-Domain – wird direkt daraus abgeleitet, kein separater Konfigurationsschritt nötig) | nein |
| `SUPABASE_SERVICE_ROLE_KEY` | Service-Role-Key für die Benutzerverwaltung | **ja – niemals im Browser/Repo!** |
| `APP_URL` | Öffentliche URL der App, z. B. `https://menu.example.de` (Origin-Check hinter Reverse-Proxy; zugleich Passkey-Domain) | nein |
| `SITE_TITLE` | Titel im Browser-Tab und Überschrift ohne Logo, Default `Canteen Menu` | nein | | `SITE_TITLE` | Titel im Browser-Tab und Überschrift ohne Logo, Default `Canteen Menu` | nein |
| `ADMIN_EMAIL` | Optional: E-Mail des automatisch angelegten Admin-Erstnutzers (nur relevant, solange die Datenbank leer ist). Ohne Angabe: `admin@<Hostname aus APP_URL>` | nein |
| `DEEPL_API_KEY` | Optional: aktiviert „Automatisch übersetzen“ für Gerichte/Allergene/Zusatzstoffe (DeepL, kostenloser Free-Tier). Ohne Key funktioniert alles weiter, nur automatisch übersetzen ist deaktiviert | **ja – niemals im Browser/Repo!** | | `DEEPL_API_KEY` | Optional: aktiviert „Automatisch übersetzen“ für Gerichte/Allergene/Zusatzstoffe (DeepL, kostenloser Free-Tier). Ohne Key funktioniert alles weiter, nur automatisch übersetzen ist deaktiviert | **ja – niemals im Browser/Repo!** |
Build & Start auf dem Server – **mit Docker** (empfohlen, Node.js und Build & Start auf dem Server – **mit Docker** (empfohlen, Node.js und
@@ -93,82 +107,55 @@ PostgreSQL müssen nicht installiert sein):
```bash ```bash
docker compose --env-file .env.local up -d --build docker compose --env-file .env.local up -d --build
# App: Port 3000 (hinter Reverse-Proxy mit HTTPS betreiben) # App: Port 3000 (hinter Reverse-Proxy mit HTTPS betreiben)
# Lokale DB-Kopie: Port 5433 (Dienst "db", optional) # PostgreSQL: Port 5433
``` ```
Oder klassisch ohne Docker: Vor dem ersten Start: eine leere `ZUGANGSDATEN.md` im Projektverzeichnis
anlegen (`touch ZUGANGSDATEN.md`), da diese Datei per Bind-Mount in den
Container eingebunden wird und die automatisch generierten Admin-
Zugangsdaten dort landen.
Oder klassisch ohne Docker (mit lokal erreichbarem PostgreSQL):
```bash ```bash
npm ci npm ci
npm run build npm run build
npm run start # Port 3000, bindet an 0.0.0.0 (hinter Reverse-Proxy betreiben) npm run start # Port 3000, bindet an 0.0.0.0 (hinter Reverse-Proxy betreiben)
``` ```
### Passkeys für die Produktions-Domain freischalten Passkeys sind an die Domain gebunden (RP-ID = Hostname aus `APP_URL`).
Vor dem Livegang `APP_URL` auf die öffentliche URL setzen – ein
Passkeys sind an die Domain gebunden. Aktuell ist `localhost` konfiguriert. zusätzlicher Konfigurationsschritt ist nicht nötig. Bereits registrierte
Vor dem Livegang: `APP_URL` auf die öffentliche URL setzen und einmalig Passkeys einer vorherigen Domain verlieren dabei ihre Gültigkeit und
müssen neu registriert werden.
```bash
SUPABASE_ACCESS_TOKEN=<personal-access-token> node scripts/configure-passkeys.mjs
```
ausführen (liest `APP_URL` aus der Umgebung; die URL kann alternativ als
Argument übergeben werden). Ohne lokales Node.js geht es auch über Docker:
```bash
docker run --rm -v .:/app -w /app \
-e SUPABASE_ACCESS_TOKEN=<token> -e APP_URL=https://menu.example.de \
node:24-alpine node scripts/configure-passkeys.mjs
```
Achtung: Bereits registrierte Passkeys der alten Domain verlieren dabei
ihre Gültigkeit und müssen neu registriert werden.
## Datenbank ## Datenbank
### Supabase (führende Datenbank) Reines PostgreSQL (kein Supabase, keine Row-Level-Security-Policies) –
alle Zugriffsrechte werden in den Route-Handlern geprüft (`lib/auth/session.ts`,
`requireRole`).
- Projekt-Ref: `hvzikwjoyhoakedlywhj` (Region `eu-central-1`, Frankfurt) - Schema: `db/migrations/*.sql`, nummeriert, wird von `db/migrate.mjs`
- Schema und Seed-Daten: `supabase/migrations/0001_init.sql` und `0002_seed.sql` automatisch angewendet (idempotent, bei jedem Start des Servers bzw.
- Ausführen weiterer SQL-Dateien: Docker-Containers).
`SUPABASE_ACCESS_TOKEN=<token> node scripts/db-apply.mjs <datei.sql>` - Neue Migration hinzufügen: `db/migrations/000N_beschreibung.sql`
anlegen, wird beim nächsten Start automatisch eingespielt.
- Manuell migrieren: `npm run db:migrate`
Sicherheitsmodell (Row Level Security): Rechte-Modell (in Anwendungscode statt Datenbank-RLS):
- `anon` darf ausschließlich lesen (Speisekarte, Essenszeiten, Allergene, - Öffentlich lesbar (kein Login nötig): Speisekarte, Essenszeiten,
Zusatzstoffe, Einstellungen). Allergene, Zusatzstoffe, Logo/Favicon.
- `staff`/`admin` (Rolle im `profiles`-Eintrag) pflegen Gerichte, - `staff`/`admin` pflegen Gerichte, Essenszeiten, Wochenplan und
Wochenplan und Kennzeichnungen. Kennzeichnungen.
- Nur `admin` ändert Einstellungen (Farben/Logo) und verwaltet Benutzer; - Nur `admin` ändert Einstellungen (Farben/Logo) und verwaltet Benutzer.
das Anlegen/Löschen von Benutzern läuft serverseitig über den - Logo/Favicon liegen als Dateien in `BRANDING_DIR` (Docker-Volume
Service-Role-Key (`app/api/admin/users`). `branding_data`), öffentlich ausgeliefert über `/branding/[filename]`.
- Storage-Bucket `branding` (Logo): öffentlich lesbar, Schreibzugriff nur Admin.
### Lokale PostgreSQL-Kopie
Die Datenbank `canteen_menu` (Kopie von Schema + Daten aus Supabase, inkl.
Minimal-Stubs für `auth`/`storage`) gibt es in zwei Varianten:
**Per Docker:** Der Dienst `db` aus `docker-compose.yml` spielt die
Skripte aus `supabase/local/` beim ersten Start automatisch ein
(PostgreSQL 17, Host-Port 5433 – 5432 ist meist durch eine native
PostgreSQL-Installation belegt –, Passwort über `LOCAL_PG_PASSWORD`).
**Nativ:** Auf dem Entwicklungsrechner läuft PostgreSQL 17 mit derselben
Datenbank. Neu aufbauen bzw. aktualisieren:
```powershell
$env:SUPABASE_DB_PASSWORD = "<Supabase-DB-Passwort>"
$env:LOCAL_PG_PASSWORD = "<lokales postgres-Passwort>"
powershell -File scripts\sync-local-db.ps1
```
Hinweis: Die lokale Kopie dient als Ablage/Backup und für Auswertungen.
Logins (Passwort-Hashes, Passkeys) verbleiben ausschließlich in Supabase.
## Zugangsdaten ## Zugangsdaten
Die Anmeldedaten (Admin, Demo-Mitarbeiter, Datenbank-Passwörter) stehen in Beim allerersten Start mit leerer Datenbank legt `scripts/bootstrap-admin.mjs`
`ZUGANGSDATEN.md` – diese Datei ist bewusst **nicht** im Repository automatisch einen Admin-Benutzer mit zufällig generiertem Passwort an und
(`.gitignore`). Nach dem ersten Login bitte die Startpasswörter ändern. schreibt die Zugangsdaten nach `ZUGANGSDATEN.md` – diese Datei ist bewusst
**nicht** im Repository (`.gitignore`). Nach dem ersten Login bitte das
Startpasswort ändern.
+19 -20
View File
@@ -1,15 +1,12 @@
# ============================================================= # =============================================================
# Canteen Menu – Docker-Setup # Canteen Menu – Docker-Setup
# #
# Produktion (App + lokale PostgreSQL-Kopie): # Produktion (App + PostgreSQL):
# docker compose --env-file .env.local up -d --build # docker compose --env-file .env.local up -d --build
# #
# Entwicklung (Dev-Server mit Hot-Reload, kein lokales Node nötig): # Entwicklung (Dev-Server mit Hot-Reload, kein lokales Node nötig):
# docker compose --env-file .env.local --profile dev up dev # docker compose --env-file .env.local --profile dev up dev
# #
# Die --env-file-Angabe speist NEXT_PUBLIC_* in den Build und
# SUPABASE_SERVICE_ROLE_KEY in die Laufzeit-Umgebung.
#
# "name" fixiert den Compose-Projektnamen explizit auf "canteen-menu" – # "name" fixiert den Compose-Projektnamen explizit auf "canteen-menu" –
# ohne diese Angabe leitet Compose ihn aus dem Ordnernamen ab. # ohne diese Angabe leitet Compose ihn aus dem Ordnernamen ab.
# ============================================================= # =============================================================
@@ -21,32 +18,32 @@ services:
app: app:
build: build:
context: . context: .
args:
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
image: canteen-menu-app image: canteen-menu-app
container_name: canteen-menu-app container_name: canteen-menu-app
environment: environment:
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} DATABASE_URL: postgres://postgres:${LOCAL_PG_PASSWORD:-canteen_menu}@db:5432/canteen_menu
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY}
# Öffentliche URL der App (Domain hinter dem Reverse-Proxy). # Öffentliche URL der App (Domain hinter dem Reverse-Proxy).
# Wird für den CSRF-Origin-Check genutzt; dieselbe URL ist die # Wird für den CSRF-Origin-Check genutzt; dieselbe URL ist auch die
# Passkey-Domain (siehe scripts/configure-passkeys.mjs). # Passkey-Domain (RP-ID/Origin werden direkt daraus abgeleitet).
APP_URL: ${APP_URL:-http://localhost:3000} APP_URL: ${APP_URL:-http://localhost:3000}
# Titel im Browser-Tab und Überschrift, solange kein Logo hochgeladen ist. # Titel im Browser-Tab und Überschrift, solange kein Logo hochgeladen ist.
SITE_TITLE: ${SITE_TITLE:-Canteen Menu} SITE_TITLE: ${SITE_TITLE:-Canteen Menu}
# Optional: E-Mail des automatisch angelegten Admin-Erstnutzers.
ADMIN_EMAIL: ${ADMIN_EMAIL:-}
# Optional: DeepL-API-Key für automatische Übersetzung von Gerichten # Optional: DeepL-API-Key für automatische Übersetzung von Gerichten
# (Kürzel-Erklärungen, Bereichen) nach Englisch/Polnisch. Ohne Key # (Essenszeiten, Kürzel-Erklärungen) nach Englisch/Polnisch. Ohne Key
# bleibt "Automatisch übersetzen" inaktiv, manuelle Eingabe geht immer. # bleibt "Automatisch übersetzen" inaktiv, manuelle Eingabe geht immer.
DEEPL_API_KEY: ${DEEPL_API_KEY:-} DEEPL_API_KEY: ${DEEPL_API_KEY:-}
ports: ports:
- "0.0.0.0:3000:3000" - "0.0.0.0:3000:3000"
volumes:
- branding_data:/data/branding
- ./ZUGANGSDATEN.md:/app/ZUGANGSDATEN.md
depends_on:
- db
restart: unless-stopped restart: unless-stopped
# ---------- Lokale PostgreSQL-Kopie (Ablage/Auswertungen) ---------- # ---------- PostgreSQL (primäre Datenbank) ----------
# Beim ersten Start werden die Skripte aus supabase/local
# automatisch in alphabetischer Reihenfolge eingespielt.
db: db:
image: postgres:17-alpine image: postgres:17-alpine
container_name: canteen-menu-db container_name: canteen-menu-db
@@ -59,7 +56,6 @@ services:
- "5433:5432" - "5433:5432"
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
- ./supabase/local:/docker-entrypoint-initdb.d:ro
restart: unless-stopped restart: unless-stopped
# ---------- Dev-Server (nur mit --profile dev) ---------- # ---------- Dev-Server (nur mit --profile dev) ----------
@@ -70,18 +66,21 @@ services:
working_dir: /app working_dir: /app
command: sh -c "npm install && npm run dev" command: sh -c "npm install && npm run dev"
environment: environment:
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} DATABASE_URL: postgres://postgres:${LOCAL_PG_PASSWORD:-canteen_menu}@db:5432/canteen_menu
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY}
APP_URL: ${APP_URL:-http://localhost:3000} APP_URL: ${APP_URL:-http://localhost:3000}
SITE_TITLE: ${SITE_TITLE:-Canteen Menu} SITE_TITLE: ${SITE_TITLE:-Canteen Menu}
ADMIN_EMAIL: ${ADMIN_EMAIL:-}
DEEPL_API_KEY: ${DEEPL_API_KEY:-} DEEPL_API_KEY: ${DEEPL_API_KEY:-}
ports: ports:
- "0.0.0.0:3000:3000" - "0.0.0.0:3000:3000"
volumes: volumes:
- .:/app - .:/app
- dev_node_modules:/app/node_modules - dev_node_modules:/app/node_modules
- branding_data:/app/data/branding
depends_on:
- db
volumes: volumes:
pgdata: pgdata:
dev_node_modules: dev_node_modules:
branding_data:
-120
View File
@@ -10,8 +10,6 @@
"dependencies": { "dependencies": {
"@simplewebauthn/browser": "^13.3.0", "@simplewebauthn/browser": "^13.3.0",
"@simplewebauthn/server": "^13.3.2", "@simplewebauthn/server": "^13.3.2",
"@supabase/ssr": "^0.12.3",
"@supabase/supabase-js": "^2.110.7",
"next": "16.2.10", "next": "16.2.10",
"pg": "^8.22.0", "pg": "^8.22.0",
"react": "19.2.4", "react": "19.2.4",
@@ -1525,102 +1523,6 @@
"node": ">=20.0.0" "node": ">=20.0.0"
} }
}, },
"node_modules/@supabase/auth-js": {
"version": "2.110.7",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.110.7.tgz",
"integrity": "sha512-M5Bpl4hCv6kHcOO/xM06Dyfg1mYLHljMkp1plhzG9IRZPc3czvyMsSN1XpL5+GKisOKM3lSN59zhpcm6sMVXfA==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.110.7",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.110.7.tgz",
"integrity": "sha512-megYmexlYEoR/0qlsr4Snh9wtzAodO7MAri3NMevZrXzNvQRKlvmTcSBoKGLQEPDakgDZMqbMdf9DwoZz6qfoA==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@supabase/phoenix": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz",
"integrity": "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==",
"license": "MIT"
},
"node_modules/@supabase/postgrest-js": {
"version": "2.110.7",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.110.7.tgz",
"integrity": "sha512-ban6YV0djhVaqVYezlOARKLIuOBSvLLhyQVZjA2nxPrtswhxHCl1+gI4giFgI9ATQAaMNbUZb4JXiuL5lEA/5g==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.110.7",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.110.7.tgz",
"integrity": "sha512-AMtZjyFA2gsmjuxopPNS/sRznLQHG0Ht5x+ytTPTOh3vAcOTUlVRLx7gW4/CONNnbb3PKOkE+HmM35HOSbmomQ==",
"license": "MIT",
"dependencies": {
"@supabase/phoenix": "0.4.5",
"tslib": "2.8.1"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@supabase/ssr": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.12.3.tgz",
"integrity": "sha512-qWXJ/dI7CiYDKyTgIPqJ4Qkh8y5edh2LdF/nkN48z47mmEVzAO2OE+YErYeQ/UemVfonn/F4kFz+lhLqoVMdpw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.2"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.110.5"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.110.7",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.110.7.tgz",
"integrity": "sha512-2tcDE8cjEDy1uKxKavBpKQod1JdMV1jDXQag48TCa+kycmJOltc0yVabC0BUlhOwAl6WykXU2aOsH3ELMtZrmQ==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.110.7",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.110.7.tgz",
"integrity": "sha512-AnfO3A230Shy6RMO7cya3Wl1OcXnABJrzH8vP+fY7/RFjhzcchB7DjKkkTIAntlwekD+GkSFzEvt2tC+D4Fp8w==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.110.7",
"@supabase/functions-js": "2.110.7",
"@supabase/postgrest-js": "2.110.7",
"@supabase/realtime-js": "2.110.7",
"@supabase/storage-js": "2.110.7"
},
"engines": {
"node": ">=22.0.0"
}
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -3150,19 +3052,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4450,15 +4339,6 @@
"hermes-estree": "0.25.1" "hermes-estree": "0.25.1"
} }
}, },
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
-2
View File
@@ -14,8 +14,6 @@
"dependencies": { "dependencies": {
"@simplewebauthn/browser": "^13.3.0", "@simplewebauthn/browser": "^13.3.0",
"@simplewebauthn/server": "^13.3.2", "@simplewebauthn/server": "^13.3.2",
"@supabase/ssr": "^0.12.3",
"@supabase/supabase-js": "^2.110.7",
"next": "16.2.10", "next": "16.2.10",
"pg": "^8.22.0", "pg": "^8.22.0",
"react": "19.2.4", "react": "19.2.4",