Skip to Content

Production Guide: Deploy ZITADEL with Docker Compose + Traefik + PostgreSQL on Ubuntu

A production-focused IAM deployment with secure secrets handling, TLS routing, backup strategy, and operational troubleshooting.

Identity and access management is often the first service that breaks during rapid growth: ad-hoc OAuth clients, brittle SSO integrations, and no clear audit trail for admin changes. If your team runs multiple internal apps and customer-facing services, you need an IAM control plane that supports OpenID Connect, SAML, delegated administration, and production-grade operations.

This guide shows how to deploy ZITADEL on Ubuntu with Docker Compose + Traefik + PostgreSQL in a way that is repeatable and supportable in production. We focus on practical operations: strict secret handling, idempotent configuration, health checks, TLS termination, backup strategy, and day-2 troubleshooting. The goal is to give platform teams a deployment they can trust, not just a quick demo.

Architecture and flow overview

In this pattern, PostgreSQL stores IAM state, ZITADEL serves the identity APIs and console, and Traefik terminates TLS and routes external traffic to the ZITADEL container. A dedicated Docker network isolates east-west traffic, and only Traefik is exposed publicly. Backups capture PostgreSQL dumps plus encrypted configuration snapshots for fast recovery.

  • Edge: Traefik with automatic HTTPS and secure headers
  • Core service: ZITADEL API and management console
  • Database: PostgreSQL 16 with persistent volume
  • Secrets: Stored in .env with strict file permissions and rotation plan
  • Operations: Health checks, log inspection, smoke tests, and restore drills

For high availability, you can later move PostgreSQL to a managed or clustered backend and run multiple ZITADEL replicas behind Traefik. Start with this single-node baseline to stabilize workflows before scaling out.

Prerequisites

  • Ubuntu 22.04/24.04 host with sudo access
  • A DNS record (for example auth.example.com) pointing to your server
  • Docker Engine 24+ and Compose plugin
  • Ports 80/443 allowed from the internet, port 5432 blocked publicly
  • A mailbox for admin notifications and reset flows

Before deployment, verify server time synchronization (NTP) and baseline hardening (SSH key auth, fail2ban, and a host firewall). IAM services are security-critical and should never be deployed on an unmaintained host.

Step-by-step deployment

1) Install Docker runtime and create directories

Create a predictable layout so operations and handoffs stay clean. We keep runtime definitions in /opt/zitadel and persistence in /srv/zitadel.

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

sudo mkdir -p /opt/zitadel /srv/zitadel/postgres /srv/zitadel/backups
sudo chown -R $USER:$USER /opt/zitadel /srv/zitadel

If the copy button does not work in your browser, manually select the code block and copy with Ctrl/Cmd+C.

2) Create environment secrets and lock permissions

ZITADEL and PostgreSQL require strong credentials. Use long random values and keep .env readable only by the deploy user.

cd /opt/zitadel
cat > .env << 'EOF'
DOMAIN=auth.example.com
TZ=UTC

POSTGRES_DB=zitadel
POSTGRES_USER=zitadel
POSTGRES_PASSWORD=REPLACE_WITH_A_32_CHAR_RANDOM_SECRET

ZITADEL_MASTERKEY=REPLACE_WITH_32_BYTES_HEX_OR_BASE64
ZITADEL_FIRSTINSTANCE_ORG=Example Corp
ZITADEL_FIRSTINSTANCE_HUMAN_USERNAME=admin
[email protected]
ZITADEL_FIRSTINSTANCE_HUMAN_PASSWORD=REPLACE_WITH_STRONG_PASSWORD
EOF

chmod 600 .env

If the copy button does not work in your browser, manually select the code block and copy with Ctrl/Cmd+C.

Secret handling guidance: never commit this file to Git, rotate credentials after team changes, and move to a secret manager (Vault/1Password/SSM) once the environment is stable.

3) Write Docker Compose stack

The Compose file runs PostgreSQL with a health check, then starts ZITADEL once the database is ready. Traefik labels expose the service on your DNS hostname with TLS enabled.

version: "3.9"

services:
  postgres:
    image: postgres:16
    container_name: zitadel-postgres
    env_file: .env
    environment:
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    volumes:
      - /srv/zitadel/postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 10
    restart: unless-stopped
    networks: [identity]

  zitadel:
    image: ghcr.io/zitadel/zitadel:latest
    container_name: zitadel
    env_file: .env
    command: start-from-init --masterkey "${ZITADEL_MASTERKEY}" --tlsMode disabled
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      - ZITADEL_DATABASE_POSTGRES_HOST=postgres
      - ZITADEL_DATABASE_POSTGRES_PORT=5432
      - ZITADEL_DATABASE_POSTGRES_DATABASE=${POSTGRES_DB}
      - ZITADEL_DATABASE_POSTGRES_USER_USERNAME=${POSTGRES_USER}
      - ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=${POSTGRES_PASSWORD}
      - ZITADEL_EXTERNALDOMAIN=${DOMAIN}
      - ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME=${ZITADEL_FIRSTINSTANCE_HUMAN_USERNAME}
      - ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_ADDRESS=${ZITADEL_FIRSTINSTANCE_HUMAN_EMAIL}
      - ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD=${ZITADEL_FIRSTINSTANCE_HUMAN_PASSWORD}
      - ZITADEL_FIRSTINSTANCE_ORG_NAME=${ZITADEL_FIRSTINSTANCE_ORG}
    labels:
      - traefik.enable=true
      - traefik.http.routers.zitadel.rule=Host(`${DOMAIN}`)
      - traefik.http.routers.zitadel.entrypoints=websecure
      - traefik.http.routers.zitadel.tls.certresolver=letsencrypt
      - traefik.http.services.zitadel.loadbalancer.server.port=8080
    restart: unless-stopped
    networks: [identity, edge]

networks:
  identity:
    name: identity
  edge:
    external: true

If the copy button does not work in your browser, manually select the code block and copy with Ctrl/Cmd+C.

4) Attach to Traefik edge network and deploy

If your host already runs Traefik, ensure the external network exists and then bring up the stack.

docker network create edge || true
cd /opt/zitadel
docker compose pull
docker compose up -d

If the copy button does not work in your browser, manually select the code block and copy with Ctrl/Cmd+C.

5) Confirm bootstrap and create first project/client

After containers are healthy, sign in to the admin console and create your first project and OIDC application for an internal service. Keep redirect URIs exact and minimal.

docker compose ps
docker compose logs --tail=100 zitadel
curl -I https://auth.example.com

If the copy button does not work in your browser, manually select the code block and copy with Ctrl/Cmd+C.

Configuration and secrets handling

Production IAM reliability depends less on first deployment and more on disciplined configuration management. Keep a versioned operations repository with sanitized templates (.env.example, compose files, and runbooks) while storing real secrets outside Git.

  • Use at least 32-character random secrets for DB and admin credentials
  • Rotate secrets quarterly or after personnel transitions
  • Restrict shell history capture when handling sensitive values
  • Enable MFA for all administrative ZITADEL accounts
  • Pin images to tested tags during change windows, then upgrade intentionally

For teams with compliance requirements, add an encrypted backup destination, immutable backup retention, and audit-friendly change approvals for identity configuration updates.

Automated backups (PostgreSQL dump)

The following script keeps timestamped encrypted dumps in /srv/zitadel/backups. Schedule it with cron or systemd timers and replicate off-host.

#!/usr/bin/env bash
set -euo pipefail
source /opt/zitadel/.env

TS=$(date +%Y%m%d_%H%M%S)
OUT="/srv/zitadel/backups/zitadel_${TS}.sql"

docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" zitadel-postgres \
  pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > "$OUT"

gzip "$OUT"
find /srv/zitadel/backups -name 'zitadel_*.sql.gz' -mtime +14 -delete

If the copy button does not work in your browser, manually select the code block and copy with Ctrl/Cmd+C.

Verification checklist

Use a repeatable checklist after every deployment and every significant upgrade:

  • Routing: https://auth.example.com returns expected login page over valid TLS
  • Container health: PostgreSQL healthy, ZITADEL running without restart loops
  • OIDC flow: test login from one real client and verify callback completes
  • Audit: admin actions are visible in logs/events
  • Backup integrity: latest dump exists and can be decompressed
cd /opt/zitadel
docker compose ps

docker compose exec postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "select now();"

docker compose logs --tail=200 zitadel | grep -Ei "error|warn|migration|ready" || true

ls -lh /srv/zitadel/backups | tail -n 5

If the copy button does not work in your browser, manually select the code block and copy with Ctrl/Cmd+C.

Common issues and fixes

Traefik route returns 404

Usually caused by hostname mismatch between DNS record, Traefik rule, and DOMAIN value. Confirm all three match exactly and reload the stack.

Login page loads but authentication fails

Check system clock skew and redirect URI configuration in your OIDC client. Time drift can break token validation silently.

Database connection refused on startup

PostgreSQL may still be initializing. Ensure health checks are configured and depends_on.condition=service_healthy is in place. Also verify the DB credentials in .env were not changed without recreating containers.

Certificate issuance fails

Verify ports 80/443 are reachable publicly, DNS is propagated, and Traefik certificate resolver is configured correctly. Review Traefik logs for ACME rate-limit or challenge errors.

Unexpected latency under load

Start by checking host CPU steal, disk I/O wait, and database query timing. Identity workloads often degrade when storage is saturated. Move database volume to faster disks and tune PostgreSQL memory parameters.

FAQ

Can I run ZITADEL without Traefik?

Yes, but in production you still need a robust TLS ingress layer and predictable routing. Traefik is convenient for certificate automation; NGINX or HAProxy are also valid if managed consistently.

Should PostgreSQL run in the same host?

For small teams and early production, yes. As traffic and security requirements grow, move PostgreSQL to a managed or clustered service and apply network segmentation and backup policies accordingly.

How do I rotate the ZITADEL master key safely?

Treat master key rotation as a planned maintenance operation with tested rollback. Snapshot database and configuration first, validate in staging, then promote to production with change approval.

What is the minimum backup policy?

At least daily encrypted dumps, 14–30 days retention, and one off-site copy. More importantly, test restore quarterly so backups are proven, not assumed.

Can I integrate external identity providers?

Yes. ZITADEL supports federation patterns including social and enterprise providers. Start with one provider, validate claim mapping and role assignment, then scale integration incrementally.

How do I harden admin access?

Enforce MFA for administrators, limit allowed IPs at the edge where possible, and separate break-glass accounts from normal operators. Review admin audit events regularly.

How do I upgrade with low risk?

Pin a tested image tag, stage upgrades in a pre-production environment, run smoke tests, then schedule production rollout with backup and rollback checkpoints.

Related guides

Talk to us

If you need help designing resilient identity architecture, integrating SSO across multiple apps, or planning a secure migration from legacy IAM, our team can help.

Contact Us

Production MinIO on Docker Compose + systemd: A Practical S3 Deployment Guide
Deploy and harden self-hosted MinIO with repeatable operations, verification, and recovery workflows.