The revert restored three statements that stopped being true earlier today. "Nicht containerisiert: Supabase (Datenbank + Auth)" — authentication is no longer Supabase, it is Entra ID with an Auth.js session cookie, and the database is any PostgreSQL 15 or later reached through DATABASE_URL. Supabase is one option among several now, not the architecture. The CI/CD note told the reader to pass --build-arg values for NEXT_PUBLIC_*. Those variables no longer exist and the Dockerfile stopped taking build arguments today. Following it would produce a puzzling failure; the point now is the opposite one, that no build arguments are needed at all and the same image runs everywhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.5 KiB
Deployment mit Docker
Dieser Guide beschreibt, wie die App 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_URLzu 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-changesauf, um fällige Versetzungen/ Beförderungen/Karenz/Reorg-Änderungen zu übernehmen). Da es außerhalb von Vercel kein Vercel-Cron gibt, übernimmt das imdocker-compose.ymlenthaltenecron-Sidecar-Container diese Aufgabe mit demselben Schema und demselben Bearer-Secret, das die Route bereits erwartet.
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 pushbzw. wie bisher).
1. .env anlegen
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.
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 überenv_file. Eine Änderung braucht deshalb nur einen Neustart, keinen neuen Build — und dasselbe Image läuft in Test und Produktion. .envsteht schon in.gitignore– nicht committen.
2. Bauen und lokal testen
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:
docker compose logs -f cron
3. Auf einem Server deployen
Einfachste Variante – Repo direkt auf dem Server bauen:
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. Beispiel mit Caddy (automatisches HTTPS via Let's Encrypt):
# /etc/caddy/Caddyfile
hr.example.com {
reverse_proxy localhost:3000
}
docker-compose.yml published Port 3000 aktuell auf den Host – bei
Verwendung eines Reverse Proxys auf demselben Host kann das Publishing auf
127.0.0.1:3000:3000 eingeschränkt werden, damit der Container-Port nicht
direkt von außen erreichbar ist.
5. Updates ausrollen
git pull
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).
Datenbank-Migrationen (supabase/migrations/*.sql) werden weiterhin über
die Supabase CLI gegen das Supabase-Projekt gefahren, unabhängig vom
App-Deployment:
supabase db push
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:
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
- Anmeldung endet auf
/login?error=…: die Umleitungs-URI in der Entra-Registrierung muss exakthttps://<host>/api/auth/callback/microsoft-entra-idlauten. Steht die Anwendung hinter einem Reverse Proxy unter einer anderen Adresse, als sie selbst sieht, zusätzlichAUTH_URLsetzen. - Angemeldet, aber sofort zurück auf
/login?error=no_hr_access: die Anmeldung hat funktioniert, es fehlt die Freischaltung. Es braucht eineprofiles-Zeile mitrole = 'hr'undis_active = trueauf derselben Kennung, die inapp_userssteht. - Cron läuft nicht:
docker compose logs cron– prüft, ob/etc/crontabs/rootkorrekt geschrieben wurde und obCRON_SECRETin.envgesetzt ist (leer/fehlend führt serverseitig zu401). 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– meistDATABASE_URLfehlend 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.