#### Init file ```yaml cat << 'EOF' | sudo -u ${ZITADEL_USER} tee ${ZITADEL_PATH}/templates/zitadel-init-steps.yaml.template > /dev/null # This file contains the initial setup steps configuration for Zitadel. # It is used by the `start-from-init` command. # All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml FirstInstance: Skip: false # The machine key from the section FirstInstance.Org.Machine.MachineKey is written to the MachineKeyPath. MachineKeyPath: /zitadel/keys/machine-user/zitadel-admin-sa.json # The personal access token from the section FirstInstance.Org.Machine.Pat is written to the PatPath. PatPath: /zitadel/keys/machine-user/machine-user.pat # The personal access token (IAM_LOGIN_CLIENT) from the section FirstInstance.Org.Machine.Pat is written to the PatPath. LoginClientPatPath: /zitadel/keys/login-client/login-client.pat InstanceName: ZITADEL Org: Name: Zitadel Human: # Use the loginname @ Username: file:/run/secrets/zitadel_org_human_username # This password needs to be changed on the first login Password: file:/run/secrets/zitadel_org_human_password PasswordChangeRequired: true # In the DefaultInstance.Org.Machine section, the initial organization's admin user with the role IAM_OWNER is defined. # If DefaultInstance.Org.Machine.Machine is defined, a service user is created with the IAM_OWNER role. Machine: Machine: Username: file:/run/secrets/zitadel_machine_username Name: file:/run/secrets/zitadel_machine_name MachineKey: # date format: 2023-01-01T00:00:00Z ExpirationDate: file:/run/secrets/zitadel_machine_expire # Currently, the only supported value is 1 for JSON Type: 1 Pat: # By configuring a machine, the setup job creates a user of type machine with the role IAM_OWNER. # It writes a personal access token (PAT) to the path specified in ZITADEL_FIRSTINSTANCE_PATPATH. # The PAT can be used to provision resources with [Terraform](/docs/guides/manage/terraform-provider), for example. # date format: 2023-01-01T00:00:00Z ExpirationDate: file:/run/secrets/zitadel_machine_expire # In the DefaultInstance.Org.LoginClient section, the initial organization's admin user with the role IAM_LOGIN_CLIENT # is defined. LoginClient: Machine: Username: file:/run/secrets/zitadel_pat_username Name: file:/run/secrets/zitadel_pat_name Pat: # date format: 2023-01-01T00:00:00Z ExpirationDate: file:/run/secrets/zitadel_pat_expire Domains: - Domain: "${ZITADEL_API_DOMAIN}" Primary: true # IMPORTANT: # - In this setup the Admin Console is behind a different subdomain, so that it can be protected by mTLS # - The additional subdomain - that is used for the Admin Console - is added here, as it CANNOT be added # through the UI, so that it is available before accessing the Admin Console for the first time - Domain: "${ZITADEL_CONSOLE_DOMAIN}" Primary: false Database: Postgres: Host: zitadel-db Port: 5432 Database: zitadel Admin: SSL: Mode: disable User: SSL: Mode: disable EOF ``` #### Secrets file ```yaml cat << 'EOF' | sudo -u ${ZITADEL_USER} tee ${ZITADEL_PATH}/templates/zitadel-secrets.yaml.template > /dev/null # This file references the Docker Secrets mounted into the container. # It does not contain any sensitive values itself. # All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml Database: Postgres: User: Username: file:/run/secrets/postgres_zitadel_user Password: file:/run/secrets/postgres_zitadel_pass Admin: Username: file:/run/secrets/postgres_admin_user Password: file:/run/secrets/postgres_admin_pass EOF ``` #### Config file ```yaml cat << 'EOF' | sudo -u ${ZITADEL_USER} tee ${ZITADEL_PATH}/templates/zitadel-config.yaml.template > /dev/null # This file contains the primary, non-secret configuration for Zitadel. # It defines domain names and high-level settings. # The actual values will be substituted at runtime from the .env file. # All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml Log: Level: 'info' ExternalSecure: true ExternalDomain: ${ZITADEL_API_DOMAIN} ExternalPort: 443 TLS: Enabled: false InstanceHostHeaders: X-Forwarded-Host,Host PublicHostHeaders: X-Forwarded-Host # Allow multiple domains AllowedOrigins: - https://${ZITADEL_API_DOMAIN} - https://${ZITADEL_CONSOLE_DOMAIN} # Create a System User, which uses the private key of the generated RSA Key Pair to signing JWTs to access the System API # This System User is needed to create the second domain needed for the mTLS connection SystemAPIUsers: - systemadmin: Path: /zitadel/keys/system-user/system-user.pub # Note: The login URLs must use the correct domain and protocol (HTTPS). DefaultInstance: LoginPolicy: AllowRegister: false Features: LoginV2: baseURI: https://${ZITADEL_CONSOLE_DOMAIN}/ui/v2/login oidc: defaultLoginURLV2: https://${ZITADEL_CONSOLE_DOMAIN}/ui/v2/login/login?authRequest= defaultLogoutURLV2: https://${ZITADEL_CONSOLE_DOMAIN}/ui/v2/login/logout?post_logout_redirect= saml: defaultLoginURLV2: https://${ZITADEL_CONSOLE_DOMAIN}/ui/v2/login/login?samlRequest= EOF ``` ### Create the network ```bash docker network create zitadel-private docker network create zitadel-public ``` ### Create the Docker Compose files #### docker-compose-base.yaml ```yaml cat << 'DOCKER_COMPOSE_EOF' | sudo -u ${ZITADEL_USER} tee ${ZITADEL_PATH}/docker-compose-base.yaml > /dev/null --- networks: traefik-private: external: true zitadel-private: external: true zitadel-public: external: true # Add the shared volume for processed configs volumes: zitadel-processed-configs: secrets: # Docker Secrets reference the symbolic link to your secrets directory. # This path must be a literal string, so we use the path of the symbolic link. postgres_admin_user: file: ./secrets/postgres_admin_user postgres_admin_pass: file: ./secrets/postgres_admin_pass postgres_zitadel_user: file: ./secrets/postgres_zitadel_user postgres_zitadel_pass: file: ./secrets/postgres_zitadel_pass # Docker Secret for the Zitadel master key zitadel_masterkey: file: ./secrets/zitadel_masterkey # Docker Secrets for initial setup zitadel_org_human_username: file: ./secrets/zitadel_org_human_username zitadel_org_human_password: file: ./secrets/zitadel_org_human_password zitadel_pat_username: file: ./secrets/zitadel_pat_username zitadel_pat_name: file: ./secrets/zitadel_pat_name zitadel_pat_expire: file: ./secrets/zitadel_pat_expire zitadel_machine_username: file: ./secrets/zitadel_machine_username zitadel_machine_name: file: ./secrets/zitadel_machine_name zitadel_machine_expire: file: ./secrets/zitadel_machine_expire # Docker Secrets for mTLS client certificates zitadel_client_cert: file: ./secrets/self-signed-client.crt zitadel_client_key: file: ./secrets/self-signed-client.key zitadel_ca_cert: file: ./secrets/self-signed-ca.crt services: zitadel-db: image: postgres:${RELEASE_VERSION_1} container_name: zitadel-db hostname: zitadel-db restart: unless-stopped environment: POSTGRES_USER_FILE: /run/secrets/postgres_admin_user POSTGRES_PASSWORD_FILE: /run/secrets/postgres_admin_pass POSTGRES_DB: "test_database" secrets: - postgres_admin_user - postgres_admin_pass healthcheck: test: ["CMD-SHELL", "pg_isready -h localhost -d test_database -U $$(cat /run/secrets/postgres_admin_user)"] interval: 10s timeout: 30s retries: 5 start_period: 20s networks: - zitadel-private ports: - 5433:5433 volumes: - './data:/var/lib/postgresql/data:rw' zitadel-config-processor: image: alpine:latest container_name: zitadel-config-processor hostname: zitadel-config-processor command: | sh -c " apk add --no-cache bash gettext # Run only the processing part, skip the 'exec /app/zitadel' at the end /process-configs.sh --process-only --verbose " environment: ZITADEL_API_DOMAIN: "${ZITADEL_API_DOMAIN}" ZITADEL_CONSOLE_DOMAIN: "${ZITADEL_CONSOLE_DOMAIN}" secrets: - postgres_admin_user - postgres_admin_pass - postgres_zitadel_user - postgres_zitadel_pass - zitadel_org_human_username - zitadel_org_human_password - zitadel_pat_username - zitadel_pat_name - zitadel_pat_expire - zitadel_machine_username - zitadel_machine_name - zitadel_machine_expire networks: - zitadel-private volumes: - './scripts/process-configs.sh:/process-configs.sh:ro' - './templates:/zitadel/templates:ro' - 'zitadel-processed-configs:/tmp/zitadel-config' zitadel-api: image: ghcr.io/zitadel/zitadel:${RELEASE_VERSION_2} container_name: zitadel-api hostname: zitadel-api restart: unless-stopped user: "0" command: > start-from-init --config /zitadel/config/zitadel-config.yaml --config /zitadel/config/zitadel-secrets.yaml --steps /zitadel/config/zitadel-init-steps.yaml --masterkeyFile /run/secrets/zitadel_masterkey secrets: - zitadel_masterkey environment: ZITADEL_API_DOMAIN: "${ZITADEL_API_DOMAIN}" ZITADEL_CONSOLE_DOMAIN: "${ZITADEL_CONSOLE_DOMAIN}" # TLS is handled by Traefik ZITADEL_TLS_ENABLED: "false" # Make sure that the Zitadel log on startup correctly reflects the console domain ZITADEL_CONSOLE_INSTANCEMANAGEMENTURL: "https://${ZITADEL_CONSOLE_DOMAIN}/ui/v2/login" # IMPORTANT: These env vars are needed for the database connection to work in the initialization phase ZITADEL_DATABASE_POSTGRES_HOST: "zitadel-db" ZITADEL_DATABASE_POSTGRES_PORT: 5432 ZITADEL_DATABASE_POSTGRES_DATABASE: "zitadel" ZITADEL_DATABASE_POSTGRES_USER_USERNAME_FILE: /run/secrets/postgres_admin_user ZITADEL_DATABASE_POSTGRES_USER_PASSWORD_FILE: /run/secrets/postgres_admin_pass ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME_FILE: /run/secrets/postgres_zitadel_user ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD_FILE: /run/secrets/postgres_zitadel_pass # Override log level ZITADEL_LOG_LEVEL: debug # Write Access Logs to stdout ZITADEL_LOGSTORE_ACCESS_STDOUT_ENABLED: true healthcheck: test: ["CMD", "/app/zitadel", "ready"] #test: ["CMD-SHELL", "echo 0"] interval: 10s timeout: 60s retries: 5 start_period: 30s volumes: # Location of Zitadel configuration files generated by zitadel-config-processor - 'zitadel-processed-configs:/zitadel/config:ro' # Public keys to validate JWT tokens - './keys/system-user/system-user.pub:/zitadel/keys/system-user/system-user.pub:ro' - './keys/machine-user:/zitadel/keys/machine-user:rw' - './keys/login-client:/zitadel/keys/login-client:rw' ports: - 8080:8080 - 3000:3000 networks: - traefik-private - zitadel-private - zitadel-public depends_on: zitadel-db: condition: service_healthy zitadel-config-processor: condition: service_completed_successfully zitadel-console: image: ghcr.io/zitadel/zitadel-login:${RELEASE_VERSION_3} container_name: zitadel-console hostname: zitadel-console restart: unless-stopped user: "0" environment: # Public-facing domains ZITADEL_API_DOMAIN: "${ZITADEL_API_DOMAIN}" ZITADEL_CONSOLE_DOMAIN: "${ZITADEL_CONSOLE_DOMAIN}" CUSTOM_REQUEST_HEADERS: "Host:${ZITADEL_API_DOMAIN}" # Internal API access (Docker network direct) ZITADEL_API_URL: "http://zitadel-api:8080" # Console config NEXT_PUBLIC_BASE_PATH: "/ui/v2/login" ZITADEL_SERVICE_USER_TOKEN_FILE: "/zitadel/keys/login-client/login-client.pat" # Debugging NODE_TLS_REJECT_UNAUTHORIZED: 0 NEXT_DEBUG: "false" NEXT_TELEMETRY_DISABLED: "1" DEBUG: "*" # 👇 override the default entrypoint/command command: > sh -c "NODE_ENV=production next start -p 3000" networks: - traefik-private - zitadel-private volumes: - ./keys/login-client/login-client.pat:/zitadel/keys/login-client/login-client.pat:ro depends_on: zitadel-api: condition: service_healthy restart: false DOCKER_COMPOSE_EOF ``` #### docker-compose.override.tls-only.yaml ```yaml cat << 'DOCKER_COMPOSE_TLS_ONLY_EOF' | sudo -u ${ZITADEL_USER} tee ${ZITADEL_PATH}/docker-compose.override.tls-only.yaml > /dev/null --- services: zitadel-api: labels: # === Dium Configuration ============================================================================================ - "diun.enable=true" # === Homepage Configuration ======================================================================================== - "homepage.group=Security" - "homepage.name=Zitadel" - "homepage.icon=zitadel.png" - "homepage.description=Identity infrastructure, simplified for you." - "homepage.href=https://${ZITADEL_CONSOLE_DOMAIN}/" # point homepage link to the Console UI - "homepage.showStats=true" # === Dozzle Configuration ========================================================================================== - "dev.dozzle.group=Security" # === Traefik Configuration ========================================================================================= # -- Traefik General ------------------------------------------------------------------------------------------------ - "traefik.enable=true" - "traefik.docker.network=traefik-private" # -- Traefik Zitadel Routers ---------------------------------------------------------------------------------------- # Router and service for the Zitadel API (HTTP) - "traefik.http.routers.zitadel-api-tls-rest.entrypoints=websecure" - "traefik.http.routers.zitadel-api-tls-rest.rule=Host(`${ZITADEL_API_DOMAIN}`)" - "traefik.http.routers.zitadel-api-tls-rest.priority=50" - "traefik.http.routers.zitadel-api-tls-rest.service=zitadel-api-http" - "traefik.http.routers.zitadel-api-tls-rest.tls=true" - "traefik.http.routers.zitadel-api-tls-rest.tls.certResolver=letsencrypt" - "traefik.http.routers.zitadel-api-tls-rest.tls.options=tls-only@file" - "traefik.http.routers.zitadel-api-tls-rest.middlewares=zitadel-headers,tls-grpcweb" # Router and service for the Zitadel API (gRPC) - "traefik.http.routers.zitadel-api-tls-grpc.entrypoints=websecure" - 'traefik.http.routers.zitadel-api-tls-grpc.rule=Host(`${ZITADEL_API_DOMAIN}`) && PathPrefix(`/zitadel\\..+\\..+Service/`)' - "traefik.http.routers.zitadel-api-tls-grpc.priority=70" - "traefik.http.routers.zitadel-api-tls-grpc.service=zitadel-api-grpc" - "traefik.http.routers.zitadel-api-tls-grpc.tls=true" - "traefik.http.routers.zitadel-api-tls-grpc.tls.certResolver=letsencrypt" - "traefik.http.routers.zitadel-api-tls-grpc.tls.options=tls-only@file" - "traefik.http.routers.zitadel-api-tls-grpc.middlewares=zitadel-headers,tls-grpcweb" # -- Traefik Zitadel Middlewares ------------------------------------------------------------------------------------ # Pass correct host headers for API requests - "traefik.http.middlewares.zitadel-headers.headers.customrequestheaders.Host=${ZITADEL_API_DOMAIN}" - "traefik.http.middlewares.zitadel-headers.headers.customrequestheaders.X-Forwarded-Host=${ZITADEL_API_DOMAIN}" - "traefik.http.middlewares.zitadel-headers.headers.customrequestheaders.X-Original-Host=${ZITADEL_API_DOMAIN}" # Allow origins for both API and UI domains (needed for cross-origin requests) - "traefik.http.middlewares.tls-grpcweb.grpcweb.allowOrigins=${ZITADEL_API_DOMAIN},${ZITADEL_CONSOLE_DOMAIN}" # -- Traefik Services ----------------------------------------------------------------------------------------------- # Service definition for the Zitadel HTTP API (port 8080) - "traefik.http.services.zitadel-api-http.loadbalancer.server.scheme=h2c" - "traefik.http.services.zitadel-api-http.loadbalancer.passHostHeader=true" - "traefik.http.services.zitadel-api-http.loadbalancer.server.port=8080" # Service definition for the Zitadel gRPC API (port 3000) - "traefik.http.services.zitadel-api-grpc.loadbalancer.server.scheme=h2c" - "traefik.http.services.zitadel-api-grpc.loadbalancer.passHostHeader=true" - "traefik.http.services.zitadel-api-grpc.loadbalancer.server.port=3000" zitadel-console: labels: # === Traefik Configuration ========================================================================================= # -- Traefik General ------------------------------------------------------------------------------------------------ - "traefik.enable=true" - "traefik.docker.network=traefik-private" # -- Traefik Zitadel Routers ---------------------------------------------------------------------------------------- # Router for the Console + Login UI - "traefik.http.routers.zitadel-console-tls.entrypoints=websecure" - "traefik.http.routers.zitadel-console-tls.rule=Host(`${ZITADEL_CONSOLE_DOMAIN}`) && PathPrefix(`/ui/v2/login`)" - "traefik.http.routers.zitadel-console-tls.priority=110" - "traefik.http.routers.zitadel-console-tls.service=zitadel-console" - "traefik.http.routers.zitadel-console-tls.tls=true" - "traefik.http.routers.zitadel-console-tls.tls.certResolver=letsencrypt" - "traefik.http.routers.zitadel-console-tls.tls.options=tls-only@file" # Router for the root path of the UI domain → redirect to Console - "traefik.http.routers.zitadel-console-redirect.entrypoints=websecure" - "traefik.http.routers.zitadel-console-redirect.rule=Host(`${ZITADEL_CONSOLE_DOMAIN}`) && Path(`/`)" - "traefik.http.routers.zitadel-console-redirect.priority=120" - "traefik.http.routers.zitadel-console-redirect.service=noop@internal" - "traefik.http.routers.zitadel-console-redirect.tls=true" - "traefik.http.routers.zitadel-console-redirect.tls.certResolver=letsencrypt" - "traefik.http.routers.zitadel-console-redirect.tls.options=tls-only@file" - "traefik.http.routers.zitadel-console-redirect.middlewares=console-redirect" # -- Traefik Zitadel Middlewares ------------------------------------------------------------------------------------ # Middleware to redirect / → /ui/v2/console on the UI domain - "traefik.http.middlewares.console-redirect.redirectregex.regex=^https://${ZITADEL_CONSOLE_DOMAIN}/?$" - "traefik.http.middlewares.console-redirect.redirectregex.replacement=https://${ZITADEL_CONSOLE_DOMAIN}/ui/v2/login" - "traefik.http.middlewares.console-redirect.redirectregex.permanent=true" # -- Traefik Services ----------------------------------------------------------------------------------------------- # Service definition for the Console/Login UI (port 3000) - "traefik.http.services.zitadel-console.loadbalancer.server.scheme=http" - "traefik.http.services.zitadel-console.loadbalancer.passHostHeader=true" - "traefik.http.services.zitadel-console.loadbalancer.server.port=3000" DOCKER_COMPOSE_TLS_ONLY_EOF ```