Zitadel mit Docker installieren: Cloud-nativer Identity Server mit OIDC, SAML und Multi-Tenancy
Zitadel ist ein cloud-nativer IAM-Server in Go mit Multi-Tenancy, OIDC, SAML und Passkeys – eine ressourceneffiziente Keycloak-Alternative für SaaS. Diese Anleitung zeigt die vollständige Docker-Compose-Installation mit PostgreSQL und Traefik.

Wer für SaaS-Produkte oder interne Dienste eine zentrale Identitäts- und Zugangsverwaltung sucht, kommt an Zitadel kaum vorbei: Der in Go geschriebene IAM-Server bringt nativ Multi-Tenancy mit, unterstützt OIDC, OAuth 2.0, SAML 2.0 sowie SCIM und bietet Passkeys, MFA, Social Logins und eine vollständige Admin-Konsole – alles in einem einzelnen Docker-Compose-Stack. Mit über 9.000 GitHub-Stars und aktivem Release-Zyklus (aktuell v4.15.1, Juni 2026) ist Zitadel die modernere, ressourceneffizientere Wahl gegenüber Keycloak, wenn es um die Verwaltung mehrerer Kunden-Tenants auf einer einzigen Instanz geht. Diese Anleitung richtet Zitadel v4.15.1 plattformneutral per Docker Compose auf einem Linux-Host ein – von der .env-Datei bis zur ersten Anmeldung in der Konsole.
Voraussetzungen
- Docker Engine >= 24.x und Docker Compose Plugin >= 2.x (Befehl:
docker compose, nicht das veraltetedocker-compose) – Installationsanleitung: Docker und Docker Compose auf Linux installieren - Linux-Host, VM oder NAS mit Docker-Unterstützung (Ubuntu 22.04/24.04 und Debian 12 empfohlen)
- Mindestens 512 MB RAM für Tests; für Produktion mindestens 1 GB, besser 4 CPU-Kerne (Passwort-Hashing erzeugt CPU-Spitzen)
- Mindestens 2 GB freier Festplattenspeicher
- Internetzugang für Image-Pulls von
ghcr.io - Für Produktion: eigene Domain und TLS-Zertifikat (Let's Encrypt wird offiziell unterstützt)
- Optional:
opensslzum Generieren sicherer Schlüssel
Schritt 1: Eckdaten und Architektur verstehen
Ab Version 4.x besteht der Zitadel-Stack aus vier Containern: zitadel-api (Go-Backend, Port 8080 intern), zitadel-login (Next.js-UI, Port 3000 intern), postgres (PostgreSQL 17, Port 5432 intern) und proxy (Traefik v3, der einzige nach außen exponierte Dienst). Das Bootstrap-Volume zitadel-bootstrap wird beim ersten Start von zitadel-api beschrieben und enthält das interne Service-Token login-client.pat, das zitadel-login für die Kommunikation mit der API benötigt. Dieses Shared Volume ist der häufigste Stolperstein – fehlt es oder ist es noch nicht befüllt, startet zitadel-login nicht korrekt.
| Komponente | Image | Interner Port | Architektur |
|---|---|---|---|
| zitadel-api | ghcr.io/zitadel/zitadel:v4.15.1 | 8080/tcp (h2c) | amd64, arm64 |
| zitadel-login | ghcr.io/zitadel/zitadel-login:v4.15.1 | 3000/tcp | amd64, arm64 |
| proxy (Traefik) | traefik:v3.6.8 | 80 → Host 8080 | amd64, arm64 |
| postgres | postgres:17.2-alpine | 5432/tcp | amd64, arm64 |
| Volume | Typ | Zweck |
|---|---|---|
| postgres-data | Named Volume | PostgreSQL-Datenbankdateien (alle Nutzer-/Konfigurationsdaten) |
| zitadel-bootstrap | Named Volume | login-client.pat (internes Service-Token, geteilt zwischen API rw und Login-UI ro) |
Verifizieren: Überprüfe die Docker- und Compose-Versionen vor dem Start:
docker --version
docker compose versionErwartete Ausgabe: Docker Engine >= 24.x und Docker Compose >= 2.x.
Schritt 2: Projektordner anlegen
Lege einen eigenen Ordner für den Zitadel-Stack an. Alle Konfigurationsdateien landen hier:
mkdir -p /opt/zitadel
cd /opt/zitadelDu kannst natürlich auch ~/zitadel oder einen anderen Pfad wählen – wichtig ist, dass compose.yaml und .env im selben Ordner liegen.
Verifizieren:
ls -la /opt/zitadelDer Ordner sollte vorhanden und leer sein.
Schritt 3: .env-Datei mit Secrets anlegen
Die .env-Datei enthält alle umgebungsspezifischen Werte. Der wichtigste Parameter ist der ZITADEL_MASTERKEY: Er muss exakt 32 Zeichen lang sein und darf nach dem ersten Start niemals geändert werden – er verschlüsselt alle sensiblen Daten in der Datenbank. Verlierst du ihn oder änderst ihn, sind diese Daten unwiederbringlich verloren. Bewahre ihn deshalb in einem Passwortmanager oder Vault auf.
Generiere einen sicheren 32-Zeichen-Schlüssel mit OpenSSL:
openssl rand -base64 24Die Base64-Ausgabe von rand -base64 24 ist exakt 32 Zeichen lang. Prüfe die Länge zur Sicherheit:
echo -n 'DeinGenerierterSchluessel32Zeich' | wc -cDie Ausgabe muss 32 sein.
Erstelle nun die .env-Datei:
# Zitadel Stack – .env
# WICHTIG: .env NIEMALS in ein öffentliches Git-Repository einchecken!
# Image-Version (immer konkret pinnen, nie "latest" für Produktion)
ZITADEL_VERSION=v4.15.1
# Masterkey – exakt 32 Zeichen, NIEMALS nach erstem Start ändern!
ZITADEL_MASTERKEY=DeinGenerierterSchluessel32Zeich
# Domain und externe URL (VOR dem ersten Start korrekt setzen!)
ZITADEL_DOMAIN=localhost
ZITADEL_EXTERNALPORT=8080
ZITADEL_EXTERNALSECURE=false
ZITADEL_PUBLIC_SCHEME=http
# Traefik-Eingangsport (Host-Port, auf dem Zitadel erreichbar ist)
PROXY_HTTP_PUBLISHED_PORT=8080
# PostgreSQL-Zugangsdaten (für Produktion starke Passwörter setzen!)
POSTGRES_ADMIN_USER=postgres
POSTGRES_ADMIN_PASSWORD=SicheresPostgresPasswort42!
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.6.8
POSTGRES_IMAGE=postgres:17.2-alpinePasse ZITADEL_DOMAIN für Produktionsinstallationen auf deine echte Domain an (z. B. auth.example.com). Für lokale Tests bleibt localhost korrekt. Achtung: Eine nachträgliche Änderung der Domain ändert die OIDC-Issuer-URL und macht bestehende Tokens ungültig.
Verifizieren:
grep ZITADEL_MASTERKEY /opt/zitadel/.env | awk -F= '{print length($2)}'Die Ausgabe muss 32 sein.
Schritt 4: compose.yaml anlegen
Das folgende compose.yaml bildet den vollständigen Zitadel-Stack ab. Besonders wichtig ist das Traefik-Label loadbalancer.server.scheme=h2c beim zitadel-api-Service – es ist zwingend erforderlich, da Zitadel intern HTTP/2 und gRPC nutzt. Ohne dieses Label antwortet Traefik mit einem 502-Fehler. Alle vier Healthchecks und die depends_on-Ketten stellen sicher, dass PostgreSQL bereit ist, bevor Zitadel startet, und Zitadel bereit ist, bevor Traefik Anfragen weiterleitet.
name: zitadel
services:
proxy:
image: ${TRAEFIK_IMAGE:-traefik:v3.6.8}
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:
- "${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.15.1}
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_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.15.1}
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
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.2-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:Verifizieren: Prüfe die YAML-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
Starte den Stack mit dem Flag --wait, damit Docker Compose erst zurückkehrt, wenn alle Healthchecks grün sind. Der erste Start dauert erfahrungsgemäß 60–120 Sekunden, da Zitadel die Datenbankschemas anlegt (start-from-init initialisiert und migriert in einem Schritt) und das Bootstrap-Verfahren durchläuft.
cd /opt/zitadel
docker compose --env-file .env up -d --waitBeobachte den Startvorgang live:
docker compose logs -f zitadel-apiSuche nach einer Zeile wie server started – das signalisiert, dass die Initialisierung abgeschlossen ist.
Verifizieren:
docker compose psAlle vier Services müssen den Status running (healthy) zeigen:
NAME IMAGE STATUS
zitadel-proxy-1 traefik:v3.6.8 running (healthy)
zitadel-zitadel-api-1 ghcr.io/zitadel/zitadel:v4.15.1 running (healthy)
zitadel-zitadel-login-1 ghcr.io/zitadel/zitadel-login:v4.15.1 running (healthy)
zitadel-postgres-1 postgres:17.2-alpine running (healthy)Prüfe zusätzlich mit curl, 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 automatisch eine erste Organisation und einen Admin-Benutzer an. Die Zugangsdaten erscheinen in den Logs:
docker compose logs zitadel-api 2>&1 | grep -i -E "admin|password|username"Öffne nun im Browser die Zitadel-Konsole:
http://localhost:8080/ui/consoleMelde dich mit dem Admin-Benutzer an, der in den Logs angezeigt wurde. Nach der Anmeldung siehst du die Zitadel-Instanzverwaltung mit Organisation, Benutzer, Anwendungen und Einstellungen.
Verifizieren: Prüfe 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 muss http://localhost:8080 lauten – das bestätigt, dass Domain und Port korrekt konfiguriert sind.
Schritt 7: HTTPS und Produktionsbetrieb vorbereiten
Für den Produktionseinsatz ist HTTPS zwingend erforderlich. ZITADEL_EXTERNALSECURE=false darf ausschließlich lokal verwendet werden – wenn ein vorgelagerter Reverse Proxy HTTPS terminiert, aber EXTERNALSECURE=false in Zitadel gesetzt ist, generiert Zitadel HTTP-Issuer-URLs, was zu OIDC-Validierungsfehlern führt. Passe für Produktion die .env an:
ZITADEL_DOMAIN=auth.example.com
ZITADEL_EXTERNALPORT=443
ZITADEL_EXTERNALSECURE=true
ZITADEL_PUBLIC_SCHEME=https
PROXY_HTTP_PUBLISHED_PORT=80Das offizielle Zitadel-Repository enthält TLS-Overlay-Dateien für Let's Encrypt. Starte dann mit:
docker compose -f compose.yaml -f compose.tls-letsencrypt.yaml --env-file .env up -d --waitAlternativ kannst du Zitadel hinter einem bereits vorhandenen Traefik-Stack betreiben – eine vollständige Anleitung dazu findest du unter Traefik als Docker-Reverse-Proxy mit automatischem HTTPS einrichten. Caddy als schlanke Alternative beschreibt Caddy als Reverse Proxy einrichten.
Verifizieren: Nach dem Start mit TLS:
curl -I https://auth.example.com/.well-known/openid-configurationErwartete Antwort: HTTP 200. Der issuer-Wert im JSON muss https://auth.example.com lauten.
Schritt 8: Update-Prozess und Backup
Zitadel nutzt ein event-sourced Datenmodell: Alle Zustandsänderungen sind als Events in der Tabelle eventstore.events in PostgreSQL gespeichert. Das Binary selbst ist vollständig zustandslos – Backups beziehen sich ausschließlich auf die PostgreSQL-Datenbank. Aus dem Eventstore kann der gesamte Zustand im Notfall wiederhergestellt werden.
Update auf neue Version: Trage die neue Version in der .env-Datei ein und führe aus:
docker compose --env-file .env pull
docker compose --env-file .env up -d --waitZitadel führt Datenbankmigrationen automatisch beim Start via start-from-init durch – kein manuelles Eingreifen notwendig.
Datenbank-Backup:
docker compose exec postgres pg_dump -U postgres zitadel \
| gzip > /opt/backups/zitadel_$(date +%Y%m%d_%H%M).sql.gzEine vollständige Backup-Automatisierung mit cron und Cloud-Sync zeigt die Anleitung MySQL & PostgreSQL Backup automatisieren mit cron.
Verifizieren: Nach einem Update:
docker compose ps
docker compose logs zitadel-api 2>&1 | tail -20Alle Container müssen running (healthy) zeigen; in den Logs dürfen keine Migrationsfehler erscheinen.
Troubleshooting / Typische Fehler
- „masterkey must be 32 characters long" beim Start: Der
ZITADEL_MASTERKEYin der.envist nicht exakt 32 Zeichen lang. Prüfen mit:echo -n 'DeinSchluessel' | wc -c. Wert korrigieren – dieser Fehler tritt ausschließlich vor dem ersten Start auf und ist dann harmlos zu beheben. - zitadel-login startet nicht oder zeigt leere Seite: Das Bootstrap-Volume wurde von
zitadel-apinoch nicht vollständig befüllt (login-client.patfehlt). Prüfen mit:docker compose logs zitadel-api | grep -i pat. Auf den Healthcheck warten; ggf.docker compose up -d --waiterneut ausführen. - 502 Bad Gateway von Traefik: Das Label
traefik.http.services.zitadel-api.loadbalancer.server.scheme=h2cfehlt oder wurde versehentlich entfernt. Zitadel erwartet HTTP/2 als Backend-Protokoll. Label wiederherstellen und Stack neu starten. - Port-Konflikt auf 8080: Ein anderer Dienst auf dem Host belegt Port 8080. In der
.envPROXY_HTTP_PUBLISHED_PORTauf einen freien Port setzen (z. B. 8090) und Stack neu starten. - OIDC-Issuer-URL falsch (http statt https): Bei TLS-Betrieb müssen
ZITADEL_EXTERNALSECURE=trueundZITADEL_PUBLIC_SCHEME=httpsgesetzt sein. Anderenfalls generiert Zitadel HTTP-Issuer-URLs, die von OIDC-Clients abgelehnt werden. - Masterkey nach erstem Start geändert: Alle verschlüsselten Daten in der Datenbank sind dauerhaft unlesbar. Fataler, nicht reversibler Fehler. Masterkey separat sicher aufbewahren.
- Redis-Container läuft nicht trotz aktivierter Einstellung: Redis ist hinter einem Docker Compose Profile versteckt. Start mit:
docker compose --profile cache --env-file .env up -d --wait. - PostgreSQL Healthcheck nicht abgewartet: Ohne
condition: service_healthyimdepends_onkann Zitadel vor PostgreSQL starten und beim Datenbankzugriff fehlschlagen. Das bereitgestelltecompose.yamlenthält diese Abhängigkeit bereits korrekt.
Häufige Fragen
Wie lautet die Admin-Konsolen-URL nach dem ersten Start?
Die Verwaltungskonsole ist unter http://<ZITADEL_DOMAIN>:<PROXY_HTTP_PUBLISHED_PORT>/ui/console erreichbar – bei Standardwerten also http://localhost:8080/ui/console. Die Bootstrap-Zugangsdaten erscheinen in den Logs: docker compose logs zitadel-api 2>&1 | grep -i -E "admin|password".
Was bedeutet natives Multi-Tenancy bei Zitadel?
In einer einzigen Zitadel-Deployment können mehrere Instanzen (jeweils mit eigener Domain) und innerhalb jeder Instanz mehrere Organisationen betrieben werden. SaaS-Anbieter können so jedem Kunden eine vollständig isolierte Identity-Umgebung anbieten – ohne separate Installationen. Das spart Betriebsaufwand im Vergleich zu Tools, die nur mandantenfähig per Realm-Konzept arbeiten.
Wie unterscheidet sich Zitadel von Authentik oder Keycloak?
Zitadel ist in Go geschrieben (sehr geringer Ressourcenverbrauch), hat natives Multi-Tenancy auf Instanz-Ebene und ein event-sourced Datenmodell. Keycloak basiert auf Java und ist ressourcenhungriger; Authentik ist Python-basiert und stärker auf Proxy-Auth und Forward-Auth fokussiert. Wer mehrere voneinander isolierte Tenant-Umgebungen auf einer Instanz betreiben will, ist mit Zitadel am besten bedient. Einen direkten SSO-Vergleich liefert die Anleitung Single Sign-On: Authentik vs. Authelia mit Traefik Forward-Auth absichern.
Kann ich Zitadel ohne Traefik betreiben?
Der offizielle Stack setzt Traefik voraus, da die Routing-Logik (API, Login-UI, Root-Redirect) über Traefik-Labels gesteuert wird. Mit einem eigenen Nginx oder Caddy ist es möglich, erfordert aber manuelle Routing-Konfiguration für die Pfade /api, /ui/v2/login und den Root-Pfad. Dabei muss h2c (HTTP/2 Cleartext) als Backend-Protokoll zu zitadel-api konfiguriert werden.
Was passiert mit dem Bootstrap-Volume beim Neustart?
Das zitadel-bootstrap-Volume bleibt persistent. Beim Neustart erkennt Zitadel, dass die Initialisierung bereits erfolgt ist, und überspringt sie. Nur wenn du das Volume und die PostgreSQL-Datenbank gleichzeitig löschst (docker compose down -v), startet Zitadel die komplette Initialisierung neu.
Unterstützt Zitadel Passkeys und MFA?
Ja. Zitadel unterstützt Passkeys (WebAuthn/FIDO2), TOTP, SMS-MFA, E-Mail-MFA und Social Logins out of the box. Diese Funktionen lassen sich in der Admin-Konsole unter Instance Settings → Login konfigurieren. Das ist besonders relevant für KMU, die Passwort-MFA-Pflicht einführen wollen, ohne eine eigene MFA-Infrastruktur aufzubauen.
Fazit
Zitadel ist eine ausgereifte, aktiv gepflegte IAM-Plattform, die sich dank des kompakten Go-Binaries und des event-sourced Datenmodells besonders für ressourcenbeschränkte Umgebungen und SaaS-Szenarien mit mehreren Tenants eignet. Der Docker-Compose-Stack ist in rund 20 Minuten lauffähig; die Verifikationsschritte in dieser Anleitung stellen sicher, dass PostgreSQL, Bootstrap-Volume und Traefik-Routing korrekt zusammenspielen, bevor du die Konsole öffnest. Für den Produktionseinsatz sind zwei Dinge unverzichtbar: ein sicher verwahrter 32-Zeichen-Masterkey und HTTPS mit korrektem ZITADEL_EXTERNALSECURE=true. Wer diese Grundregeln beachtet, betreibt mit Zitadel eine stabile, zukunftssichere Identitätsplattform – ohne die Betriebslast einer Keycloak-Installation.
Weiterführende Anleitungen und Quellen
- 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