Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Sicherheit & Datenschutz 05.07.2026 · 11 min Lesezeit Geprüft 23.10.2026

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

Titelbild: Zitadel mit Docker installieren, Identität, OIDC, Mandanten

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.

KomponenteImageInterner PortArchitektur
zitadel-apighcr.io/zitadel/zitadel:v4.20.08080/tcp (h2c)amd64, arm64
zitadel-loginghcr.io/zitadel/zitadel-login:v4.20.03000/tcpamd64, arm64
proxy (Traefik)traefik:v3.7.1380 → Host 127.0.0.1:8080amd64, arm64
postgrespostgres:17.11-alpine5432/tcpamd64, arm64
VolumeZweck
postgres-dataDatenbank mit allen Nutzer- und Konfigurationsdaten
zitadel-bootstraplogin-client.pat (API schreibt, Login-UI liest)

Prüfen Sie die Docker- und Compose-Versionen:

$ docker --version
$ docker compose version

Erwartet: 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/zitadel

Der 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-Passwort

Erstellen 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-alpine

Das 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 --wait

Bei Problemen verfolgen Sie den Start mit docker compose logs -f zitadel-api.

$ docker compose ps

Alle 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.localhost

Richten 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 -10

Die 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.com

Zitadel 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 --wait

Die 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-configuration

Erwartete 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 --wait

Zitadel 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/null

Eine 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 -20

Alle Container zeigen running (healthy), die Logs enthalten keine Migrationsfehler.

Troubleshooting / Typische Fehler

„masterkey must be 32 characters long“

beim Start: Der ZITADEL_MASTERKEY in der .env ist 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.pat fehlt noch im Bootstrap-Volume. Prüfen mit docker compose logs zitadel-api | grep -i pat, dann docker compose up -d --wait erneut ausführen.

502 Bad Gateway von Traefik

Das Label loadbalancer.server.scheme=h2c fehlt. Label wiederherstellen und Stack neu starten.

Port-Konflikt auf 8080

Ein anderer Dienst belegt den Port. PROXY_HTTP_PUBLISHED_PORT und ZITADEL_EXTERNALPORT vor 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 .env wirken nicht; Passwort in der Konsole ändern.

Login-UI meldet Fehler zum Session-Cookie

LOGIN_SESSION_COOKIE_SECRET fehlt oder ist kürzer als 32 Zeichen.

OIDC-Issuer-URL falsch (http statt https)

ZITADEL_EXTERNALSECURE=true und ZITADEL_PUBLIC_SCHEME=https setzen, 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

  1. Docker Compose absichern
  2. Docker und Docker Compose auf Linux installieren, die Self-Hosting-Grundlage
  3. Traefik als Docker-Reverse-Proxy mit automatischem HTTPS einrichten
  4. Single Sign-On: Authentik vs. Authelia mit Traefik Forward-Auth absichern
  5. MySQL & PostgreSQL Backup automatisieren mit cron und rclone-Cloud-Sync
  6. Casdoor mit Docker installieren: Zentraler Auth-Server
  7. HashiCorp Vault mit Docker installieren

Offizielle Quellen: Zitadel Docs, Docker Compose Deployment | Zitadel GitHub Repository | Zitadel Production Guide

Passende Anleitungen auf S-EDV

  1. GPT-5.5-Cyber: OpenAI startet Sicherheitsmodell als Konkurrenz zu Anthropic Myth
  2. PCI DSS 4.0.1: Checkout-Skripte als Sicherheitsrisiko: was Admins jetzt prüfen
  3. PostgreSQL schließt elf Sicherheitslücken in den Versionen 14 bis 18