apps/docs/content/self-hosting/deploy/compose.mdx
import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx'; import DefaultUser from './_defaultuser.mdx' import Next from './_next.mdx'
This guide takes you from zero to a running ZITADEL instance in minutes and then shows you how to harden it for a homelab or semi-production deployment.
docker compose)See Requirements for supported database, cache, and proxy versions.
Download the two required files, copy the example config, and start:
mkdir zitadel-compose && cd zitadel-compose
# Download the compose file and example environment
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/docker-compose.yml &&
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/.env.example
# Create your environment file and start
cp .env.example .env
docker compose up -d --wait
That's it. Visit http://localhost:8080 to open the login screen.
<DefaultUser components={props.components} /> <Callout type="info"> The base stack runs: **Traefik** ([reverse proxy](/self-hosting/manage/reverseproxy/reverse_proxy)) → **ZITADEL API** (Go) + **ZITADEL Login** (Next.js) → **PostgreSQL**. All routing, including [gRPC over HTTP/2](/self-hosting/manage/http2), is handled automatically by Traefik — no extra configuration needed. </Callout>Edit .env and set your real domain (see Custom Domains for details):
ZITADEL_DOMAIN=auth.example.com
Pick a TLS mode and download the matching overlay file:
| Mode | Overlay file | When to use |
|---|---|---|
| Let's Encrypt | docker-compose.mode-letsencrypt.yml | Public domain, automatic certs |
| External TLS | docker-compose.mode-external-tls.yml | Behind a load balancer, CDN, or WAF that terminates TLS |
| Local TLS | docker-compose.mode-local-tls.yml | Self-signed certs for LAN-only access |
Download the overlay you need, then start with both files:
# Download the overlay (replace with your chosen mode)
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/docker-compose.mode-letsencrypt.yml
# Start with the overlay
docker compose --env-file .env \
-f docker-compose.yml \
-f docker-compose.mode-letsencrypt.yml \
up -d --wait
Set LETSENCRYPT_EMAIL in .env to receive certificate expiry notifications.
# Generate a secure masterkey (must be exactly 32 characters)
ZITADEL_MASTERKEY=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)
echo "ZITADEL_MASTERKEY=$ZITADEL_MASTERKEY" >> .env
# Set strong database passwords
echo "POSTGRES_ADMIN_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" >> .env
echo "POSTGRES_ZITADEL_PASSWORD=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)" >> .env
These three settings must match your public endpoint exactly:
| Setting | Meaning | Example |
|---|---|---|
ZITADEL_DOMAIN | The public domain users type in | auth.example.com |
ZITADEL_EXTERNALPORT | The port visible to users | 443 for HTTPS |
ZITADEL_EXTERNALSECURE | Whether the public URL uses HTTPS | true for any TLS mode |
If these don't match reality, ZITADEL returns "Instance not found" errors. This is the most common deployment issue — see TLS Modes for details.
<NoteInstanceNotFound components={props.components} />See Cache Configuration for all available options. Add these to .env:
ZITADEL_CACHES_CONNECTORS_REDIS_ENABLED=true
ZITADEL_CACHES_INSTANCE_CONNECTOR=redis
ZITADEL_CACHES_MILESTONES_CONNECTOR=redis
ZITADEL_CACHES_ORGANIZATION_CONNECTOR=redis
Then start with the cache profile:
docker compose --env-file .env -f docker-compose.yml --profile cache up -d --wait
For controlled upgrades, separate database initialization from the running API:
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/docker-compose.prodlike.yml &&
docker compose --env-file .env \
-f docker-compose.yml \
-f docker-compose.prodlike.yml \
up -d --wait
This creates three ZITADEL containers:
zitadel-init — runs database migrations (one-shot)zitadel-setup — configures the instance (one-shot)zitadel-api — starts the API server (long-running)Set in .env:
ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_TYPE=grpc
Download the collector configuration and start with the observability profile:
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel/main/deploy/compose/otel-collector-config.yaml &&
docker compose --env-file .env -f docker-compose.yml --profile observability up -d --wait
Traces are logged to the collector's stdout by default (docker compose logs otel-collector).
To forward traces to your own backend (Grafana Tempo, Jaeger, OpenObserve, etc.), set OTEL_BACKEND_ENDPOINT in .env and uncomment the otlp exporter in otel-collector-config.yaml.
See Metrics for Prometheus scraping and available metric types.
docker compose --env-file .env \
-f docker-compose.yml \
-f docker-compose.prodlike.yml \
up -d --scale zitadel-api=3
Edit ZITADEL_VERSION in .env, then:
docker compose --env-file .env -f docker-compose.yml pull
docker compose --env-file .env -f docker-compose.yml up -d --wait
Docker Compose is ideal for getting started and homelab deployments. For production workloads, review the Production Checklist and deploy with the official Helm chart for Kubernetes.
The compose pack and the Helm chart share the same application configuration model (ZITADEL_* environment variables), so migration is straightforward.