A production-ready Docker Compose setup to run ZITADEL on your own domain, with:
- PostgreSQL 17 as the backend database
- Traefik as reverse proxy with automatic HTTPS via ACME
- ZITADEL Login v2 served from a dedicated container and path (
/ui/v2/login
) - Clear separation between API traffic and Login traffic
✨ This repo gives you a full working compose that you can drop into a server with Docker + Traefik and be up quickly. You only need to change a handful of values (domain, secrets, passwords).
-
A working domain pointing to your server’s public IP. You can use a subdomain (e.g.,
auth.yourdomain.com
). -
Docker Engine and docker-compose installed.
-
A Traefik instance already running on the same Docker host, with:
- An external Docker network that Traefik and ZITADEL will share (e.g.,
traefik-network
). - A certresolver configured (e.g.,
resolver
) for Let’s Encrypt or your chosen CA.
- An external Docker network that Traefik and ZITADEL will share (e.g.,
The compose below expects an external Docker network named
traefik-network
. If you use a different name, update it accordingly.
Internet → Traefik (TLS termination)
├─ route /ui/v2/login → zitadel-login (Next.js app, port 3000)
└─ route everything else → zitadel (HTTP/2 cleartext on 8080)
zitadel ↔ postgres (17-alpine)
- TLS is terminated at Traefik; ZITADEL runs in
--tlsMode external
and speaks h2c (HTTP/2 cleartext) internally. - Login v2 is a separate container that consumes a Service User token (PAT) written during first-run bootstrap.
-
Clone this repository and cd into it.
-
Edit environment placeholders in the compose:
- Domain everywhere (
your.domain.com
) and external domain variable value. - Strong secrets: Master key, DB passwords, initial admin password, etc.
- Domain everywhere (
-
Ensure Traefik is up and exposing the expected entrypoints (
web
,websecure
) and has a certresolver (e.g.,resolver
). -
Create the shared Docker network if it doesn’t exist yet:
docker network create traefik-network
-
Launch ZITADEL stack:
docker compose up -d
-
Watch logs until healthy:
docker compose logs -f zitadel docker compose logs -f login
-
Visit
https://your.domain.com
and log in with the first admin credentials you configured.
Note: Compose will create the
pgdata_docs
volume automatically on first run. You can also predeclare it undervolumes:
at top-level if you prefer.
- Domain consistency: Use the same domain in all places (
ZITADEL_EXTERNALDOMAIN
, Traefik router rules, Login envs, and the*_DEFAULTLOGINURLV2
/*_DEFAULTLOGOUTURLV2
). - Master key:
--masterkey
must be exactly 32 characters. - Strong passwords: Replace all placeholder passwords with strong secrets.
- Traefik certresolver: Ensure the label value matches your
traefik.yml
configuration (e.g.,resolver
). - Shared network: The compose must join the exact external Traefik network name.
- Create an A/AAAA record for
your.domain.com
to your server. - Traefik terminates TLS, obtains/renews certificates, and forwards h2c to ZITADEL on port 8080.
- The compose defines an HTTP router for redirect and a secure router for the API. The Login router has a higher priority (30) to win for
/ui/v2/login
.
If your Traefik version does not support rule negation (
!PathPrefix(...)
), the priority alone will still route/ui/v2/login
to the login service.
- Postgres data: stored in the
pgdata_docs
Docker volume. - Service token (PAT): written to
./login-client.pat
on the host by the ZITADEL container at first init. Keep this safe.
Backup tips:
# Stop app traffic briefly (optional) and dump DB
PGPASSWORD=yourpostgrespassword \
pg_dump -h 127.0.0.1 -U postgres -d zitadel_docs > zitadel_backup_$(date +%F).sql
# Or snapshot the named Docker volume using your preferred tooling
- ZITADEL: update the image tag (ideally pin by digest), read the release notes, and
docker compose pull && docker compose up -d
. - Login: update
ghcr.io/zitadel/zitadel-login:latest
or pin to a specific version. - Postgres: upgrade only after confirming compatibility and having a valid backup.
- Prefer Docker secrets or an
.env
file with tight permissions for sensitive values (master key, passwords). Avoid committing secrets. - Restrict access to the host directory that contains
login-client.pat
. - Consider adding basic resource limits (CPU/memory) per service.
- 404 on /ui/v2/login: Check router priority and that the login container is healthy and on the
traefik-network
. - TLS not issued: Verify your DNS, Traefik entrypoints (
web
/websecure
), and that thecertresolver
name matches your Traefik config. - Login PAT missing: Ensure the
ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH
path is bind-mounted and writable by the ZITADEL container. - 403/redirect loops: Re-check domain consistency across
ZITADEL_EXTERNALDOMAIN
and all*_DEFAULT*URLV2
variables.
docker compose down
# Remove persistent data (⚠️ irreversible)
docker volume rm <your_project>_pgdata_docs
rm -f ./login-client.pat
MIT
Made with ❤️ by AXCODE