Skip to main content

On This Page

Deploying Zitadel: A Modern Keycloak Alternative for Self-Hosted Identity Management

2 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

Self-Hosting Zitadel with Docker Compose

Zitadel is an open-source identity management platform that provides native support for OIDC, SAML, passkeys, and multi-tenancy. The v4 architecture utilizes a two-container system consisting of a Go-based core and a Next.js Login UI.

Why This Matters

While traditional identity providers like Keycloak often rely on complex XML-heavy configurations, Zitadel adopts an API-first approach with a clean Next.js-based console. However, this modern architecture demands precise resource allocation, specifically at least 4 CPU cores for production instances to handle the intensive bcrypt/argon2 password hashing required during concurrent login storms. Technical reality dictates that while OIDC and SAML are standard, their implementation in a self-hosted environment requires strict adherence to domain-based routing, as these protocols often fail when configured solely on IP addresses.

Key Insights

  • Zitadel v4 uses a decoupled architecture separating the Core API (ghcr.io/zitadel/zitadel) from the Login UI (ghcr.io/zitadel/zitadel-login) for modularity.
  • PostgreSQL 17 is currently the only supported database backend, as previous support for CockroachDB has been officially dropped.
  • A mandatory 32-character masterkey is used to encrypt secrets at rest; losing this key results in total loss of access to encrypted database data.
  • The system requires specific environment variables for Login V2 integration, including ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED set to true.
  • Password hashing (bcrypt/argon2) is CPU-bound by design, meaning high CPU usage during logins is a security feature to prevent brute-force attacks.
  • The login UI container must run in the same network namespace as the core service using ‘network_mode: service:zitadel’ to function correctly.

Working Examples

A simplified Docker Compose configuration for Zitadel v4 with PostgreSQL 17.

services:
  zitadel:
    image: ghcr.io/zitadel/zitadel:v4.12.2
    command: start-from-init --masterkey "YourExactly32CharacterMasterKey!"
    environment:
      ZITADEL_EXTERNALDOMAIN: auth.example.com
      ZITADEL_DATABASE_POSTGRES_HOST: zitadel-db
      ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: change-postgres-password
      ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: "true"
      ZITADEL_OIDC_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?authRequest=
    ports:
      - "8080:8080"
      - "3000:3000"
    healthcheck:
      test: ["CMD", "/app/zitadel", "ready"]
  login:
    image: ghcr.io/zitadel/zitadel-login:v4.12.2
    environment:
      - ZITADEL_API_URL=http://localhost:8080
    network_mode: service:zitadel
  zitadel-db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: change-postgres-password

Practical Applications

  • System: Implementing machine-to-machine (M2M) authentication using Zitadel’s first-class support for service accounts and Personal Access Tokens (PATs).
  • Pitfall: Misconfiguring ZITADEL_TLS_ENABLED behind a reverse proxy, leading to connection refused errors during OIDC discovery.
  • System: Deploying multi-tenant identity solutions where each organization requires isolated password policies and MFA requirements.
  • Pitfall: Failing to share the zitadel-data volume between core and login containers, which prevents the Login UI from reading the necessary PAT file.

References:

Continue reading

Next article

Optimizing Node.js Production Uptime with systemd

Related Content