mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-07 22:55:12 +00:00
chore: improve docker compose template, tests and docs (#11593)
Replaces the single-file `docker-compose.yaml` quickstart with a production-aware, Traefik-based compose pack in `deploy/compose/`. The pack covers the full arc from a 2-minute localhost quickstart to a hardened homelab or semi-production deployment. ### What's in the pack **Stack**: Traefik (proxy) → ZITADEL API (Go `:8080`) + ZITADEL Login (Next.js `:3000`) → PostgreSQL All HTTP/gRPC routing is handled by Traefik via Docker labels — no manual proxy config needed. The Login V2 UI is enabled by default. Login URLs are derived automatically from `ZITADEL_DOMAIN`, `ZITADEL_EXTERNALPORT`, and `ZITADEL_PUBLIC_SCHEME` — no separate URL variables needed. **Compose files** | File | Purpose | |------|---------| | `docker-compose.yml` | Base stack — works standalone. Uses explicit `name: zitadel` network for reliable Traefik service discovery. | | `docker-compose.mode-letsencrypt.yml` | TLS overlay: ACME HTTP challenge | | `docker-compose.mode-external-tls.yml` | TLS overlay: upstream LB/CDN terminates TLS. Uses `forwardedHeaders.trustedIPs` (configurable via `TRAEFIK_TRUSTED_IPS`) instead of `insecure=true`. | | `docker-compose.mode-local-tls.yml` | TLS overlay: self-signed certs for LAN | | `docker-compose.prodlike.yml` | Splits init / setup / start for controlled upgrades | | `docker-compose.test.yml` | CI overlay: swaps images to locally-built `:local` tags | **Optional profiles**: `cache` (Redis), `observability` (OpenTelemetry Collector) ### Build infra - New `@zitadel/api:pack` and `@zitadel/login:pack` Nx targets build local Docker images (`zitadel/zitadel:local`, `zitadel/zitadel-login:local`) for use in CI and local testing - `apps/api/Dockerfile` now accepts a `BINARY` build arg so local and release builds share the same image ### Testing - New `@zitadel/compose` Nx project with targets: `test-config` (validates all overlay combinations using `--quiet`), `test-run` (starts full stack with local images), `test-e2e` (Playwright wiring + protocol matrix tests through Traefik), `test-full` (end-to-end: build → start → test → teardown), `stop` - **`@zitadel/compose` is explicitly excluded from `nx affected` in CI for now** — the full stack smoke test requires a Docker daemon and significant resources. The intent is to add a dedicated `compose_smoke_test` CI job in a follow-up. The targets can be run locally with `pnpm nx run @zitadel/compose:test-full`. ### Documentation - **`compose.mdx`**: Complete rewrite with a staged structure (Stage 1 Quickstart → Stage 2 Homelab → Stage 3 Beyond Compose). Documents TLS modes, profiles, secrets hardening, ExternalDomain/Port/Secure invariant, upgrades, and the path to Kubernetes - **New `requirements.mdx`**: Lists supported PostgreSQL versions (14–18), Redis (standalone), Docker Compose v2.x, and reverse proxy h2c requirements - **`reverse_proxy.mdx`**: Added intro covering h2c requirements, TLS modes table, and Login UI routing split - **`troubleshooting.mdx`**: New sections for container restarts on upgrade, FIRSTINSTANCE env vars not taking effect, and diagnosing unhealthy containers - **`caddy/index.mdx`**: Known issue and workaround for the `TE: trailers` header hang - Removed the old `apps/docs/content/self-hosting/deploy/docker-compose.yaml` embedded in the docs ### Breaking change The old `apps/docs/content/self-hosting/deploy/docker-compose.yaml` file is deleted. The getting-started docs page (`/self-hosting/deploy/compose`) now points to the new pack via a `curl | tar` download command. --- ### Checklist - [x] `deploy/compose/` smoke test passes end-to-end locally (`pnpm nx run @zitadel/compose:test-full`) - [x] Docs build passes (`pnpm nx run @zitadel/docs:build`) - [ ] Follow-up issue created to add `compose_smoke_test` CI job --------- Co-authored-by: Mridang Agarwalla <[email protected]>
This commit is contained in:
co-authored by
Mridang Agarwalla
parent
54095b64b1
commit
bac224c56d
@@ -1,90 +1,201 @@
|
||||
---
|
||||
title: Set up Zitadel with Docker Compose
|
||||
description: "Deploy a ZITADEL instance locally using Docker Compose for rapid testing and development."
|
||||
title: Set up ZITADEL with Docker Compose
|
||||
sidebar_label: Docker Compose
|
||||
description: Deploy ZITADEL with Docker Compose — from a 2-minute localhost quickstart to a hardened homelab setup with TLS, caching, and observability.
|
||||
---
|
||||
|
||||
import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx';
|
||||
import DockerCompose from './docker-compose.yaml';
|
||||
import { DynamicCodeBlock } from 'fumadocs-ui/components/dynamic-codeblock';
|
||||
|
||||
import Disclaimer from './_disclaimer.mdx'
|
||||
import DefaultUser from './_defaultuser.mdx'
|
||||
import Next from './_next.mdx'
|
||||
|
||||
This guide is the entrypoint for running the Zitadel platform locally for the first time.
|
||||
It is for demonstration and development purposes only and does not set up a production-ready and security hardened instance of Zitadel.
|
||||
Once Zitadel up is and running, learn more about a production setup in the [What's Next section](#next-steps).
|
||||
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
|
||||
|
||||
The setup is likely to work with other software versions too, but it is tested against the following environment:
|
||||
- Docker Engine 24+ with the Compose plugin (`docker compose`)
|
||||
- A machine with at least 2 GB RAM
|
||||
|
||||
- Ubuntu 24.04.1
|
||||
- Docker 28.3.2
|
||||
- Docker Compose v2.38.2
|
||||
- curl 8.5.0
|
||||
See [Requirements](/self-hosting/manage/requirements) for supported database, cache, and proxy versions.
|
||||
|
||||
## Docker compose
|
||||
## Stage 1 — Quickstart (2 minutes)
|
||||
|
||||
The following commands set up services for a PostgreSQL database, a Zitadel API and a Zitadel login:
|
||||
Download the two required files, copy the example config, and start:
|
||||
|
||||
1. Download the `docker-compose.yaml` file from the Zitadel repository:
|
||||
```shell
|
||||
curl -L https://raw.githubusercontent.com/zitadel/zitadel/main/apps/docs/content/self-hosting/deploy/docker-compose.yaml -o docker-compose.yaml
|
||||
```
|
||||
2. Make sure the containers use the latest image versions.
|
||||
```shell
|
||||
docker compose pull
|
||||
```
|
||||
3. Run the PostgreSQL database, the Zitadel API, and the Zitadel login.
|
||||
```shell
|
||||
docker compose up --detach --wait
|
||||
```
|
||||
4. Verify the containers are running and healthy.
|
||||
```shell
|
||||
docker compose ps
|
||||
```
|
||||
```shell
|
||||
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](http://localhost:8080) to open the login screen.
|
||||
|
||||
<DefaultUser components={props.components} />
|
||||
|
||||
## What's next
|
||||
<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>
|
||||
|
||||
Before proceeding, review the downloaded `docker-compose.yaml` file:
|
||||
The comments give context to the used variables and show examples for commonly used configuration variants.
|
||||
## Stage 2 — Homelab / Semi-Production
|
||||
|
||||
<details>
|
||||
<summary>docker-compose.yaml</summary>
|
||||
<DynamicCodeBlock lang="yaml" code={DockerCompose} />
|
||||
</details>
|
||||
### Use your own domain
|
||||
|
||||
Here are some natural steps forward to go from the current setup to production:
|
||||
Edit `.env` and set your real domain (see [Custom Domains](/self-hosting/manage/custom-domain) for details):
|
||||
|
||||
- **Use a different master key**: Set up an instance from scratch and use a different master key. For example, generate one by running `tr -dc A-Za-z0-9 </dev/urandom | head -c 32`
|
||||
- **Use files for secrets instead of environment variables, and don't commit them to Git**: Learn how to [configure the platform](/self-hosting/manage/configure)
|
||||
- **Run the containers as non-root users**: Remove the `user: "0"` lines from the `docker-compose.yaml` file.
|
||||
- **Configure a different external domain or IP**: Beware that the login only works with HTTPS on non-localhost domains. [Run and configure a reverse proxy](/self-hosting/manage/reverseproxy/reverse_proxy)
|
||||
- **Serve the API and the UI together on the same port**: [Run and configure a reverse proxy](/self-hosting/manage/reverseproxy/reverse_proxy)
|
||||
- **Encrypt Traffic**: [Run and configure a reverse proxy](/self-hosting/manage/reverseproxy/reverse_proxy)
|
||||
```dotenv
|
||||
ZITADEL_DOMAIN=auth.example.com
|
||||
```
|
||||
|
||||
For more detailed recommendations, read the [Zitadel Production Setup Guide](/self-hosting/manage/production).
|
||||
### Enable TLS
|
||||
|
||||
<details>
|
||||
<summary>Read this if the environment uses the login v1</summary>
|
||||
<p>
|
||||
The login v2 is the next generation of the Zitadel login.
|
||||
Its code and deployment are easily customizable.
|
||||
Unlike the login v1, it runs in its own process.
|
||||
The login v2 is enabled by default in new installations.
|
||||
But if an existing `docker-compose.yaml` file is updated by following this guide,
|
||||
an additional `zitadel-login` service is now running which is actually not doing anything, yet.
|
||||
The following steps give guidance on how to enable the login v2 in an environment.
|
||||
</p>
|
||||
<ol>
|
||||
<li>Read the login v2 related comments in the docker-compose.yaml.</li>
|
||||
<li><a href="../../self-hosting/manage/login-client#create-login-client">Manually create a login client personal access token (PAT) for the now running v2 login</a>.</li>
|
||||
<li>Move the PAT to `./login-client.pat`</li>
|
||||
<li>Restart the login process: `docker compose restart login`</li>
|
||||
<li><a href="../../self-hosting/manage/login-client#require-login-v2">Enable the Login UI for all users</a></li>
|
||||
</ol>
|
||||
</details>
|
||||
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:
|
||||
|
||||
```shell
|
||||
# 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
|
||||
|
||||
<Callout type="warn">
|
||||
The masterkey [encrypts sensitive data at rest](/concepts/architecture/secrets). 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.
|
||||
</Callout>
|
||||
|
||||
```shell
|
||||
# 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
|
||||
```
|
||||
|
||||
### External URL settings
|
||||
|
||||
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](/self-hosting/manage/tls_modes) for details.
|
||||
|
||||
<NoteInstanceNotFound components={props.components} />
|
||||
|
||||
### Enable caching with Redis
|
||||
|
||||
See [Cache Configuration](/self-hosting/manage/cache) for all available options. Add these to `.env`:
|
||||
|
||||
```dotenv
|
||||
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:
|
||||
|
||||
```shell
|
||||
docker compose --env-file .env -f docker-compose.yml --profile cache up -d --wait
|
||||
```
|
||||
|
||||
## Stage 3 — Beyond Compose
|
||||
|
||||
### Production-like init/setup/start split
|
||||
|
||||
For controlled upgrades, separate [database initialization from the running API](/self-hosting/manage/updating_scaling):
|
||||
|
||||
```shell
|
||||
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
|
||||
|
||||
Set in `.env`:
|
||||
|
||||
```dotenv
|
||||
ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_TYPE=grpc
|
||||
```
|
||||
|
||||
Download the collector configuration and start with the `observability` profile:
|
||||
|
||||
```shell
|
||||
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](/self-hosting/manage/metrics/overview) for Prometheus scraping and available metric types.
|
||||
|
||||
### Scale API replicas
|
||||
|
||||
<Callout type="info">
|
||||
Scaling requires the **prodlike overlay** so that migrations run once in `zitadel-init` instead of on every replica.
|
||||
</Callout>
|
||||
|
||||
```shell
|
||||
docker compose --env-file .env \
|
||||
-f docker-compose.yml \
|
||||
-f docker-compose.prodlike.yml \
|
||||
up -d --scale zitadel-api=3
|
||||
```
|
||||
|
||||
### Updating ZITADEL
|
||||
|
||||
Edit `ZITADEL_VERSION` in `.env`, then:
|
||||
|
||||
```shell
|
||||
docker compose --env-file .env -f docker-compose.yml pull
|
||||
docker compose --env-file .env -f docker-compose.yml up -d --wait
|
||||
```
|
||||
|
||||
<Callout>
|
||||
`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.
|
||||
</Callout>
|
||||
|
||||
### Moving to Kubernetes
|
||||
|
||||
Docker Compose is ideal for getting started and homelab deployments.
|
||||
For production workloads, review the [Production Checklist](/self-hosting/manage/productionchecklist) and deploy with the official [Helm chart for Kubernetes](/self-hosting/deploy/kubernetes).
|
||||
|
||||
The compose pack and the Helm chart share the same application configuration model (`ZITADEL_*` environment variables), so migration is straightforward.
|
||||
|
||||
<Next components={props.components} />
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
services:
|
||||
zitadel:
|
||||
restart: unless-stopped
|
||||
image: ghcr.io/zitadel/zitadel:latest
|
||||
command: start-from-init --masterkey "MasterkeyNeedsToHave32Characters"
|
||||
environment:
|
||||
# See "What's next" to learn about how to serve Zitadel on a different domain or IP.
|
||||
ZITADEL_EXTERNALDOMAIN: localhost
|
||||
|
||||
# See "What's next" to learn about how to enable TLS.
|
||||
ZITADEL_EXTERNALSECURE: false
|
||||
ZITADEL_TLS_ENABLED: false
|
||||
|
||||
# Database connection settings.
|
||||
ZITADEL_DATABASE_POSTGRES_HOST: db
|
||||
ZITADEL_DATABASE_POSTGRES_PORT: 5432
|
||||
# The database is created by the init job if it does not exist.
|
||||
ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel
|
||||
# The admin user must already exist in the database.
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
|
||||
ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
|
||||
# The zitadel user is created by the init job if it does not exist.
|
||||
ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel
|
||||
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel
|
||||
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable
|
||||
|
||||
# By configuring a login application, the setup job creates a user of type machine with the role IAM_LOGIN_CLIENT.
|
||||
# It writes a PAT to the path specified in ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH.
|
||||
# The PAT is passed to the login container via the environment variable ZITADEL_SERVICE_USER_TOKEN_FILE.
|
||||
ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /current-dir/login-client.pat
|
||||
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: false
|
||||
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client
|
||||
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Automatically Initialized IAM_LOGIN_CLIENT
|
||||
ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: '2029-01-01T00:00:00Z'
|
||||
# Activate the login v2 on an installation from scratch.
|
||||
# To activate the login v2 on an existing installation, read the "What's next" section.
|
||||
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: true # To use the login v1, set this to false.
|
||||
ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: http://localhost:3000/ui/v2/login/
|
||||
# Configure the redirection paths to the login v2.
|
||||
ZITADEL_OIDC_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?authRequest=
|
||||
ZITADEL_OIDC_DEFAULTLOGOUTURLV2: http://localhost:3000/ui/v2/login/logout?post_logout_redirect=
|
||||
ZITADEL_SAML_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?samlRequest=
|
||||
|
||||
# By configuring a machine, the setup job creates a user of type machine with the role IAM_OWNER.
|
||||
# It writes a personal access token (PAT) to the path specified in ZITADEL_FIRSTINSTANCE_PATPATH.
|
||||
# The PAT can be used to provision resources with [Terraform](/guides/manage/terraform-provider), for example.
|
||||
# ZITADEL_FIRSTINSTANCE_PATPATH: /current-dir/admin.pat
|
||||
# ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: admin
|
||||
# ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Automatically Initialized IAM_OWNER
|
||||
# ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE: '2029-01-01T00:00:00Z'
|
||||
|
||||
# To change the initial human admin users username and password, uncomment the following lines.
|
||||
# The first login name is formatted like this: <username>@<org_name>.<external_domain>
|
||||
# With the following incommented configuration, this would be [email protected]
|
||||
# Visit http://localhost:8080/ui/console to check if the login name works.
|
||||
# If you can't log in, check the available login names:
|
||||
# echo "select * from projections.login_names3;" | psql -h localhost -U postgres -d zitadel
|
||||
# The postgres users password is postgres.
|
||||
# ZITADEL_FIRSTINSTANCE_ORG_NAME: My Organization
|
||||
# ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: root
|
||||
# ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: RootPassword1!
|
||||
|
||||
# Enable debug logs
|
||||
# ZITADEL_LOG_LEVEL: debug
|
||||
# Write Access Logs to stdout.
|
||||
# ZITADEL_LOGSTORE_ACCESS_STDOUT_ENABLED: true
|
||||
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- /app/zitadel
|
||||
- ready
|
||||
interval: 10s
|
||||
timeout: 60s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
user: "0"
|
||||
volumes:
|
||||
- .:/current-dir:delegated
|
||||
ports:
|
||||
- 8080:8080
|
||||
- 3000:3000
|
||||
networks:
|
||||
- zitadel
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
login:
|
||||
restart: unless-stopped
|
||||
image: ghcr.io/zitadel/zitadel-login:latest
|
||||
# If you can't use the network_mode service:zitadel, you can pass the environment variables ZITADEL_API_URL=http://zitadel:8080 and CUSTOM_REQUEST_HEADERS=Host:localhost instead.
|
||||
environment:
|
||||
- ZITADEL_API_URL=http://localhost:8080
|
||||
- NEXT_PUBLIC_BASE_PATH=/ui/v2/login
|
||||
- ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client.pat
|
||||
network_mode: service:zitadel
|
||||
user: "0"
|
||||
volumes:
|
||||
- .:/current-dir:ro
|
||||
depends_on:
|
||||
zitadel:
|
||||
condition: service_healthy
|
||||
restart: false
|
||||
|
||||
db:
|
||||
restart: unless-stopped
|
||||
image: postgres:17
|
||||
environment:
|
||||
PGUSER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- pg_isready
|
||||
- -d
|
||||
- zitadel
|
||||
- -U
|
||||
- postgres
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
networks:
|
||||
- zitadel
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- 'data:/var/lib/postgresql/data:rw'
|
||||
|
||||
networks:
|
||||
zitadel:
|
||||
|
||||
volumes:
|
||||
data:
|
||||
@@ -14,3 +14,38 @@ import UpstreamHeader from "../../../self-hosting/deploy/troubleshooting/_upstre
|
||||
## upstream sent too big header while reading response header from upstream
|
||||
|
||||
<UpstreamHeader components={props.components} />
|
||||
|
||||
## Container restarts after upgrading to a new version
|
||||
|
||||
When you upgrade ZITADEL to a newer version, the setup phase runs database migrations automatically.
|
||||
|
||||
**Which command to use for upgrades:**
|
||||
|
||||
- `start-from-init` (used in the quickstart) runs **init → setup → start** in sequence. The init phase is only needed once ever (for a fresh database), but it is safe to re-run because it skips resources that already exist. Use `start-from-init` only for the quickstart or when you're sure the database needs initialising from scratch.
|
||||
- **For upgrades on existing installations**, prefer `start-from-setup` (runs setup → start) or run `zitadel setup` followed by `zitadel start` as separate steps. This avoids running the init phase unnecessarily and is the recommended production approach. See [Update and Scale ZITADEL](/self-hosting/manage/updating_scaling) for details.
|
||||
|
||||
If the container exits with code 1 and the logs show a database migration error, check the following:
|
||||
|
||||
- Make sure the PostgreSQL version meets the [requirements](/self-hosting/manage/database). PostgreSQL 18 support requires ZITADEL v4.11.0 or later.
|
||||
- Log messages like `role "zitadel" already exists` or `database "zitadel" already exists` during `start-from-init` re-runs are **expected and harmless** — the init phase skips those steps.
|
||||
- If the container still fails after a version upgrade, check whether the version you are upgrading to lists any known migration issues in the [release notes](https://github.com/zitadel/zitadel/releases).
|
||||
|
||||
## Instance settings (FIRSTINSTANCE / DEFAULTINSTANCE) not taking effect after restart
|
||||
|
||||
Environment variables prefixed with `ZITADEL_FIRSTINSTANCE_` and `ZITADEL_DEFAULTINSTANCE_` are only applied during the **initial creation** of a ZITADEL instance.
|
||||
Changing these variables and restarting the container will not update an already-existing instance.
|
||||
|
||||
To change instance-level settings on a running installation:
|
||||
- Use the [Admin Console](https://your-domain/ui/console/instance) to update settings interactively.
|
||||
- Use the Admin API to automate changes.
|
||||
|
||||
**Login V2**: Starting with ZITADEL v4, the Login V2 UI is enabled by default for new installations.
|
||||
If you are running an existing installation and want to enable it, follow the [Login Client guide](/self-hosting/manage/login-client).
|
||||
|
||||
## ZITADEL service becomes unhealthy without visible error messages
|
||||
|
||||
If the `zitadel` container shows as `unhealthy` but the log output does not reveal an obvious error:
|
||||
|
||||
1. Increase log verbosity by setting `ZITADEL_LOG_LEVEL: debug` and `ZITADEL_LOGSTORE_ACCESS_STDOUT_ENABLED: true`.
|
||||
2. Check that the healthcheck command can reach ZITADEL. The built-in `zitadel ready` command uses HTTP when `ZITADEL_TLS_ENABLED=false` and HTTPS otherwise. Ensure that this variable matches your TLS settings.
|
||||
3. If ZITADEL is running behind a reverse proxy, make sure the proxy forwards traffic correctly before the first healthcheck succeeds. See [Run and configure a reverse proxy](/self-hosting/manage/reverseproxy/reverse_proxy).
|
||||
|
||||
@@ -21,14 +21,15 @@ services:
|
||||
environment:
|
||||
- POSTGRES_USER=root
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=zitadel
|
||||
networks:
|
||||
- 'zitadel'
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"]
|
||||
test: ["CMD-SHELL", "pg_isready -d zitadel -U root"]
|
||||
interval: 10s
|
||||
timeout: 60s
|
||||
timeout: 30s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
start_period: 20s
|
||||
volumes:
|
||||
- 'data:/var/lib/postgresql/data:rw'
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: ZITADEL Requirements
|
||||
sidebar_label: Requirements
|
||||
---
|
||||
|
||||
This page lists the infrastructure versions that ZITADEL is tested against and officially supports.
|
||||
|
||||
## Database
|
||||
|
||||
ZITADEL requires a PostgreSQL database. Versions **14 to 18** are supported.
|
||||
|
||||
See the [Database](/self-hosting/manage/database) page for full configuration options, how to pre-create users and databases, and how to rotate credentials.
|
||||
|
||||
## Cache (optional)
|
||||
|
||||
ZITADEL supports Redis as an optional cache to speed up lookups of frequently needed objects.
|
||||
Redis must run in **standalone mode** (Cluster and Sentinel are not yet supported).
|
||||
|
||||
See the [Caches](/self-hosting/manage/cache) page for configuration options.
|
||||
|
||||
## Container runtime
|
||||
|
||||
- **Docker Compose**: v2.x (Compose V1 `docker-compose` CLI is not supported)
|
||||
- **Kubernetes**: see the [Kubernetes deployment guide](/self-hosting/deploy/kubernetes)
|
||||
|
||||
## Reverse proxy
|
||||
|
||||
Any reverse proxy that supports HTTP/2 upstream connections (h2c or h2) works with ZITADEL.
|
||||
The following proxies are tested and have dedicated configuration guides:
|
||||
|
||||
- **Traefik**: v3.x
|
||||
- **NGINX**: v1.x stable releases
|
||||
- **Caddy**: v2.x
|
||||
- **Apache httpd**: 2.4.x
|
||||
|
||||
See the [Reverse Proxy Configuration](/self-hosting/manage/reverseproxy/reverse_proxy) page for details and Docker Compose examples.
|
||||
@@ -28,6 +28,21 @@ You can either setup your environment for <a href={'#tls-mode-external'}>TLS mod
|
||||
|
||||
<ProxyGuideTLSMode components={props.components} mode="enabled" configfilename="enabled-tls.Caddyfile" configfilecontent={ConfigEnabled} providername={providername} link={link} lower={lower}></ProxyGuideTLSMode>
|
||||
|
||||
## Known issues
|
||||
|
||||
### TE: trailers header causes requests to hang
|
||||
|
||||
When a browser sends a `TE: trailers` HTTP header (common with HTTP/2), Caddy may forward it upstream where it conflicts with ZITADEL's gRPC-gateway handling.
|
||||
Requests that result in an error response will hang indefinitely instead of returning the error to the client.
|
||||
|
||||
**Workaround**: strip the header in Caddy before it reaches ZITADEL:
|
||||
|
||||
```
|
||||
reverse_proxy h2c://zitadel:8080 {
|
||||
header_up -TE
|
||||
}
|
||||
```
|
||||
|
||||
## More Information
|
||||
|
||||
<ProxyGuideMore components={props.components} ></ProxyGuideMore>
|
||||
|
||||
@@ -3,6 +3,36 @@ title: Reverse Proxy Configuration
|
||||
description: "Setup and configure reverse proxies like Traefik, NGINX, and Caddy to manage external traffic for self-hosted ZITADEL."
|
||||
---
|
||||
|
||||
ZITADEL exposes a single HTTP/2 port (default `8080`) that serves both gRPC and HTTP APIs, as well as the management console.
|
||||
Running a reverse proxy in front of ZITADEL is the recommended approach for production deployments because it:
|
||||
|
||||
- Terminates TLS and provides HTTPS to clients
|
||||
- Exposes standard ports (`80`/`443`) while keeping ZITADEL on an internal port
|
||||
- Handles certificate renewal (e.g. via Let's Encrypt)
|
||||
- Enables path-based routing between the ZITADEL API and the Login UI
|
||||
|
||||
## HTTP/2 and h2c requirements
|
||||
|
||||
ZITADEL relies on HTTP/2 for all gRPC communication.
|
||||
When the reverse proxy terminates TLS, it must forward traffic to ZITADEL using **h2c** (unencrypted HTTP/2), not HTTP/1.1.
|
||||
Not all reverse proxies support h2c by default — check the proxy-specific guides below for configuration examples.
|
||||
|
||||
For more details, see [HTTP/2 Support in ZITADEL](/self-hosting/manage/http2).
|
||||
|
||||
## TLS modes
|
||||
|
||||
ZITADEL supports three TLS modes that affect how the reverse proxy should be configured:
|
||||
|
||||
| Mode | Description | Typical use |
|
||||
|------|-------------|-------------|
|
||||
| `disabled` | ZITADEL listens on plain HTTP, reverse proxy handles nothing special | Local development, h2c passthrough |
|
||||
| `external` | ZITADEL listens on plain HTTP; reverse proxy terminates TLS | Most production setups |
|
||||
| `enabled` | ZITADEL itself terminates TLS (end-to-end encryption) | Environments requiring mutual TLS |
|
||||
|
||||
For more details, see [TLS Modes](/self-hosting/manage/tls_modes).
|
||||
|
||||
## Proxy guides
|
||||
|
||||
Check out one of the following guides to configure your favorite reverse proxy:
|
||||
|
||||
- [Traefik](/self-hosting/manage/reverseproxy/traefik)
|
||||
@@ -12,3 +42,14 @@ Check out one of the following guides to configure your favorite reverse proxy:
|
||||
- [Cloudflare](/self-hosting/manage/reverseproxy/cloudflare)
|
||||
- [Cloudflare Tunnel](/self-hosting/manage/reverseproxy/cloudflare_tunnel)
|
||||
- [Fronting Zitadel Cloud](/self-hosting/manage/reverseproxy/zitadel_cloud)
|
||||
|
||||
## Login UI routing
|
||||
|
||||
When running both the ZITADEL API and the Login V2 UI (`zitadel-login`) behind a reverse proxy, the proxy must route traffic to the correct upstream:
|
||||
|
||||
- `/ui/v2/login` → Login UI container (port `3000` by default)
|
||||
- All other paths → ZITADEL API container (port `8080` by default)
|
||||
|
||||
The proxy-specific guides include Docker Compose examples that demonstrate this split routing.
|
||||
|
||||
For tested proxy versions, see [ZITADEL Requirements](/self-hosting/manage/requirements#reverse-proxy).
|
||||
|
||||
@@ -773,6 +773,7 @@ export const guidesSidebar: readonly SidebarItem[] = [
|
||||
label: "Production & Operations",
|
||||
collapsed: false,
|
||||
items: [
|
||||
"self-hosting/manage/requirements",
|
||||
"self-hosting/manage/production",
|
||||
"self-hosting/manage/productionchecklist",
|
||||
"self-hosting/manage/usage_control",
|
||||
|
||||
Reference in New Issue
Block a user