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

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.

Zitadel mit Docker installieren: Cloud nativer Identity Server mit Docker Container, OIDC, SAML, OAuth 2.0, Multi Tenancy, Benutzerverwaltung und sicherem Single Sign On für moderne Self Hosting und Unternehmensumgebungen. KI-generiert

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

  1. Docker Engine >= 24.x und Docker Compose Plugin >= 2.x (Befehl: docker compose, nicht das veraltete docker-compose) – Installationsanleitung: Docker und Docker Compose auf Linux installieren
  2. Linux-Host, VM oder NAS mit Docker-Unterstützung (Ubuntu 22.04/24.04 und Debian 12 empfohlen)
  3. Mindestens 512 MB RAM für Tests; für Produktion mindestens 1 GB, besser 4 CPU-Kerne (Passwort-Hashing erzeugt CPU-Spitzen)
  4. Mindestens 2 GB freier Festplattenspeicher
  5. Internetzugang für Image-Pulls von ghcr.io
  6. Für Produktion: eigene Domain und TLS-Zertifikat (Let's Encrypt wird offiziell unterstützt)
  7. Optional: openssl zum 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.

KomponenteImageInterner PortArchitektur
zitadel-apighcr.io/zitadel/zitadel:v4.15.18080/tcp (h2c)amd64, arm64
zitadel-loginghcr.io/zitadel/zitadel-login:v4.15.13000/tcpamd64, arm64
proxy (Traefik)traefik:v3.6.880 → Host 8080amd64, arm64
postgrespostgres:17.2-alpine5432/tcpamd64, arm64
VolumeTypZweck
postgres-dataNamed VolumePostgreSQL-Datenbankdateien (alle Nutzer-/Konfigurationsdaten)
zitadel-bootstrapNamed Volumelogin-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 version

Erwartete 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/zitadel

Du 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/zitadel

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

Die Base64-Ausgabe von rand -base64 24 ist exakt 32 Zeichen lang. Prüfe die Länge zur Sicherheit:

echo -n 'DeinGenerierterSchluessel32Zeich' | wc -c

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

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

Beobachte den Startvorgang live:

docker compose logs -f zitadel-api

Suche nach einer Zeile wie server started – das signalisiert, dass die Initialisierung abgeschlossen ist.

Verifizieren:

docker compose ps

Alle 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/console

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

Die 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=80

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

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

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

Zitadel 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.gz

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

Alle Container müssen running (healthy) zeigen; in den Logs dürfen keine Migrationsfehler erscheinen.

Troubleshooting / Typische Fehler

  1. „masterkey must be 32 characters long" beim Start: Der ZITADEL_MASTERKEY in der .env ist 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.
  2. zitadel-login startet nicht oder zeigt leere Seite: Das Bootstrap-Volume wurde von zitadel-api noch nicht vollständig befüllt (login-client.pat fehlt). Prüfen mit: docker compose logs zitadel-api | grep -i pat. Auf den Healthcheck warten; ggf. docker compose up -d --wait erneut ausführen.
  3. 502 Bad Gateway von Traefik: Das Label traefik.http.services.zitadel-api.loadbalancer.server.scheme=h2c fehlt oder wurde versehentlich entfernt. Zitadel erwartet HTTP/2 als Backend-Protokoll. Label wiederherstellen und Stack neu starten.
  4. Port-Konflikt auf 8080: Ein anderer Dienst auf dem Host belegt Port 8080. In der .env PROXY_HTTP_PUBLISHED_PORT auf einen freien Port setzen (z. B. 8090) und Stack neu starten.
  5. OIDC-Issuer-URL falsch (http statt https): Bei TLS-Betrieb müssen ZITADEL_EXTERNALSECURE=true und ZITADEL_PUBLIC_SCHEME=https gesetzt sein. Anderenfalls generiert Zitadel HTTP-Issuer-URLs, die von OIDC-Clients abgelehnt werden.
  6. Masterkey nach erstem Start geändert: Alle verschlüsselten Daten in der Datenbank sind dauerhaft unlesbar. Fataler, nicht reversibler Fehler. Masterkey separat sicher aufbewahren.
  7. 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.
  8. PostgreSQL Healthcheck nicht abgewartet: Ohne condition: service_healthy im depends_on kann Zitadel vor PostgreSQL starten und beim Datenbankzugriff fehlschlagen. Das bereitgestellte compose.yaml enthä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 SettingsLogin 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

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