$ services: zitadel: image: ghcr.io/zitadel/zitadel:v4.12.2 container_name: zitadel -weight: 500;">restart: unless-stopped command: -weight: 500;">start-from-init --masterkey "YourExactly32CharacterMasterKey!" # CHANGE THIS — must be exactly 32 chars environment: # External access ZITADEL_EXTERNALDOMAIN: auth.example.com # CHANGE to your domain ZITADEL_EXTERNALSECURE: "false" # Set true if Zitadel handles TLS directly ZITADEL_TLS_ENABLED: "false" # Set false when behind a reverse proxy # Database — admin connection (for schema setup) ZITADEL_DATABASE_POSTGRES_HOST: zitadel-db ZITADEL_DATABASE_POSTGRES_PORT: "5432" ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: change-postgres-password # CHANGE THIS ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: -weight: 500;">disable # Database — application connection ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: change-zitadel-password # CHANGE THIS ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: -weight: 500;">disable # Login V2 integration ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /zitadel-data/login-client.pat ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: "false" ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Login Client Service Account ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: "2029-01-01T00:00:00Z" ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: "true" ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: http://localhost:3000/ui/v2/login/ ZITADEL_OIDC_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?authRequest= ZITADEL_OIDC_DEFAULTLOGOUTURLV2: http://localhost:3000/ui/v2/login/logout?post_logout_redirect= ZITADEL_SAML_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?samlRequest= ports: - "8080:8080" - "3000:3000" volumes: - zitadel-data:/zitadel-data healthcheck: test: ["CMD", "/app/zitadel", "ready"] interval: 10s timeout: 60s retries: 5 start_period: 10s depends_on: zitadel-db: condition: service_healthy networks: - zitadel login: image: ghcr.io/zitadel/zitadel-login:v4.12.2 container_name: zitadel-login -weight: 500;">restart: unless-stopped environment: - ZITADEL_API_URL=http://localhost:8080 - NEXT_PUBLIC_BASE_PATH=/ui/v2/login - ZITADEL_SERVICE_USER_TOKEN_FILE=/zitadel-data/login-client.pat network_mode: -weight: 500;">service:zitadel volumes: - zitadel-data:/zitadel-data:ro depends_on: zitadel: condition: service_healthy zitadel-db: image: postgres:17 container_name: zitadel-db -weight: 500;">restart: unless-stopped environment: PGUSER: postgres POSTGRES_PASSWORD: change-postgres-password # Must match ADMIN_PASSWORD above healthcheck: test: ["CMD-SHELL", "pg_isready -d zitadel -U postgres"] interval: 10s timeout: 30s retries: 5 start_period: 20s volumes: - zitadel-postgres:/var/lib/postgresql/data networks: - zitadel volumes: zitadel-data: zitadel-postgres: networks: zitadel: driver: bridge
services: zitadel: image: ghcr.io/zitadel/zitadel:v4.12.2 container_name: zitadel -weight: 500;">restart: unless-stopped command: -weight: 500;">start-from-init --masterkey "YourExactly32CharacterMasterKey!" # CHANGE THIS — must be exactly 32 chars environment: # External access ZITADEL_EXTERNALDOMAIN: auth.example.com # CHANGE to your domain ZITADEL_EXTERNALSECURE: "false" # Set true if Zitadel handles TLS directly ZITADEL_TLS_ENABLED: "false" # Set false when behind a reverse proxy # Database — admin connection (for schema setup) ZITADEL_DATABASE_POSTGRES_HOST: zitadel-db ZITADEL_DATABASE_POSTGRES_PORT: "5432" ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: change-postgres-password # CHANGE THIS ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: -weight: 500;">disable # Database — application connection ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: change-zitadel-password # CHANGE THIS ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: -weight: 500;">disable # Login V2 integration ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /zitadel-data/login-client.pat ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: "false" ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Login Client Service Account ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: "2029-01-01T00:00:00Z" ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: "true" ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: http://localhost:3000/ui/v2/login/ ZITADEL_OIDC_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?authRequest= ZITADEL_OIDC_DEFAULTLOGOUTURLV2: http://localhost:3000/ui/v2/login/logout?post_logout_redirect= ZITADEL_SAML_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?samlRequest= ports: - "8080:8080" - "3000:3000" volumes: - zitadel-data:/zitadel-data healthcheck: test: ["CMD", "/app/zitadel", "ready"] interval: 10s timeout: 60s retries: 5 start_period: 10s depends_on: zitadel-db: condition: service_healthy networks: - zitadel login: image: ghcr.io/zitadel/zitadel-login:v4.12.2 container_name: zitadel-login -weight: 500;">restart: unless-stopped environment: - ZITADEL_API_URL=http://localhost:8080 - NEXT_PUBLIC_BASE_PATH=/ui/v2/login - ZITADEL_SERVICE_USER_TOKEN_FILE=/zitadel-data/login-client.pat network_mode: -weight: 500;">service:zitadel volumes: - zitadel-data:/zitadel-data:ro depends_on: zitadel: condition: service_healthy zitadel-db: image: postgres:17 container_name: zitadel-db -weight: 500;">restart: unless-stopped environment: PGUSER: postgres POSTGRES_PASSWORD: change-postgres-password # Must match ADMIN_PASSWORD above healthcheck: test: ["CMD-SHELL", "pg_isready -d zitadel -U postgres"] interval: 10s timeout: 30s retries: 5 start_period: 20s volumes: - zitadel-postgres:/var/lib/postgresql/data networks: - zitadel volumes: zitadel-data: zitadel-postgres: networks: zitadel: driver: bridge
services: zitadel: image: ghcr.io/zitadel/zitadel:v4.12.2 container_name: zitadel -weight: 500;">restart: unless-stopped command: -weight: 500;">start-from-init --masterkey "YourExactly32CharacterMasterKey!" # CHANGE THIS — must be exactly 32 chars environment: # External access ZITADEL_EXTERNALDOMAIN: auth.example.com # CHANGE to your domain ZITADEL_EXTERNALSECURE: "false" # Set true if Zitadel handles TLS directly ZITADEL_TLS_ENABLED: "false" # Set false when behind a reverse proxy # Database — admin connection (for schema setup) ZITADEL_DATABASE_POSTGRES_HOST: zitadel-db ZITADEL_DATABASE_POSTGRES_PORT: "5432" ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: change-postgres-password # CHANGE THIS ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: -weight: 500;">disable # Database — application connection ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: change-zitadel-password # CHANGE THIS ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: -weight: 500;">disable # Login V2 integration ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /zitadel-data/login-client.pat ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: "false" ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Login Client Service Account ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: "2029-01-01T00:00:00Z" ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: "true" ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: http://localhost:3000/ui/v2/login/ ZITADEL_OIDC_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?authRequest= ZITADEL_OIDC_DEFAULTLOGOUTURLV2: http://localhost:3000/ui/v2/login/logout?post_logout_redirect= ZITADEL_SAML_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?samlRequest= ports: - "8080:8080" - "3000:3000" volumes: - zitadel-data:/zitadel-data healthcheck: test: ["CMD", "/app/zitadel", "ready"] interval: 10s timeout: 60s retries: 5 start_period: 10s depends_on: zitadel-db: condition: service_healthy networks: - zitadel login: image: ghcr.io/zitadel/zitadel-login:v4.12.2 container_name: zitadel-login -weight: 500;">restart: unless-stopped environment: - ZITADEL_API_URL=http://localhost:8080 - NEXT_PUBLIC_BASE_PATH=/ui/v2/login - ZITADEL_SERVICE_USER_TOKEN_FILE=/zitadel-data/login-client.pat network_mode: -weight: 500;">service:zitadel volumes: - zitadel-data:/zitadel-data:ro depends_on: zitadel: condition: service_healthy zitadel-db: image: postgres:17 container_name: zitadel-db -weight: 500;">restart: unless-stopped environment: PGUSER: postgres POSTGRES_PASSWORD: change-postgres-password # Must match ADMIN_PASSWORD above healthcheck: test: ["CMD-SHELL", "pg_isready -d zitadel -U postgres"] interval: 10s timeout: 30s retries: 5 start_period: 20s volumes: - zitadel-postgres:/var/lib/postgresql/data networks: - zitadel volumes: zitadel-data: zitadel-postgres: networks: zitadel: driver: bridge
-weight: 500;">docker compose up -d
-weight: 500;">docker compose up -d
-weight: 500;">docker compose up -d
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: https://auth.example.com:3000/ui/v2/login/
ZITADEL_OIDC_DEFAULTLOGINURLV2: https://auth.example.com:3000/ui/v2/login/login?authRequest=
ZITADEL_OIDC_DEFAULTLOGOUTURLV2: https://auth.example.com:3000/ui/v2/login/logout?post_logout_redirect=
ZITADEL_SAML_DEFAULTLOGINURLV2: https://auth.example.com:3000/ui/v2/login/login?samlRequest=
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: https://auth.example.com:3000/ui/v2/login/
ZITADEL_OIDC_DEFAULTLOGINURLV2: https://auth.example.com:3000/ui/v2/login/login?authRequest=
ZITADEL_OIDC_DEFAULTLOGOUTURLV2: https://auth.example.com:3000/ui/v2/login/logout?post_logout_redirect=
ZITADEL_SAML_DEFAULTLOGINURLV2: https://auth.example.com:3000/ui/v2/login/login?samlRequest=
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: https://auth.example.com:3000/ui/v2/login/
ZITADEL_OIDC_DEFAULTLOGINURLV2: https://auth.example.com:3000/ui/v2/login/login?authRequest=
ZITADEL_OIDC_DEFAULTLOGOUTURLV2: https://auth.example.com:3000/ui/v2/login/logout?post_logout_redirect=
ZITADEL_SAML_DEFAULTLOGINURLV2: https://auth.example.com:3000/ui/v2/login/login?samlRequest=
python3 -c "import secrets; print(secrets.token_urlsafe(24))"
python3 -c "import secrets; print(secrets.token_urlsafe(24))"
python3 -c "import secrets; print(secrets.token_urlsafe(24))"
-weight: 500;">docker exec zitadel-db pg_dump -U postgres zitadel > zitadel-backup.sql
-weight: 500;">docker exec zitadel-db pg_dump -U postgres zitadel > zitadel-backup.sql
-weight: 500;">docker exec zitadel-db pg_dump -U postgres zitadel > zitadel-backup.sql - A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed (guide)
- 2 GB of RAM (minimum)
- 10 GB of free disk space
- A domain name (required for production — OIDC doesn't work properly on IP addresses) - Access the admin console at http://your-server-ip:8080/ui/console
- Log in with the default admin credentials: Email: zitadel-admin@zitadel.localhost Password: Password1!
- Email: zitadel-admin@zitadel.localhost
- Password: Password1!
- Change the admin password immediately - Email: zitadel-admin@zitadel.localhost
- Password: Password1! - Settings → General: Set your instance name and default language
- Settings → Login Behavior: Configure password policies, MFA requirements
- Organizations: Your default org is created automatically. Rename it to match your organization.
- Projects: Create your first project to -weight: 500;">start adding OIDC/SAML applications - Go to Projects → Your Project → Applications → New
- Select "Web" application type
- Set the redirect URI to your application's callback URL (e.g., https://app.example.com/callback)
- Copy the Client ID and Client Secret
- Your OIDC endpoints are: Discovery: https://auth.example.com/.well-known/openid-configuration Authorization: https://auth.example.com/oauth/v2/authorize Token: https://auth.example.com/oauth/v2/token
- Discovery: https://auth.example.com/.well-known/openid-configuration
- Authorization: https://auth.example.com/oauth/v2/authorize
- Token: https://auth.example.com/oauth/v2/token - Discovery: https://auth.example.com/.well-known/openid-configuration
- Authorization: https://auth.example.com/oauth/v2/authorize
- Token: https://auth.example.com/oauth/v2/token - RAM: ~512 MB for Zitadel, ~4-6 GB total with PostgreSQL caching
- CPU: 2 cores minimum, 4 cores recommended for password hashing under load
- Disk: 10 GB minimum for database and Docker images - Zitadel vs Keycloak: Self-Hosted IAM Compared
- How to Self-Host Keycloak
- Zitadel vs Authentik
- How to Self-Host Authentik
- How to Self-Host Authelia
- Best Self-Hosted Authentication & SSO Tools
- Docker Compose Basics
- Reverse Proxy Guide
- Backup Strategy