Zitadel mit Docker installieren: Cloud-nativer Identity Server mit OIDC, SAML und Multi-Tenancy
Zitadel ist ein IAM-Server in Go mit Multi-Tenancy, OIDC, SAML und Passkeys. Diese Anleitung zeigt die Installation per Docker Compose mit PostgreSQL und Traefik, eigenem Admin-Passwort, Let's-Encrypt-Overlay, Update und Backup.
Werbung Mit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Zitadel ist ein in Go geschriebener IAM-Server mit nativer Multi-Tenancy. Er unterstützt OIDC, OAuth 2.0, SAML 2.0 und SCIM, bietet Passkeys, MFA, Social Logins und eine Admin-Konsole und läuft als ein Docker-Compose-Stack. Gegenüber Keycloak spielt Zitadel seine Stärken aus, wenn Sie mehrere Kunden-Tenants auf einer Instanz verwalten. Diese Anleitung richtet Zitadel v4.20.0 per Docker Compose auf einem Linux-Host ein, von der .env-Datei bis zur ersten Anmeldung in der Konsole.
Voraussetzungen
- Docker Engine ab 24 mit Compose-Plugin ab 2 (
docker compose), siehe Docker und Docker Compose auf Linux installieren - Linux-Host oder VM, z. B. Ubuntu 24.04 oder Debian 12, mit
openssl - Mindestens 1 GB RAM und 2 CPU-Kerne; für den Produktivbetrieb 4 CPU-Kerne, da Passwort-Hashing CPU-Spitzen erzeugt
- 2 GB freier Speicher, Zugriff auf
ghcr.io - Für den Produktivbetrieb: eigene Domain, deren DNS-Eintrag auf den Server zeigt, sowie offene Ports 80 und 443 für Let's Encrypt
Schritt 1: Eckdaten und Architektur verstehen
Ab Version 4 besteht der Stack aus vier Containern: zitadel-api (Go-Backend), zitadel-login (Next.js-Login-UI), postgres und proxy (Traefik, einziger nach außen erreichbarer Dienst). zitadel-api schreibt beim ersten Start das Token login-client.pat in das Volume zitadel-bootstrap, ohne dieses startet die Login-UI nicht.
| Komponente | Image | Interner Port | Architektur |
|---|---|---|---|
| zitadel-api | ghcr.io/zitadel/zitadel:v4.20.0 | 8080/tcp (h2c) | amd64, arm64 |
| zitadel-login | ghcr.io/zitadel/zitadel-login:v4.20.0 | 3000/tcp | amd64, arm64 |
| proxy (Traefik) | traefik:v3.7.13 | 80 → Host 127.0.0.1:8080 | amd64, arm64 |
| postgres | postgres:17.11-alpine | 5432/tcp | amd64, arm64 |
| Volume | Zweck |
|---|---|
| postgres-data | Datenbank mit allen Nutzer- und Konfigurationsdaten |
| zitadel-bootstrap | login-client.pat (API schreibt, Login-UI liest) |
Prüfen Sie die Docker- und Compose-Versionen:
$ docker --version
$ docker compose versionErwartet: Docker ab 24 und Compose ab 2.
Schritt 2: Projektordner anlegen
Legen Sie einen Ordner für den Stack an. compose.yaml und .env liegen später gemeinsam darin:
$ sudo mkdir -p /opt/zitadel
$ sudo chown "$USER": /opt/zitadel
$ cd /opt/zitadel$ ls -la /opt/zitadelDer Ordner ist vorhanden, leer und gehört Ihrem Benutzer.
Schritt 3: .env-Datei mit Secrets anlegen
Der ZITADEL_MASTERKEY muss exakt 32 Zeichen lang sein und verschlüsselt sensible Daten in der Datenbank. Nach dem ersten Start lässt er sich nicht mehr ändern; geht er verloren, sind diese Daten nicht mehr lesbar. Bewahren Sie ihn in einem Passwortmanager auf. Erzeugen Sie Masterkey, Cookie-Secret und Datenbankpasswort mit OpenSSL:
$ openssl rand -base64 24 # Masterkey, genau 32 Zeichen
$ openssl rand -base64 32 # Cookie-Secret, 44 Zeichen
$ openssl rand -hex 16 # PostgreSQL-PasswortErstellen Sie nun die .env-Datei und setzen Sie die erzeugten Werte ein:
# Zitadel Stack: .env, nicht in Git einchecken
ZITADEL_VERSION=v4.20.0
# exakt 32 Zeichen, nach dem ersten Start nicht mehr ändern
ZITADEL_MASTERKEY=ErzeugterMasterkey32ZeichenLang0
# Signiert das Session-Cookie der Login-UI, mindestens 32 Zeichen
LOGIN_SESSION_COOKIE_SECRET=HierDasErzeugteCookieSecretEinsetzen
# Passwort des ersten Admins (zitadel-admin), vor dem ersten Start setzen
# mind. 8 Zeichen mit Groß-, Kleinbuchstaben, Ziffer und Sonderzeichen
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD=EigenesAdminPasswort!2026
# Domain und externe URL (VOR dem ersten Start korrekt setzen!)
ZITADEL_DOMAIN=localhost
ZITADEL_EXTERNALPORT=8080
ZITADEL_EXTERNALSECURE=false
ZITADEL_PUBLIC_SCHEME=http
# Host-Port von Traefik (in compose.yaml nur an 127.0.0.1 gebunden)
PROXY_HTTP_PUBLISHED_PORT=8080
# PostgreSQL-Zugangsdaten (für Produktion starke Passwörter setzen!)
POSTGRES_ADMIN_USER=postgres
POSTGRES_ADMIN_PASSWORD=ErzeugtesPostgresPasswort
POSTGRES_DB=zitadel
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://postgres:SicheresPostgresPasswort42!@postgres:5432/zitadel?sslmode=disable
# Internes Service-Token-Ablaufdatum
LOGIN_CLIENT_PAT_EXPIRATION=2099-01-01T00:00:00Z
# Gepinnte Image-Tags für Infrastruktur-Container
TRAEFIK_IMAGE=traefik:v3.7.13
POSTGRES_IMAGE=postgres:17.11-alpineDas Passwort in ZITADEL_DATABASE_POSTGRES_DSN muss mit POSTGRES_ADMIN_PASSWORD übereinstimmen. Zitadel empfiehlt für den Produktivbetrieb in der DSN eine eigene Rolle ohne Superuser-Rechte. Schützen Sie die Datei mit chmod 600 .env. Setzen Sie ZITADEL_DOMAIN vor dem ersten Start endgültig; eine spätere Änderung ändert die Issuer-URL und macht Tokens ungültig.
$ grep ZITADEL_MASTERKEY /opt/zitadel/.env | awk -F= '{print length($2)}'Die Ausgabe ist 32.
Schritt 4: compose.yaml anlegen
Die folgende compose.yaml entspricht dem offiziellen Compose-Beispiel von Zitadel, ergänzt um die lokale Port-Bindung. Das Label loadbalancer.server.scheme=h2c ist Pflicht, weil Zitadel intern HTTP/2 und gRPC nutzt; ohne das Label antwortet Traefik mit 502. Die Healthchecks und depends_on-Bedingungen sorgen dafür, dass PostgreSQL vor Zitadel und Zitadel vor Traefik bereit ist.
name: zitadel
services:
proxy:
image: ${TRAEFIK_IMAGE:-traefik:v3.7.13}
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --providers.docker.network=zitadel
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --api.dashboard=false
- --api.insecure=false
- --ping=true
- --ping.entrypoint=web
- --log.level=INFO
- --accesslog=true
ports:
- "127.0.0.1:${PROXY_HTTP_PUBLISHED_PORT:-8080}:80"
networks:
- zitadel
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
depends_on:
zitadel-api:
condition: service_healthy
zitadel-login:
condition: service_healthy
zitadel-api:
image: ghcr.io/zitadel/zitadel:${ZITADEL_VERSION:-v4.20.0}
restart: unless-stopped
user: "0"
command: start-from-init --masterkey "${ZITADEL_MASTERKEY}"
environment:
ZITADEL_PORT: 8080
ZITADEL_EXTERNALDOMAIN: ${ZITADEL_DOMAIN:-localhost}
ZITADEL_EXTERNALPORT: ${ZITADEL_EXTERNALPORT:-8080}
ZITADEL_EXTERNALSECURE: ${ZITADEL_EXTERNALSECURE:-false}
ZITADEL_TLS_ENABLED: "false"
ZITADEL_DATABASE_POSTGRES_DSN: ${ZITADEL_DATABASE_POSTGRES_DSN}
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: "false"
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: ${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD}
ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /zitadel/bootstrap/login-client.pat
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: "Automatically Initialized IAM_LOGIN_CLIENT"
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: ${LOGIN_CLIENT_PAT_EXPIRATION:-2099-01-01T00:00:00Z}
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: "true"
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: "${ZITADEL_PUBLIC_SCHEME:-http}://${ZITADEL_DOMAIN:-localhost}:${ZITADEL_EXTERNALPORT:-8080}/ui/v2/login/"
ZITADEL_OIDC_DEFAULTLOGINURLV2: "${ZITADEL_PUBLIC_SCHEME:-http}://${ZITADEL_DOMAIN:-localhost}:${ZITADEL_EXTERNALPORT:-8080}/ui/v2/login/login?authRequest="
ZITADEL_OIDC_DEFAULTLOGOUTURLV2: "${ZITADEL_PUBLIC_SCHEME:-http}://${ZITADEL_DOMAIN:-localhost}:${ZITADEL_EXTERNALPORT:-8080}/ui/v2/login/logout?post_logout_redirect="
ZITADEL_SAML_DEFAULTLOGINURLV2: "${ZITADEL_PUBLIC_SCHEME:-http}://${ZITADEL_DOMAIN:-localhost}:${ZITADEL_EXTERNALPORT:-8080}/ui/v2/login/login?samlRequest="
ZITADEL_LOGSTORE_ACCESS_STDOUT_ENABLED: "true"
ZITADEL_CACHES_CONNECTORS_REDIS_ENABLED: "false"
healthcheck:
test:
- CMD
- /app/zitadel
- ready
interval: 10s
timeout: 30s
retries: 12
start_period: 20s
volumes:
- zitadel-bootstrap:/zitadel/bootstrap:rw
networks:
- zitadel
depends_on:
postgres:
condition: service_healthy
labels:
- traefik.enable=true
- traefik.docker.network=zitadel
- traefik.http.services.zitadel-api.loadbalancer.server.port=8080
- traefik.http.services.zitadel-api.loadbalancer.server.scheme=h2c
- traefik.http.middlewares.zitadel-strip-api.stripprefix.prefixes=/api
- traefik.http.middlewares.zitadel-strip-api.stripprefix.forceSlash=false
- "traefik.http.routers.zitadel-api-alias-web.rule=Host(`${ZITADEL_DOMAIN:-localhost}`) && PathPrefix(`/api`)"
- traefik.http.routers.zitadel-api-alias-web.entrypoints=web
- traefik.http.routers.zitadel-api-alias-web.middlewares=zitadel-strip-api
- traefik.http.routers.zitadel-api-alias-web.service=zitadel-api
- traefik.http.routers.zitadel-api-alias-web.priority=200
- "traefik.http.routers.zitadel-canonical-web.rule=Host(`${ZITADEL_DOMAIN:-localhost}`) && !PathPrefix(`/ui/v2/login`) && !PathPrefix(`/api`) && !Path(`/`)"
- traefik.http.routers.zitadel-canonical-web.entrypoints=web
- traefik.http.routers.zitadel-canonical-web.service=zitadel-api
- traefik.http.routers.zitadel-canonical-web.priority=100
zitadel-login:
image: ghcr.io/zitadel/zitadel-login:${ZITADEL_VERSION:-v4.20.0}
restart: unless-stopped
user: "0"
environment:
ZITADEL_API_URL: http://zitadel-api:8080
NEXT_PUBLIC_BASE_PATH: /ui/v2/login
ZITADEL_SERVICE_USER_TOKEN_FILE: /zitadel/bootstrap/login-client.pat
ZITADEL_SESSION_COOKIE_SECRET: ${LOGIN_SESSION_COOKIE_SECRET}
CUSTOM_REQUEST_HEADERS: "Host:${ZITADEL_DOMAIN:-localhost},X-Forwarded-Proto:${ZITADEL_PUBLIC_SCHEME:-http}"
healthcheck:
test:
- CMD
- /bin/sh
- -c
- "node /app/healthcheck.mjs http://localhost:3000/ui/v2/login/healthy"
interval: 10s
timeout: 30s
retries: 12
start_period: 20s
volumes:
- zitadel-bootstrap:/zitadel/bootstrap:ro
networks:
- zitadel
depends_on:
zitadel-api:
condition: service_healthy
labels:
- traefik.enable=true
- traefik.docker.network=zitadel
- traefik.http.services.zitadel-login.loadbalancer.server.port=3000
- traefik.http.middlewares.zitadel-root-rewrite.replacepath.path=/ui/v2/login/
- "traefik.http.routers.zitadel-root-web.rule=Host(`${ZITADEL_DOMAIN:-localhost}`) && Path(`/`)"
- traefik.http.routers.zitadel-root-web.entrypoints=web
- traefik.http.routers.zitadel-root-web.middlewares=zitadel-root-rewrite
- traefik.http.routers.zitadel-root-web.service=zitadel-login
- traefik.http.routers.zitadel-root-web.priority=400
- "traefik.http.routers.zitadel-login-web.rule=Host(`${ZITADEL_DOMAIN:-localhost}`) && PathPrefix(`/ui/v2/login`)"
- traefik.http.routers.zitadel-login-web.entrypoints=web
- traefik.http.routers.zitadel-login-web.service=zitadel-login
- traefik.http.routers.zitadel-login-web.priority=250
postgres:
image: ${POSTGRES_IMAGE:-postgres:17.11-alpine}
restart: unless-stopped
environment:
POSTGRES_PASSWORD: ${POSTGRES_ADMIN_PASSWORD}
POSTGRES_USER: ${POSTGRES_ADMIN_USER:-postgres}
POSTGRES_DB: ${POSTGRES_DB:-zitadel}
healthcheck:
test:
- CMD-SHELL
- "pg_isready -d ${POSTGRES_DB:-zitadel} -U ${POSTGRES_ADMIN_USER:-postgres}"
interval: 10s
timeout: 30s
retries: 10
start_period: 20s
volumes:
- postgres-data:/var/lib/postgresql/data:rw
networks:
- zitadel
networks:
zitadel:
name: zitadel
volumes:
postgres-data:
zitadel-bootstrap:Prüfen Sie die Syntax vor dem Start:
$ docker compose --env-file .env config --quiet && echo "YAML ok"Erwartete Ausgabe: YAML ok ohne Fehlermeldung.
Schritt 5: Stack starten und Initialisierung abwarten
Starten Sie den Stack mit --wait, dann kehrt Docker Compose erst zurück, wenn alle Healthchecks grün sind. Der erste Start dauert 60 bis 120 Sekunden, weil start-from-init das Datenbankschema anlegt.
$ cd /opt/zitadel
$ docker compose --env-file .env up -d --waitBei Problemen verfolgen Sie den Start mit docker compose logs -f zitadel-api.
$ docker compose psAlle vier Dienste zeigen running (healthy), gekürzte Ausgabe:
NAME IMAGE STATUS
zitadel-proxy-1 traefik:v3.7.13 running (healthy)
zitadel-zitadel-api-1 ghcr.io/zitadel/zitadel:v4.20.0 running (healthy)
zitadel-zitadel-login-1 ghcr.io/zitadel/zitadel-login:v4.20.0 running (healthy)
zitadel-postgres-1 postgres:17.11-alpine running (healthy)Prüfen Sie zusätzlich, ob Traefik antwortet:
$ curl -I http://localhost:8080/Erwartete Antwort: HTTP 200 oder 302 (Weiterleitung zum Login).
Schritt 6: Admin-Zugangsdaten auslesen und erste Anmeldung
Zitadel legt beim ersten Start eine Organisation und den Admin zitadel-admin@zitadel.localhost an. Das Passwort steht nicht in den Logs, es ist der Wert aus ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD. Ohne diese Variable gilt der dokumentierte Standard Password1!, den Sie dann sofort ändern. Öffnen Sie die Konsole:
http://localhost:8080/ui/console?login_hint=zitadel-admin@zitadel.localhostRichten Sie nach der Anmeldung für den Admin direkt MFA ein. Von einem anderen Rechner erreichen Sie die lokal gebundene Konsole per SSH-Tunnel: ssh -L 8080:127.0.0.1:8080 user@server.
Prüfen Sie den OIDC-Discovery-Endpunkt:
$ curl -s http://localhost:8080/.well-known/openid-configuration | python3 -m json.tool | head -10Die Ausgabe zeigt das OIDC-Dokument. Der Wert issuer lautet http://localhost:8080: Domain und Port stimmen also.
Schritt 7: HTTPS und Produktionsbetrieb vorbereiten
Für den Produktivbetrieb ist HTTPS Pflicht. Terminiert ein Proxy HTTPS, während ZITADEL_EXTERNALSECURE=false gesetzt ist, erzeugt Zitadel HTTP-Issuer-URLs und OIDC-Clients lehnen die Tokens ab. Passen Sie die .env an:
ZITADEL_DOMAIN=auth.example.com
ZITADEL_EXTERNALPORT=443
ZITADEL_EXTERNALSECURE=true
ZITADEL_PUBLIC_SCHEME=https
LETSENCRYPT_EMAIL=admin@example.comZitadel stellt für Let's Encrypt ein Overlay bereit, das Traefik auf den Ports 80 und 443 veröffentlicht und das Zertifikat per HTTP-Challenge bezieht. Laden Sie es passend zur Version und starten Sie den Stack damit:
$ curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/v4.20.0/deploy/compose/docker-compose.mode-letsencrypt.yml
$ docker compose --env-file .env -f compose.yaml -f docker-compose.mode-letsencrypt.yml up -d --waitDie Ports-Liste des Overlays ersetzt die lokale Bindung aus compose.yaml; Traefik ist danach nur auf 80 und 443 erreichbar.
Alternativ betreiben Sie Zitadel hinter einem vorhandenen Traefik, siehe Traefik als Docker-Reverse-Proxy mit automatischem HTTPS einrichten.
Nach dem Start mit TLS:
$ curl -I https://auth.example.com/.well-known/openid-configurationErwartete Antwort: HTTP 200, der issuer im JSON lautet https://auth.example.com.
Schritt 8: Update-Prozess und Backup
Alle Daten liegen in PostgreSQL. Sichern Sie die Datenbank und separat den Masterkey; ohne ihn ist ein Backup wertlos. Erstellen Sie vor jedem Update ein Backup.
Update: Lesen Sie die Release Notes, tragen Sie die neue Version in der .env ein und führen Sie aus:
$ docker compose --env-file .env pull
$ docker compose --env-file .env up -d --waitZitadel führt Datenbankmigrationen beim Start über start-from-init automatisch aus.
Hinweise für das Update von 4.19.3 auf 4.20.0: Laut Release Notes erzwingen alle Listen-Endpunkte ab 4.19.5 Standard- und Höchstwerte für die Abfragegröße (Advisory A10018), und Geräte-Anmeldungen (Device Flow) verlangen eine Bestätigung auf einer Zustimmungsseite. Die Login-UI v2 signiert ab 4.19.5 auch E-Mail- und Einladungsbestätigung, Passkey-Registrierung und IdP-Verknüpfung mit ZITADEL_SESSION_COOKIE_SECRET, ab 4.20.0 zusätzlich die Device-Code-Prüfung. Steht in Ihrer .env noch ein Beispielwert wie SessionCookieSecretNeedsAtLeast32Chars, ersetzen Sie ihn durch einen Zufallswert aus Schritt 3. Wer während des Updates mitten in einem dieser Schritte steckt, muss ihn wiederholen. 4.20.0 ist mit Go 1.26 gebaut: Ausgehende TLS-Verbindungen (SMTP, externe IdPs, Actions, Webhooks, SMS-Anbieter) lehnen Zertifikatsketten über die kompromittierten eMudhra-Zwischenzertifizierungsstellen ab, betroffene Gegenstellen brauchen ein neues Zertifikat. Bricht nach dem Update eine konfigurierte URL oder ausgehende Verbindung ab, stellen GODEBUG=urlstrictcolons=0 oder GODEBUG=tlssecpmlkem=0 das alte Verhalten wieder her. Das Update von 4.19.3 auf 4.20.0 lief mit der Compose-Datei aus Schritt 4 und den beiden Befehlen oben: Die Migrationen endeten mit setup completed, alle Container waren healthy, der Issuer unter /.well-known/openid-configuration blieb gleich, die Anmeldung des Admins mit dem bestehenden Passwort über die Session-API gelang, ein falsches Passwort wurde mit Password is invalid abgewiesen, und die Ereignisse aus der alten Version blieben in der Datenbank.
Datenbank-Backup:
sudo mkdir -p /opt/backups
docker compose exec -T postgres pg_dump -U postgres zitadel \
| gzip | sudo tee /opt/backups/zitadel_$(date +%Y%m%d_%H%M).sql.gz >/dev/nullEine vollständige Backup-Automatisierung mit cron und Cloud-Sync zeigt die Anleitung MySQL & PostgreSQL Backup automatisieren mit cron.
gunzip -t auf die Backup-Datei meldet keinen Fehler. Nach einem Update:
$ docker compose ps
$ docker compose logs zitadel-api 2>&1 | tail -20Alle Container zeigen running (healthy), die Logs enthalten keine Migrationsfehler.
Troubleshooting / Typische Fehler
- „masterkey must be 32 characters long“
beim Start: Der
ZITADEL_MASTERKEYin der.envist nicht exakt 32 Zeichen lang. Prüfen mit dem Befehl aus Schritt 3, Wert korrigieren. Vor dem ersten erfolgreichen Start ist das folgenlos.- zitadel-login startet nicht
login-client.patfehlt noch im Bootstrap-Volume. Prüfen mitdocker compose logs zitadel-api | grep -i pat, danndocker compose up -d --waiterneut ausführen.- 502 Bad Gateway von Traefik
Das Label
loadbalancer.server.scheme=h2cfehlt. Label wiederherstellen und Stack neu starten.- Port-Konflikt auf 8080
Ein anderer Dienst belegt den Port.
PROXY_HTTP_PUBLISHED_PORTundZITADEL_EXTERNALPORTvor dem ersten Start auf denselben freien Port setzen, z. B. 8090.- Anmeldung mit Admin-Passwort scheitert
ZITADEL_FIRSTINSTANCE_*-Werte gelten nur beim ersten Start. Nachträgliche Änderungen in der.envwirken nicht; Passwort in der Konsole ändern.- Login-UI meldet Fehler zum Session-Cookie
LOGIN_SESSION_COOKIE_SECRETfehlt oder ist kürzer als 32 Zeichen.- OIDC-Issuer-URL falsch (http statt https)
ZITADEL_EXTERNALSECURE=trueundZITADEL_PUBLIC_SCHEME=httpssetzen, siehe Schritt 7.- Masterkey nach erstem Start geändert
Die verschlüsselten Daten sind nicht mehr lesbar. Ursprünglichen Masterkey aus dem Passwortmanager wieder eintragen.
Häufige Fragen
Wie lautet die Admin-Konsolen-URL nach dem ersten Start?
Mit den Standardwerten http://localhost:8080/ui/console, nach der TLS-Umstellung https://auth.example.com/ui/console. Der Admin heißt zitadel-admin@zitadel.localhost, das Passwort stammt aus der .env.
Was bedeutet natives Multi-Tenancy bei Zitadel?
Eine Installation betreibt mehrere Instanzen mit eigener Domain, jede mit mehreren Organisationen. Jeder Kunde erhält so eine isolierte Identity-Umgebung ohne eigene Installation.
Wie unterscheidet sich Zitadel von Authentik oder Keycloak?
Keycloak basiert auf Java und braucht mehr Speicher; Authentik ist in Python geschrieben und stärker auf Forward-Auth ausgerichtet. Einen SSO-Vergleich liefert die Anleitung Single Sign-On: Authentik vs. Authelia mit Traefik Forward-Auth absichern.
Kann ich Zitadel ohne Traefik betreiben?
Ja, aber der offizielle Stack steuert das Routing über Traefik-Labels. Mit Nginx oder Caddy konfigurieren Sie die Pfade /api, /ui/v2/login und / selbst und sprechen zitadel-api per h2c (HTTP/2 ohne TLS) an.
Was passiert mit dem Bootstrap-Volume beim Neustart?
Es bleibt erhalten, Zitadel überspringt die Initialisierung. Erst docker compose down -v löscht Volume und Datenbank und damit alle Daten.
Unterstützt Zitadel Passkeys und MFA?
Ja. Zitadel unterstützt Passkeys (WebAuthn/FIDO2), TOTP, SMS, E-Mail-Codes und Social Logins ohne Zusatzsoftware. Sie konfigurieren das in den Login-Einstellungen der Instanz in der Konsole.
Fazit
Der Stack läuft in etwa 20 Minuten. Für den Produktivbetrieb zählen drei Punkte: sicher verwahrter Masterkey, eigenes Admin-Passwort mit MFA und HTTPS mit ZITADEL_EXTERNALSECURE=true.
Weiterführende Anleitungen und Quellen
- Docker Compose absichern
- Docker und Docker Compose auf Linux installieren, die Self-Hosting-Grundlage
- Traefik als Docker-Reverse-Proxy mit automatischem HTTPS einrichten
- Single Sign-On: Authentik vs. Authelia mit Traefik Forward-Auth absichern
- MySQL & PostgreSQL Backup automatisieren mit cron und rclone-Cloud-Sync
- Casdoor mit Docker installieren: Zentraler Auth-Server
- HashiCorp Vault mit Docker installieren
Offizielle Quellen: Zitadel Docs, Docker Compose Deployment | Zitadel GitHub Repository | Zitadel Production Guide


