# Set up ZITADEL with Docker Compose

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.

## [Prerequisites](/content/docs/self-hosting/deploy/compose#prerequisites/index.html)

- Docker Engine 24+ with the Compose plugin (`docker compose`)
- A machine with at least 2 GB RAM

See [Requirements](/content/docs/self-hosting/manage/requirements/index.html) for supported database, cache, and proxy versions.

## [Stage 1 — Quickstart (2 minutes)](/content/docs/self-hosting/deploy/compose#stage-1-quickstart-2-minutes/index.html)

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
pc .env.example .env
docker compose up -d --wait
```

That's it. Visit [http://localhost:8080](http://localhost:8080/) to open the login screen.

Visit [http://localhost:8080/ui/console?login_hint=zitadel-admin@zitadel.localhost](http://localhost:8080/ui/console?login_hint=zitadel-admin@zitadel.localhost) and enter `Password1!` to log in.

The base stack runs: **Traefik** ( [reverse proxy](/content/docs/self-hosting/manage/reverseproxy/reverse_proxy/index.html)) → **ZITADEL API** (Go) + **ZITADEL Login** (Next.js) → **PostgreSQL**. All routing, including [gRPC over HTTP/2](/content/docs/self-hosting/manage/http2/index.html), is handled automatically by Traefik — no extra configuration needed.

## [Stage 2 — Homelab / Semi-Production](/content/docs/self-hosting/deploy/compose#stage-2-homelab-semi-production/index.html)

### [Use your own domain](/content/docs/self-hosting/deploy/compose#use-your-own-domain/index.html)

Edit `.env` and set your real domain (see [Custom Domains](/content/docs/self-hosting/manage/custom-domain/index.html) for details):

```
ZITADEL_DOMAIN=auth.example.com
```

### [Enable TLS](/content/docs/self-hosting/deploy/compose#enable-tls/index.html)

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.

### [Harden secrets](/content/docs/self-hosting/deploy/compose#harden-secrets/index.html)

The masterkey [encrypts sensitive data at rest](/content/docs/concepts/architecture/secrets/index.html). Once ZITADEL has been initialized with a masterkey, it **cannot be changed** without losing access to encrypted data. Generate it **before first start** and store it safely.

```
# 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

# Generate the Login UI session cookie secret (at least 32 characters)
echo "LOGIN_SESSION_COOKIE_SECRET=$(openssl rand -base64 32)" >> .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
```

The commands above cover the masterkey, the session cookie secret and the database passwords. The **initial admin user** (`zitadel-admin`) has a password too, defaulting to `Password1!`. To set your own before the first start, add it to `.env`:

```
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD=MyInitialPassw0rd!
```

The initial admin password must satisfy the default [password complexity policy](/content/docs/guides/manage/console/default-settings#password-complexity/index.html): at least 8 characters, including an uppercase letter, a lowercase letter, a number, and a symbol. A password that doesn't meet these requirements causes the initial setup to fail with a password complexity error. Like all `ZITADEL_FIRSTINSTANCE_*` variables, it is only applied during the first start; change it later from the Console.

### [External URL settings](/content/docs/self-hosting/deploy/compose#external-url-settings/index.html)

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](/content/docs/self-hosting/manage/tls_modes/index.html) for details.

### [Enable caching with Redis](/content/docs/self-hosting/deploy/compose#enable-caching-with-redis/index.html)

See [Cache Configuration](/content/docs/self-hosting/manage/cache/index.html) 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
```

## [Stage 3 — Beyond Compose](/content/docs/self-hosting/deploy/compose#stage-3-beyond-compose/index.html)

### [Production-like init/setup/start split](/content/docs/self-hosting/deploy/compose#production-like-init-setup-start-split/index.html)

For controlled upgrades, separate [database initialization from the running API](/content/docs/self-hosting/manage/updating_scaling/index.html):

```
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:

1. `zitadel-init` — runs database migrations (one-shot)
2. `zitadel-setup` — configures the instance (one-shot)
3. `zitadel-api` — starts the API server (long-running)

### [Enable observability](/content/docs/self-hosting/deploy/compose#enable-observability/index.html)

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](/content/docs/self-hosting/manage/metrics/overview/index.html) for Prometheus scraping and available metric types.

### [Scale API replicas](/content/docs/self-hosting/deploy/compose#scale-api-replicas/index.html)

Scaling requires the **prodlike overlay** so that migrations run once in `zitadel-init` instead of on every replica.

```
docker compose --env-file .env \
  -f docker-compose.yml \
  -f docker-compose.prodlike.yml \
  up -d --scale zitadel-api=3
```

### [Updating ZITADEL](/content/docs/self-hosting/deploy/compose#updating-zitadel/index.html)

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
```

`ZITADEL_FIRSTINSTANCE_*` and `ZITADEL_DEFAULTINSTANCE_*` environment variables are only applied during the **initial setup**. To change settings on an existing installation, use the Admin Console or Admin API.

### [Moving to Kubernetes](/content/docs/self-hosting/deploy/compose#moving-to-kubernetes/index.html)

Docker Compose is ideal for getting started and homelab deployments. For production workloads, review the [Production Checklist](/content/docs/self-hosting/manage/productionchecklist/index.html) and deploy with the official [Helm chart for Kubernetes](/content/docs/self-hosting/deploy/kubernetes/index.html).

The compose pack and the Helm chart share the same application configuration model (`ZITADEL_*` environment variables), so migration is straightforward.
