OpenLogs

All tools

Zitadel

Identity management server with single sign-on, passkeys and multi-tenant organizations.

Replaces

Paid identity and login services, such as Auth0.

All open-source alternatives to Auth0

Difficulty

Difficulty: Hard

6 services, 4 volume mounts, 33 env values to set, host access: docker_socket. Heuristic score 8 of 11: hard.

Stack

Go (language)

Deployment

docker_compose

Requirements

Not stated.

License

AGPL-3.0 (OSI-approved open source)

Read from github.com/zitadel/zitadel/blob/ce12560/LICENSE

Business model

Unknown

The README at the pinned commit does not state a business model, so it stays unknown.

Source: github.com/zitadel/zitadel/blob/ce12560/README.md, read on .

Repository

zitadel/zitadel

GitHub stats

Stars
15,385
Forks
1,396
Open issues and pull requests
1,310
Last push
2026-10-22
Latest release
v4.20.0
Released
2026-10-22

From the GitHub API on . These numbers are a snapshot, not live. GitHub detected the license as AGPL-3.0.

Compose file

Config check passed docker compose config exited 0 on this exact file on 2026-10-22.
docker-compose.yml Version 1
name: zitadel

services:
  proxy:
    image: ${TRAEFIK_IMAGE}
    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=${TRAEFIK_DASHBOARD_ENABLED}
      - --api.insecure=false
      - --ping=true
      - --ping.entrypoint=web
      - --log.level=${TRAEFIK_LOG_LEVEL}
      - --accesslog=${TRAEFIK_ACCESSLOG_ENABLED}
    ports:
      - ${PROXY_HTTP_PUBLISHED_PORT}: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}
    restart: unless-stopped
    user: "0"
    command: start-from-init --masterkey "${ZITADEL_MASTERKEY}"
    environment:
      ZITADEL_PORT: 8080
      ZITADEL_EXTERNALDOMAIN: ${ZITADEL_DOMAIN}
      ZITADEL_EXTERNALPORT: ${ZITADEL_EXTERNALPORT}
      ZITADEL_EXTERNALSECURE: ${ZITADEL_EXTERNALSECURE}
      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}

      ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: true
      ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: ${ZITADEL_PUBLIC_SCHEME}://${ZITADEL_DOMAIN}:${ZITADEL_EXTERNALPORT}/ui/v2/login/
      ZITADEL_OIDC_DEFAULTLOGINURLV2: ${ZITADEL_PUBLIC_SCHEME}://${ZITADEL_DOMAIN}:${ZITADEL_EXTERNALPORT}/ui/v2/login/login?authRequest=
      ZITADEL_OIDC_DEFAULTLOGOUTURLV2: ${ZITADEL_PUBLIC_SCHEME}://${ZITADEL_DOMAIN}:${ZITADEL_EXTERNALPORT}/ui/v2/login/logout?post_logout_redirect=
      ZITADEL_SAML_DEFAULTLOGINURLV2: ${ZITADEL_PUBLIC_SCHEME}://${ZITADEL_DOMAIN}:${ZITADEL_EXTERNALPORT}/ui/v2/login/login?samlRequest=

      ZITADEL_LOGSTORE_ACCESS_STDOUT_ENABLED: ${ZITADEL_ACCESS_LOG_STDOUT_ENABLED}

      ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_TYPE: ${ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_TYPE}
      ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_ENDPOINT: ${ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_ENDPOINT}
      ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_INSECURE: ${ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_INSECURE}
      ZITADEL_INSTRUMENTATION_SERVICENAME: ${ZITADEL_INSTRUMENTATION_SERVICENAME}

      ZITADEL_CACHES_CONNECTORS_REDIS_ENABLED: ${ZITADEL_CACHES_CONNECTORS_REDIS_ENABLED}
      ZITADEL_CACHES_CONNECTORS_REDIS_URL: ${ZITADEL_CACHES_CONNECTORS_REDIS_URL}
      ZITADEL_CACHES_INSTANCE_CONNECTOR: ${ZITADEL_CACHES_INSTANCE_CONNECTOR}
      ZITADEL_CACHES_MILESTONES_CONNECTOR: ${ZITADEL_CACHES_MILESTONES_CONNECTOR}
      ZITADEL_CACHES_ORGANIZATION_CONNECTOR: ${ZITADEL_CACHES_ORGANIZATION_CONNECTOR}

    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

      # Note: no dedicated gRPC router needed. All gRPC and Connect-RPC traffic
      # is handled by the catch-all router below since the backend already uses h2c.

      - traefik.http.routers.zitadel-api-alias-web.rule=Host(`${ZITADEL_DOMAIN}`) && 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-api-alias-websecure.rule=Host(`${ZITADEL_DOMAIN}`) && PathPrefix(`/api`)
      - traefik.http.routers.zitadel-api-alias-websecure.entrypoints=websecure
      - traefik.http.routers.zitadel-api-alias-websecure.tls=true
      - traefik.http.routers.zitadel-api-alias-websecure.middlewares=zitadel-strip-api
      - traefik.http.routers.zitadel-api-alias-websecure.service=zitadel-api
      - traefik.http.routers.zitadel-api-alias-websecure.priority=200

      - traefik.http.routers.zitadel-canonical-web.rule=Host(`${ZITADEL_DOMAIN}`) && !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

      - traefik.http.routers.zitadel-canonical-websecure.rule=Host(`${ZITADEL_DOMAIN}`) && !PathPrefix(`/ui/v2/login`) && !PathPrefix(`/api`) && !Path(`/`)
      - traefik.http.routers.zitadel-canonical-websecure.entrypoints=websecure
      - traefik.http.routers.zitadel-canonical-websecure.tls=true
      - traefik.http.routers.zitadel-canonical-websecure.service=zitadel-api
      - traefik.http.routers.zitadel-canonical-websecure.priority=100

  zitadel-login:
    image: ghcr.io/zitadel/zitadel-login:${ZITADEL_VERSION}
    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
      # Signs the Login UI session cookie. At least 32 characters, e.g. `openssl rand -base64 32`.
      ZITADEL_SESSION_COOKIE_SECRET: ${LOGIN_SESSION_COOKIE_SECRET}
      CUSTOM_REQUEST_HEADERS: Host:${ZITADEL_DOMAIN},X-Forwarded-Proto:${ZITADEL_PUBLIC_SCHEME}

      # Future-ready placeholders for upcoming Login OTEL support.
      OTEL_SERVICE_NAME: ${LOGIN_OTEL_SERVICE_NAME}
      OTEL_EXPORTER_OTLP_ENDPOINT: ${LOGIN_OTEL_EXPORTER_OTLP_ENDPOINT}
      OTEL_EXPORTER_OTLP_PROTOCOL: ${LOGIN_OTEL_EXPORTER_OTLP_PROTOCOL}
    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}`) && 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-root-websecure.rule=Host(`${ZITADEL_DOMAIN}`) && Path(`/`)
      - traefik.http.routers.zitadel-root-websecure.entrypoints=websecure
      - traefik.http.routers.zitadel-root-websecure.tls=true
      - traefik.http.routers.zitadel-root-websecure.middlewares=zitadel-root-rewrite
      - traefik.http.routers.zitadel-root-websecure.service=zitadel-login
      - traefik.http.routers.zitadel-root-websecure.priority=400

      - traefik.http.routers.zitadel-login-web.rule=Host(`${ZITADEL_DOMAIN}`) && 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

      - traefik.http.routers.zitadel-login-websecure.rule=Host(`${ZITADEL_DOMAIN}`) && PathPrefix(`/ui/v2/login`)
      - traefik.http.routers.zitadel-login-websecure.entrypoints=websecure
      - traefik.http.routers.zitadel-login-websecure.tls=true
      - traefik.http.routers.zitadel-login-websecure.service=zitadel-login
      - traefik.http.routers.zitadel-login-websecure.priority=250

  postgres:
    image: ${POSTGRES_IMAGE}
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_ADMIN_PASSWORD}
      POSTGRES_USER: ${POSTGRES_ADMIN_USER}
      POSTGRES_DB: ${POSTGRES_DB}
    healthcheck:
      test:
        - CMD-SHELL
        - pg_isready -d ${POSTGRES_DB} -U ${POSTGRES_ADMIN_USER}
      interval: 10s
      timeout: 30s
      retries: 10
      start_period: 20s
    volumes:
      - postgres-data:/var/lib/postgresql/data:rw
    networks:
      - zitadel

  redis:
    image: ${REDIS_IMAGE}
    restart: unless-stopped
    profiles:
      - cache
    command:
      - --save
      - ""
      - --appendonly
      - "no"
    networks:
      - zitadel

  otel-collector:
    image: ${OTEL_COLLECTOR_IMAGE}
    restart: unless-stopped
    profiles:
      - observability
    command:
      - --config=/etc/otelcol/config.yaml
    volumes:
      - ./otel-collector-config.yaml:/etc/otelcol/config.yaml:ro
    networks:
      - zitadel

networks:
  zitadel:
    name: zitadel

volumes:
  postgres-data:
  zitadel-bootstrap:

From the app's own repository: github.com/zitadel/zitadel/blob/ce12560/deploy/compose/docker-compose.yml.

Compose file from the app repository.

Install

  1. Docker with the Compose plugin (github.com/zitadel/zitadel/blob/ce12560/README.md)

    A host with Docker and the Docker Compose plugin. This entry runs from a compose file.

  2. Save the compose file as docker-compose.yml (github.com/zitadel/zitadel/blob/ce12560/deploy/compose/docker-compose.yml)

    Use the compose file shown above.

  3. Save as .env next to the compose file (github.com/zitadel/zitadel/blob/ce12560/deploy/compose/.env.example)
    # ⚠️  INSECURE DEFAULTS — for local development only.
    # Before exposing this stack to a network, change ZITADEL_MASTERKEY,
    # LOGIN_SESSION_COOKIE_SECRET, POSTGRES_ADMIN_PASSWORD, and POSTGRES_ZITADEL_PASSWORD.
    # See: https://zitadel.com/docs/self-hosting/deploy/compose#homelab
    
    # -----------------------------
    # Domain and external URL
    # -----------------------------
    ZITADEL_DOMAIN=localhost
    # Used by the base (non-TLS) compose; TLS overlays also publish 80/443 and may still expose
    # this port unless their ports are adjusted.
    PROXY_HTTP_PUBLISHED_PORT=8080
    ZITADEL_EXTERNALPORT=8080
    ZITADEL_EXTERNALSECURE=false
    # Overridden by TLS overlays (they hardcode X-Forwarded-Proto: https).
    ZITADEL_PUBLIC_SCHEME=http
    
    # -----------------------------
    # Security/bootstrap
    # -----------------------------
    # Must be exactly 32 chars for new deployments.
    ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters
    LOGIN_CLIENT_PAT_EXPIRATION=2099-01-01T00:00:00Z
    # Signs the Login UI session cookie. Must be at least 32 chars.
    # Generate with: openssl rand -base64 32
    LOGIN_SESSION_COOKIE_SECRET=SessionCookieSecretNeedsAtLeast32Chars
    
    # -----------------------------
    # Pinned image tags
    # To upgrade ZITADEL, bump ZITADEL_VERSION and run:
    #   docker compose --env-file .env -f docker-compose.yml pull
    #   docker compose --env-file .env -f docker-compose.yml up -d --wait
    # -----------------------------
    ZITADEL_VERSION=v4.16.0
    TRAEFIK_IMAGE=traefik:v3.7.7
    POSTGRES_IMAGE=postgres:17.10-alpine
    REDIS_IMAGE=redis:7.4.9-alpine
    OTEL_COLLECTOR_IMAGE=otel/opentelemetry-collector-contrib:0.156.0
    
    # -----------------------------
    # Proxy settings
    # -----------------------------
    TRAEFIK_DASHBOARD_ENABLED=false
    TRAEFIK_LOG_LEVEL=INFO
    TRAEFIK_ACCESSLOG_ENABLED=true
    # Trusted proxy IPs for X-Forwarded-* headers (external-tls mode).
    # Comma-separated CIDR ranges of your upstream load balancer / reverse proxy.
    TRAEFIK_TRUSTED_IPS=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
    
    # Let's Encrypt mode
    [email protected]
    
    # -----------------------------
    # Postgres settings
    # -----------------------------
    POSTGRES_DB=zitadel
    POSTGRES_ADMIN_USER=postgres
    POSTGRES_ADMIN_PASSWORD=postgres
    # DSN used by ZITADEL to connect to PostgreSQL.
    # The start-from-init command uses this DSN to create the database schema and for
    # normal operation. When a DSN is configured, ZITADEL does not create or switch
    # to a separate unprivileged database user; the user in this DSN is used directly
    # and must already exist. For production, configure this DSN to use a non-superuser
    # role with only the permissions ZITADEL needs.
    # See https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
    ZITADEL_DATABASE_POSTGRES_DSN=postgresql://postgres:postgres@postgres:5432/zitadel?sslmode=disable
    
    # -----------------------------
    # Access logs
    # -----------------------------
    ZITADEL_ACCESS_LOG_STDOUT_ENABLED=true
    
    # -----------------------------
    # Optional Redis cache settings
    # Default is disabled.
    # Enable with --profile cache and switch connectors to redis.
    # -----------------------------
    ZITADEL_CACHES_CONNECTORS_REDIS_ENABLED=false
    # DSN used by ZITADEL to connect to Redis.
    # See https://redis.io/docs/latest/develop/tools/cli/#host-port-password-and-database
    ZITADEL_CACHES_CONNECTORS_REDIS_URL=redis://redis:6379/0
    ZITADEL_CACHES_INSTANCE_CONNECTOR=
    ZITADEL_CACHES_MILESTONES_CONNECTOR=
    ZITADEL_CACHES_ORGANIZATION_CONNECTOR=
    
    # -----------------------------
    # Optional API tracing (OTEL)
    # Default is disabled. Enable with --profile observability.
    # The collector receives traces from ZITADEL on port 4317 (gRPC) and 4318 (HTTP)
    # and logs them to stdout by default.
    # To forward traces to your own backend (Grafana Tempo, Jaeger, OpenObserve, etc.),
    # uncomment the otlp exporter in otel-collector-config.yaml and set:
    # OTEL_BACKEND_ENDPOINT=http://your-backend:4317
    # -----------------------------
    ZITADEL_INSTRUMENTATION_SERVICENAME=zitadel-api
    ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_TYPE=none
    ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_ENDPOINT=otel-collector:4317
    ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_INSECURE=true
    
    # -----------------------------
    # Future Login OTEL placeholders
    # Current login image may ignore these.
    # -----------------------------
    LOGIN_OTEL_SERVICE_NAME=zitadel-login
    LOGIN_OTEL_EXPORTER_OTLP_ENDPOINT=
    LOGIN_OTEL_EXPORTER_OTLP_PROTOCOL=grpc
    
  4. What the compose file check ran with (github.com/zitadel/zitadel/blob/ce12560/README.md)

    The compose file check ran together with .env.example from the same commit, saved as .env. The folder held only these files. The README tells the reader to download docker-compose.yml and .env.example, copy the example to .env and run docker compose up.

  5. Start it (github.com/zitadel/zitadel/blob/ce12560/README.md)
    docker compose up -d
  6. Open the app (github.com/zitadel/zitadel/blob/ce12560/deploy/compose/docker-compose.yml)

    Open http://localhost:8080. The compose file publishes host port 8080 for the proxy service.

Compose file check: config_ok on 2026-10-22.

Gotchas and maintenance

Gotchas

Signals

  • Open issues and pull requests: 1310
  • Last push: 2026-10-22 (0 days before this page was built)
  • Latest release: v4.20.0 on 2026-10-22 (0 days before this page was built)