With these examples, you create and run a minimal NGINX configuration for Zitadel with Docker Compose.
Whereas the guide focuses on the configuration for NGINX, you can inspect the settings for the Zitadel API, the Zitadel login and the database in the base Docker Compose file.
base docker-compose.yaml
services: zitadel-disabled-tls: extends: service: zitadel-init command: 'start-from-setup --masterkey "MasterkeyNeedsToHave32Characters"' environment: ZITADEL_EXTERNALPORT: 80 ZITADEL_EXTERNALSECURE: false ZITADEL_TLS_ENABLED: false networks: - app - db depends_on: zitadel-init: condition: 'service_completed_successfully' db: condition: 'service_healthy' zitadel-external-tls: extends: service: zitadel-init command: 'start-from-setup --masterkey "MasterkeyNeedsToHave32Characters"' environment: ZITADEL_EXTERNALPORT: 443 ZITADEL_EXTERNALSECURE: true ZITADEL_TLS_ENABLED: false networks: - app - db depends_on: db: condition: 'service_healthy' zitadel-init: condition: 'service_completed_successfully' zitadel-enabled-tls: extends: service: zitadel-init command: 'start-from-setup --masterkey "MasterkeyNeedsToHave32Characters"' 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 volumes: - ./selfsigned.crt:/etc/certs/selfsigned.crt - ./selfsigned.key:/etc/certs/selfsigned.key networks: - app - db depends_on: zitadel-init: condition: 'service_completed_successfully' db: condition: 'service_healthy' zitadel-init: image: '${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}' command: 'init' 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_USER_PASSWORD: zitadel_pw # Set up a service account with IAM_LOGIN_CLIENT role and write the PAT to the file ./login-client.pat ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /current-dir/login-client.pat ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Automatically Initialized instance Login Client ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: '2029-01-01T00:00:00Z' # The master key is used to networks: - db healthcheck: test: [ "CMD", "/app/zitadel", "ready" ] interval: '10s' timeout: '5s' retries: 5 start_period: '10s' volumes: - '.:/current-dir:rw' db: restart: 'always' image: postgres:17-alpine environment: POSTGRES_PASSWORD: postgres healthcheck: test: [ "CMD-SHELL", "pg_isready" ] interval: 5s timeout: 60s retries: 10 start_period: 5s networks: - db volumes: - 'data:/var/lib/postgresql/data:rw' login-disabled-tls: restart: 'unless-stopped' image: 'ghcr.io/zitadel/zitadel-login:latest' environment: - ZITADEL_API_URL=http://zitadel-disabled-tls:8080 - NEXT_PUBLIC_BASE_PATH=/ui/v2/login - ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client.pat - CUSTOM_REQUEST_HEADERS=Host:127.0.0.1.sslip.io - NODE_ENV=test volumes: - '.:/current-dir:ro' networks: - app depends_on: zitadel-disabled-tls: condition: 'service_healthy' login-external-tls: restart: 'unless-stopped' image: 'ghcr.io/zitadel/zitadel-login:latest' environment: - ZITADEL_API_URL=http://zitadel-external-tls:8080 - NEXT_PUBLIC_BASE_PATH=/ui/v2/login - ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client.pat - CUSTOM_REQUEST_HEADERS=Host:127.0.0.1.sslip.io volumes: - '.:/current-dir:ro' networks: - app depends_on: zitadel-external-tls: condition: 'service_healthy' login-enabled-tls: restart: 'unless-stopped' image: 'ghcr.io/zitadel/zitadel-login:latest' environment: - ZITADEL_API_URL=https://zitadel-enabled-tls:8080 - NEXT_PUBLIC_BASE_PATH=/ui/v2/login - ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client.pat - CUSTOM_REQUEST_HEADERS=Host:127.0.0.1.sslip.io - NODE_TLS_REJECT_UNAUTHORIZED=0 volumes: - '.:/current-dir:ro' networks: - app depends_on: zitadel-enabled-tls: condition: 'service_healthy'networks: app: db:volumes: data:
For running NGINX, you will extend the base Docker Compose file with the NGINX specific Docker Compose file.
specific docker-compose.yaml
NGINX terminates TLS and forwards the requests to Zitadel via unencrypted h2c. This example uses an unsafe self-signed certificate for NGINX. By executing the commands below, you will download the files necessary to run Zitadel behind NGINX with the following config:
nginx-external-tls.conf
# Download the configuration files.export ZITADEL_CONFIG_FILES=https://raw.githubusercontent.com/zitadel/zitadel/main/apps/docs/content/self-hosting/manage/reverseproxywget ${ZITADEL_CONFIG_FILES}/docker-compose.yaml -O docker-compose-base.yaml --quietwget ${ZITADEL_CONFIG_FILES}/nginx/docker-compose.yaml -O docker-compose-nginx.yaml --quietwget ${ZITADEL_CONFIG_FILES}/nginx/nginx-external-tls.conf -O nginx-external-tls.conf --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, and NGINX.docker compose --file docker-compose-base.yaml --file docker-compose-nginx.yaml up --detach --wait db zitadel-init zitadel-external-tls login-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#installationgrpcurl --insecure 127.0.0.1.sslip.io:443 zitadel.admin.v1.AdminService/Healthzcurl --insecure https://127.0.0.1.sslip.io:443/admin/v1/healthz
If the management console loads normally, you know that the HTTP and gRPC-Web and gRPC APIs are working correctly.
# You can now stop the database, the Zitadel API, the Zitadel login and NGINX.docker compose --file docker-compose-base.yaml --file docker-compose-nginx.yaml down
NGINX terminates TLS and forwards the requests to Zitadel via encrypted HTTP/2. This example uses an unsafe self-signed certificate for NGINX and the same for Zitadel. By executing the commands below, you will download the files necessary to run Zitadel behind NGINX with the following config:
nginx-enabled-tls.conf
# Download the configuration files.export ZITADEL_CONFIG_FILES=https://raw.githubusercontent.com/zitadel/zitadel/main/apps/docs/content/self-hosting/manage/reverseproxywget ${ZITADEL_CONFIG_FILES}/docker-compose.yaml -O docker-compose-base.yaml --quietwget ${ZITADEL_CONFIG_FILES}/nginx/docker-compose.yaml -O docker-compose-nginx.yaml --quietwget ${ZITADEL_CONFIG_FILES}/nginx/nginx-enabled-tls.conf -O nginx-enabled-tls.conf --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, and NGINX.docker compose --file docker-compose-base.yaml --file docker-compose-nginx.yaml up --detach --wait db zitadel-init zitadel-enabled-tls login-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#installationgrpcurl --insecure 127.0.0.1.sslip.io:443 zitadel.admin.v1.AdminService/Healthzcurl --insecure https://127.0.0.1.sslip.io:443/admin/v1/healthz
If the management console loads normally, you know that the HTTP and gRPC-Web and gRPC APIs are working correctly.
# You can now stop the database, the Zitadel API, the Zitadel login and NGINX.docker compose --file docker-compose-base.yaml --file docker-compose-nginx.yaml down