Configure ZITADEL with Caddy
With these examples, you create and run a minimal Caddy configuration for ZITADEL with Docker Compose.
Whereas the guide focuses on the configuration for Caddy, you can inspect the configurations for ZITADEL and the database in the base Docker Compose file.
For running Caddy, you will extend the base Docker Compose file with the Caddy specific Docker Compose file.
When the docker compose command exits successfully, go to https://127.0.0.1.sslip.io/ui/console/?login_hint=zitadel-admin@zitadel.127.0.0.1.sslip.io and log in:
When the docker compose command exits successfully, go to https://127.0.0.1.sslip.io/ui/console/?login_hint=zitadel-admin@zitadel.127.0.0.1.sslip.io and log in:
base docker-compose.yaml
services:
  zitadel-disabled-tls:
    extends:
      service: zitadel-init
    command: 'start-from-setup --init-projections --masterkey "MasterkeyNeedsToHave32Characters" --config /zitadel.yaml --steps /zitadel.yaml'
    environment:
      ZITADEL_EXTERNALPORT: 80
      ZITADEL_EXTERNALSECURE: false
      ZITADEL_TLS_ENABLED: false
      # database configuration
      ZITADEL_DATABASE_POSTGRES_HOST: db
      ZITADEL_DATABASE_POSTGRES_PORT: 5432
      ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
      ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel_user
      ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_pw
      ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
      ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
      ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
      ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
    networks:
      - 'zitadel'
    depends_on:
      zitadel-init:
        condition: 'service_completed_successfully'
      db:
        condition: 'service_healthy'
  zitadel-external-tls:
    extends:
      service: zitadel-init
    command: 'start-from-setup --init-projections --masterkey "MasterkeyNeedsToHave32Characters" --config /zitadel.yaml --steps /zitadel.yaml'
    environment:
      ZITADEL_EXTERNALPORT: 443
      ZITADEL_EXTERNALSECURE: true
      ZITADEL_TLS_ENABLED: false
      # database configuration
      ZITADEL_DATABASE_POSTGRES_HOST: db
      ZITADEL_DATABASE_POSTGRES_PORT: 5432
      ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
      ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel_user
      ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_pw
      ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
      ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
      ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
      ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
    networks:
      - 'zitadel'
    depends_on:
      db:
        condition: 'service_healthy'
      zitadel-init:
        condition: 'service_completed_successfully'
  zitadel-enabled-tls:
    extends:
      service: zitadel-init
    command: 'start-from-setup --init-projections --masterkey "MasterkeyNeedsToHave32Characters" --config /zitadel.yaml --steps /zitadel.yaml'
    environment:
      ZITADEL_EXTERNALPORT: 443
      ZITADEL_EXTERNALSECURE: true
      ZITADEL_TLS_ENABLED: true
      ZITADEL_TLS_CERTPATH: /etc/certs/selfsigned.crt
      ZITADEL_TLS_KEYPATH: /etc/certs/selfsigned.key
      # database configuration
      ZITADEL_DATABASE_POSTGRES_HOST: db
      ZITADEL_DATABASE_POSTGRES_PORT: 5432
      ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
      ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel_user
      ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_pw
      ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
      ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
      ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
      ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
    volumes:
      - ./selfsigned.crt:/etc/certs/selfsigned.crt
      - ./selfsigned.key:/etc/certs/selfsigned.key
    networks:
      - 'zitadel'
    depends_on:
      zitadel-init:
        condition: 'service_completed_successfully'
      db:
        condition: 'service_healthy'
  zitadel-init:
    user: '$UID'
    image: '${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}'
    command: 'init --config /zitadel.yaml'
    depends_on:
      db:
        condition: 'service_healthy'
    environment:
      # Using an external domain other than localhost proofs, that the proxy configuration works.
      # If ZITADEL can't resolve a requests original host to this domain,
      # it will return a 404 Instance not found error.
      ZITADEL_EXTERNALDOMAIN: 127.0.0.1.sslip.io
      # In case something doesn't work as expected,
      # it can be handy to be able to read the access logs.
      ZITADEL_LOGSTORE_ACCESS_STDOUT_ENABLED: true
      # For convenience, ZITADEL should not ask to change the initial admin users password.
      ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: false
      # database configuration
      ZITADEL_DATABASE_POSTGRES_HOST: db
      ZITADEL_DATABASE_POSTGRES_PORT: 5432
      ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
      ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel_user
      ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_pw
      ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
      ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
      ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
      ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
    networks:
      - 'zitadel'
    healthcheck:
      test: ["CMD", "/app/zitadel", "ready"]
      interval: '10s'
      timeout: '5s'
      retries: 5
      start_period: '10s'
  db:
    restart: 'always'
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready"]
      interval: 5s
      timeout: 60s
      retries: 10
      start_period: 5s
    networks:
      - 'zitadel'
    volumes:
      - 'data:/var/lib/postgresql/data:rw'
networks:
  zitadel:
volumes:
  data:
specific docker-compose.yaml
services:
  proxy-disabled-tls:
    image: "caddy:latest"
    volumes:
      - "./disabled-tls.Caddyfile:/etc/caddy/Caddyfile:ro"
    ports:
      - "80:80"
    networks:
      - 'zitadel'
    depends_on:
      zitadel-disabled-tls:
        condition: 'service_healthy'
  proxy-external-tls:
    image: "caddy:latest"
    volumes:
      - "./external-tls.Caddyfile:/etc/caddy/Caddyfile:ro"
      - "./selfsigned.crt:/etc/certs/selfsigned.crt:ro"
      - "./selfsigned.key:/etc/certs/selfsigned.key:ro"
    ports:
      - "443:443"
    networks:
      - 'zitadel'
    depends_on:
      zitadel-external-tls:
        condition: 'service_healthy'
  proxy-enabled-tls:
    image: "caddy:latest"
    volumes:
      - "./enabled-tls.Caddyfile:/etc/caddy/Caddyfile:ro"
      - "./selfsigned.crt:/etc/certs/selfsigned.crt:ro"
      - "./selfsigned.key:/etc/certs/selfsigned.key:ro"
    ports:
      - "443:443"
    networks:
      - 'zitadel'
    depends_on:
      zitadel-enabled-tls:
        condition: 'service_healthy'
networks:
  zitadel:
You can either setup your environment for TLS mode external or TLS mode enabled.
TLS mode external​
Caddy terminates TLS and forwards the requests to ZITADEL via unencrypted h2c. This example uses an unsafe self-signed certificate for CaddyBy executing the commands below, you will download the files necessary to run ZITADEL behind Caddy with the following config:external-tls.Caddyfile
https://127.0.0.1.sslip.io {
	tls /etc/certs/selfsigned.crt /etc/certs/selfsigned.key
	reverse_proxy h2c://zitadel-external-tls:8080
}
# Download the configuration files.
export ZITADEL_CONFIG_FILES=https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/reverseproxy
wget ${ZITADEL_CONFIG_FILES}/docker-compose.yaml -O docker-compose-base.yaml --quiet 
wget ${ZITADEL_CONFIG_FILES}/caddy/docker-compose.yaml -O docker-compose-caddy.yaml --quiet 
wget ${ZITADEL_CONFIG_FILES}/caddy/external-tls.Caddyfile -O external-tls.Caddyfile --quiet 
# Generate a self signed certificate and key.
openssl req -x509 -batch -subj "/CN=127.0.0.1.sslip.io/O=ZITADEL Demo" -nodes -newkey rsa:2048 -keyout ./selfsigned.key -out ./selfsigned.crt 2>/dev/null
# Run the database, ZITADEL and Caddy.
docker compose --file docker-compose-base.yaml --file docker-compose-caddy.yaml up --detach --wait db zitadel-init zitadel-external-tls proxy-external-tls
# Test that gRPC and HTTP APIs work. Empty brackets like {} means success.
# Make sure you have the grpcurl cli installed on your machine https://github.com/fullstorydev/grpcurl?tab=readme-ov-file#installation
grpcurl --insecure 127.0.0.1.sslip.io:443 zitadel.admin.v1.AdminService/Healthz
curl --insecure https://127.0.0.1.sslip.io:443/admin/v1/healthz
- username: zitadel-admin@zitadel.127.0.0.1.sslip.io
- password: Password1!
If the console loads normally, you know that the HTTP and gRPC-Web and gRPC APIs are working correctly.
# You can now stop the database, ZITADEL and Caddy.
docker compose --file docker-compose-base.yaml --file docker-compose-caddy.yaml down
TLS mode enabled​
Caddy terminates TLS and forwards the requests to ZITADEL via encrypted HTTP/2. This example uses an unsafe self-signed certificate for Caddy and the same for ZITADEL.By executing the commands below, you will download the files necessary to run ZITADEL behind Caddy with the following config:enabled-tls.Caddyfile
https://127.0.0.1.sslip.io {
	tls /etc/certs/selfsigned.crt /etc/certs/selfsigned.key
	reverse_proxy https://zitadel-enabled-tls:8080 {
		transport http {
			tls_insecure_skip_verify
		}
	}
}
# Download the configuration files.
export ZITADEL_CONFIG_FILES=https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/manage/reverseproxy
wget ${ZITADEL_CONFIG_FILES}/docker-compose.yaml -O docker-compose-base.yaml --quiet 
wget ${ZITADEL_CONFIG_FILES}/caddy/docker-compose.yaml -O docker-compose-caddy.yaml --quiet 
wget ${ZITADEL_CONFIG_FILES}/caddy/enabled-tls.Caddyfile -O enabled-tls.Caddyfile --quiet 
# Generate a self signed certificate and key.
openssl req -x509 -batch -subj "/CN=127.0.0.1.sslip.io/O=ZITADEL Demo" -nodes -newkey rsa:2048 -keyout ./selfsigned.key -out ./selfsigned.crt 2>/dev/null
# Run the database, ZITADEL and Caddy.
docker compose --file docker-compose-base.yaml --file docker-compose-caddy.yaml up --detach --wait db zitadel-init zitadel-enabled-tls proxy-enabled-tls
# Test that gRPC and HTTP APIs work. Empty brackets like {} means success.
# Make sure you have the grpcurl cli installed on your machine https://github.com/fullstorydev/grpcurl?tab=readme-ov-file#installation
grpcurl --insecure 127.0.0.1.sslip.io:443 zitadel.admin.v1.AdminService/Healthz
curl --insecure https://127.0.0.1.sslip.io:443/admin/v1/healthz
- username: zitadel-admin@zitadel.127.0.0.1.sslip.io
- password: Password1!
If the console loads normally, you know that the HTTP and gRPC-Web and gRPC APIs are working correctly.
# You can now stop the database, ZITADEL and Caddy.
docker compose --file docker-compose-base.yaml --file docker-compose-caddy.yaml down