A ZITADEL Load Balancing Example
With this example configuration, you create a near production environment for ZITADEL with Docker Compose.
The stack consists of three long-running containers:
- A Traefik reverse proxy with upstream HTTP/2 enabled, issuing a self-signed TLS certificate.
- A secure ZITADEL container configured for a custom domain. As we terminate TLS with Traefik, we configure ZITADEL for --tlsMode external.
- An insecure PostgreSQL.
The setup is tested against Docker version 20.10.17 and Docker Compose version v2.2.3
By executing the commands below, you will download the following files:
docker-compose.yaml
version: '3.8'
services:
  traefik:
    networks:
      - 'zitadel'
    image: "traefik:latest"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "./example-traefik.yaml:/etc/traefik/traefik.yaml"
  zitadel:
    restart: 'always'
    networks:
      - 'zitadel'
    image: 'ghcr.io/zitadel/zitadel:stable'
    command: 'start-from-init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml --steps /example-zitadel-init-steps.yaml --masterkey "${ZITADEL_MASTERKEY}" --tlsMode external'
    depends_on:
      db:
        condition: 'service_healthy'
    volumes:
      - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
      - './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro'
      - './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro'
  db:
    image: postgres:16-alpine
    restart: always
    environment:
      - POSTGRES_USER=root
      - POSTGRES_PASSWORD=postgres
    networks:
      - 'zitadel'
    healthcheck:
      test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"]
      interval: 10s
      timeout: 60s
      retries: 5
      start_period: 10s 
    volumes:
      - 'data:/var/lib/postgresql/data:rw'
networks:
  zitadel:
volumes:
  data:
example-traefik.yaml
log:
  level: DEBUG
accessLog: {}
entrypoints:
  web:
    address: ":80"
  websecure:
    address: ":443"
tls:
  stores:
    default:
      # generates self-signed certificates
      defaultCertificate:
providers:
  file:
    filename: /etc/traefik/traefik.yaml
http:
  middlewares:
    zitadel:
      headers:
        isDevelopment: false
        allowedHosts:
        - 'my.domain'
        customRequestHeaders:
          authority: 'my.domain'
    redirect-to-https:
      redirectScheme:
        scheme: https
        port: 443
        permanent: true
  routers:
    # Redirect HTTP to HTTPS
    router0:
      entryPoints:
      - web
      middlewares:
      - redirect-to-https
      rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)'
      service: zitadel
    # The actual ZITADEL router
    router1:
      entryPoints:
      - websecure
      service: zitadel
      middlewares:
      - zitadel
      rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)'
      tls:
        domains:
          - main: "my.domain"
            sans:
              - "*.my.domain"
              - "my.domain"
  # Add the service
  services:
    zitadel:
      loadBalancer:
        servers:
          # h2c is the scheme for unencrypted HTTP/2
        - url: h2c://zitadel:8080
        passHostHeader: true
example-zitadel-config.yaml
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml
Log:
  Level: 'info'
# Make ZITADEL accessible over HTTPs, not HTTP
ExternalSecure: true
ExternalDomain: my.domain
ExternalPort: 443
# If not using the docker compose example, adjust these values for connecting ZITADEL to your PostgreSQL
Database:
  postgres:
    Host: 'db'
    Port: 5432
    Database: zitadel
    User:
      SSL:
        Mode: 'disable'
    Admin:
      SSL:
        Mode: 'disable'
LogStore:
  Access:
    Stdout:
      Enabled: true
example-zitadel-secrets.yaml
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml
# If not using the docker compose example, adjust these values for connecting ZITADEL to your PostgreSQL
Database:
  postgres:
    User:
      # If the user doesn't exist already, it is created
      Username: 'zitadel_user'
      Password: 'zitadel'
    Admin:
      Username: 'root'
      Password: 'postgres'
example-zitadel-init-steps.yaml
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml
FirstInstance:
  Org:
    Name: 'My Org'
    Human:
      # use the loginname root@my-org.my.domain
      Username: 'root'
      Password: 'RootPassword1!'
# Download the docker compose example configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml
# Download the Traefik example configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml
# Download and adjust the example configuration file containing standard configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml
# Download and adjust the example configuration file containing secret configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-secrets.yaml
# Download and adjust the example configuration file containing database initialization configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml
# A single ZITADEL instance always needs the same 32 bytes long masterkey
# Generate one to a file if you haven't done so already and pass it as environment variable
tr -dc A-Za-z0-9 </dev/urandom | head -c 32 > ./zitadel-masterkey
export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)"
# Run the database and application containers
docker compose up --detach
Make 127.0.0.1 available at my.domain. For example, this can be achieved with an entry 127.0.0.1 my.domain in the /etc/hosts file.
Open your favorite internet browser at https://my.domain/ui/console/. You can safely proceed, if your browser warns you about the insecure self-signed TLS certificate. This is the IAM admin users login according to your configuration in the example-zitadel-init-steps.yaml:
- username: root@my-org.my.domain
- password: RootPassword1!
Read more about the login process.
This guide is based on a local setup. If you encounter an error "Instance Not Found" please read the following section: Instance not found
Troubleshooting​
You can connect to cockroach like this: docker exec -it loadbalancing-example-my-cockroach-db-1 cockroach sql --host my-cockroach-db --certs-dir /cockroach/certs/
For example, to show all login names: docker exec -it loadbalancing-example-my-cockroach-db-1 cockroach sql --database zitadel --host my-cockroach-db --certs-dir /cockroach/certs/ --execute "select * from projections.login_names3"