Configure ZITADEL with NGINX
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 configurations for ZITADEL and the database in the base Docker Compose file.
For running NGINX, you will extend the base Docker Compose file with the NGINX specific Docker Compose file.
When the docker compose command exits successfully, go to http://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:
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
version: '3.8'
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: root
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: root
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-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: root
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: root
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:
PGUSER: root
POSTGRES_PASSWORD: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready", "-d", "zitadel", "-U", "postgres"]
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
version: '3.8'
services:
proxy-disabled-tls:
image: "nginx:mainline-alpine"
volumes:
- "./nginx-disabled-tls.conf:/etc/nginx/nginx.conf:ro"
ports:
- "80:80"
networks:
- 'zitadel'
depends_on:
zitadel-disabled-tls:
condition: 'service_healthy'
proxy-external-tls:
image: "nginx:mainline-alpine"
volumes:
- "./nginx-external-tls.conf:/etc/nginx/nginx.conf: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: "nginx:mainline-alpine"
volumes:
- "./nginx-enabled-tls.conf:/etc/nginx/nginx.conf: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 disabled, TLS mode external or TLS mode enabled.
TLS mode disabled​
Neither NGINX nor ZITADEL terminates TLS. Nevertheless, NGINX forwards unencrypted HTTP/2 traffic, aka h2c, to ZITADEL.By executing the commands below, you will download the files necessary to run ZITADEL behind NGINX with the following config:nginx-disabled-tls.conf
events {
worker_connections 1024;
}
http {
server {
listen 80;
http2 on;
location / {
grpc_pass grpc://zitadel-disabled-tls:8080;
grpc_set_header Host $host;
}
}
}
# 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
wget ${ZITADEL_CONFIG_FILES}/nginx/docker-compose.yaml -O docker-compose-nginx.yaml
wget ${ZITADEL_CONFIG_FILES}/nginx/nginx-disabled-tls.conf -O nginx-disabled-tls.conf
# Run the database, ZITADEL and NGINX.
docker compose --file docker-compose-base.yaml --file docker-compose-nginx.yaml up --detach proxy-disabled-tls
# Test that gRPC and HTTP APIs work. Empty brackets like {} means success.
sleep 3
grpcurl --plaintext 127.0.0.1.sslip.io:80 zitadel.admin.v1.AdminService/Healthz
curl http://127.0.0.1.sslip.io:80/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 NGINX.
docker compose --file docker-compose-base.yaml --file docker-compose-nginx.yaml down
TLS mode external​
NGINX terminates TLS and forwards the requests to ZITADEL via unencrypted h2c. This example uses an unsafe self-signed certificate for NGINXBy executing the commands below, you will download the files necessary to run ZITADEL behind NGINX with the following config:nginx-external-tls.conf
events {
worker_connections 1024;
}
http {
server {
listen 443 ssl;
http2 on;
ssl_certificate /etc/certs/selfsigned.crt;
ssl_certificate_key /etc/certs/selfsigned.key;
location / {
grpc_pass grpc://zitadel-external-tls:8080;
grpc_set_header Host $host;
}
}
}
# 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
wget ${ZITADEL_CONFIG_FILES}/nginx/docker-compose.yaml -O docker-compose-nginx.yaml
wget ${ZITADEL_CONFIG_FILES}/nginx/nginx-external-tls.conf -O nginx-external-tls.conf
# 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
# Run the database, ZITADEL and NGINX.
docker compose --file docker-compose-base.yaml --file docker-compose-nginx.yaml up --detach proxy-external-tls
# Test that gRPC and HTTP APIs work. Empty brackets like {} means success.
sleep 3
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 NGINX.
docker compose --file docker-compose-base.yaml --file docker-compose-nginx.yaml down
TLS mode enabled​
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
events {
worker_connections 1024;
}
http {
server {
listen 443 ssl;
http2 on;
ssl_certificate /etc/certs/selfsigned.crt;
ssl_certificate_key /etc/certs/selfsigned.key;
location / {
grpc_pass grpcs://zitadel-enabled-tls:8080;
grpc_set_header Host $host;
}
}
}
# 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
wget ${ZITADEL_CONFIG_FILES}/nginx/docker-compose.yaml -O docker-compose-nginx.yaml
wget ${ZITADEL_CONFIG_FILES}/nginx/nginx-enabled-tls.conf -O nginx-enabled-tls.conf
# 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
# Run the database, ZITADEL and NGINX.
docker compose --file docker-compose-base.yaml --file docker-compose-nginx.yaml up --detach proxy-enabled-tls
# Test that gRPC and HTTP APIs work. Empty brackets like {} means success.
sleep 3
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 NGINX.
docker compose --file docker-compose-base.yaml --file docker-compose-nginx.yaml down