ZITADEL Docs
Deploy & OperateSelf-Hosted

Deploy ZITADEL on Kubernetes

This guide takes you from zero to a running ZITADEL instance on Kubernetes and then shows you how to harden it for production.

Stage 1 — Quickstart

The ZITADEL chart ships with an optional PostgreSQL subchart so a single helm install deploys the database alongside ZITADEL. You still need a routing controller running in your cluster to expose the service.

Prerequisites

  • A Kubernetes cluster (1.30+)
  • An Ingress controller (e.g., Traefik, NGINX) OR a Gateway API controller
  • kubectl
  • Helm 3.x or 4.x

No cluster yet? k3d is a quick way to spin one up locally — it runs k3s in Docker, with Traefik as the built-in ingress controller:

k3d cluster create zitadel --port "80:80@loadbalancer"

Install the full stack

mkdir zitadel-helm && cd zitadel-helm &&
curl -fsSLO https://raw.githubusercontent.com/zitadel/zitadel-charts/main/examples/0-quickstart/quickstart-values.yaml

Edit quickstart-values.yaml before installing. For a local k3d or k3s cluster, these standard Ingress values usually work as-is:

zitadel:
  configmapConfig:
    ExternalDomain: localhost
    ExternalPort: 80

ingress:
  className: traefik

login:
  ingress:
    className: traefik

If you are using a different ingress controller, replace both className values with that controller's IngressClass name.

helm repo add zitadel https://charts.zitadel.com &&
helm repo update &&
helm upgrade --install zitadel zitadel/zitadel --values quickstart-values.yaml --wait

That's it. Visit http://localhost/ui/console?login_hint=zitadel-admin@zitadel.localhost and log in with password Password1!.

The masterkey encrypts sensitive data at rest. The quickstart-values.yaml file contains a dev placeholder masterkey — generate a real one before any non-local deployment:

tr -dc A-Za-z0-9 </dev/urandom | head -c 32

Once ZITADEL has been initialized with a masterkey, it cannot be changed without losing access to encrypted data.

The stack runs: your ingress/gateway controller → ZITADEL API (Go) + ZITADEL Login (Next.js) → PostgreSQL (bundled). See HTTP/2 for requirements on how traffic must be forwarded to ZITADEL pods via h2c.

Swap out components

The bundled PostgreSQL subchart is for quickstart use only. You can replace it independently:

ComponentHow to replace
DatabaseSet postgresql.enabled: false and configure ZITADEL_DATABASE_POSTGRES_DSN pointing to your own PostgreSQL. Use the postgres maintenance database so ZITADEL can create its own database during initialization.
Routing / IngressUpdate ingress.className to match your controller, or disable the Helm ingress (ingress.enabled: false) to manage your own Gateway API HTTPRoute resources.
CachingAdd Redis by configuring zitadel.configmapConfig.Caches (see Caching).

Stage 2 — Production Cluster

For production you need:

  • A Kubernetes cluster (1.30+)
  • A routing solution: Ingress controller (Traefik, NGINX) or a Gateway API implementation
  • A domain with DNS configured
  • TLS certificates (via cert-manager, ACME, or manually)
  • PostgreSQL 14+ (managed service or in-cluster)
  • Helm 3.x+

PostgreSQL compatibility: ZITADEL supports PostgreSQL 14–18. When using PostgreSQL 18, ensure you run ZITADEL v4.11.0 or newer. For the most up-to-date compatibility matrix and configuration details, see Database requirements.

The ZITADEL management console requires end-to-end HTTP/2 support. Ensure your routing controller is configured to forward HTTP/2 (h2c) traffic to ZITADEL pods.

Create secrets

# Masterkey — generate once, store safely, cannot be changed after first run
kubectl create secret generic zitadel-masterkey \
  --from-literal=masterkey="$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)"

# Database credentials — store the full DSN
kubectl create secret generic zitadel-db-credentials \
  --from-literal=dsn="postgresql://zitadel:your-secure-password@postgres.database.svc.cluster.local:5432/postgres?sslmode=verify-full"

Configure values

Save the base configuration below as values.yaml. Replace zitadel.example.com with your domain:

replicaCount: 2

zitadel:
  masterkeySecretName: zitadel-masterkey
  env:
    - name: ZITADEL_DATABASE_POSTGRES_DSN
      valueFrom:
        secretKeyRef:
          name: zitadel-db-credentials
          key: dsn
  configmapConfig:
    ExternalDomain: "zitadel.example.com"
    ExternalSecure: true
    ExternalPort: 443
    TLS:
      Enabled: false
    FirstInstance:
      Org:
        Human:
          UserName: "admin"
          Email: "admin@example.com"
          Password: "YourSecurePassword123!"
          PasswordChangeRequired: true

podDisruptionBudget:
  enabled: true
  minAvailable: 1

Exposing ZITADEL (Choose your routing method)

ZITADEL can be exposed using standard Kubernetes Ingress or the newer Gateway API. Choose the method that matches your cluster's architecture.

Option A: Using Standard Ingress (e.g., Traefik)

Append the following routing block to your values.yaml to have Helm automatically generate standard Ingress objects:

ingress:
  enabled: true
  className: traefik
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls: "true"
    traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
  hosts:
    - host: zitadel.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: zitadel-tls
      hosts:
        - zitadel.example.com

login:
  ingress:
    enabled: true
    className: traefik
    annotations:
      traefik.ingress.kubernetes.io/router.entrypoints: websecure
      traefik.ingress.kubernetes.io/router.tls: "true"
      traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
    hosts:
      - host: zitadel.example.com
        paths:
          - path: /ui/v2/login
            pathType: Prefix
    tls:
      - secretName: zitadel-tls
        hosts:
          - zitadel.example.com

Option B: Using Gateway API

If your cluster utilizes the Kubernetes Gateway API, disable the Helm chart's built-in ingress generation by ensuring both ingress.enabled: false and login.ingress.enabled: false are set (which is the default).

After deploying the Helm chart, apply an HTTPRoute resource to route traffic to the ZITADEL services via your Gateway:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: zitadel-route
  namespace: zitadel
spec:
  parentRefs:
    - name: your-cluster-gateway
      namespace: gateway-system
  hostnames:
    - "zitadel.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /ui/v2/login
      backendRefs:
        - name: zitadel-login
          port: 8080
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: zitadel
          port: 8080

(Note: Ensure your Gateway controller supports h2c/HTTP2 backend connections).

Install

helm repo add zitadel https://charts.zitadel.com && helm repo update

helm install zitadel zitadel/zitadel --values values.yaml --wait

Verify

Watch the pods come up:

kubectl get pods --watch

You should see the zitadel-init and zitadel-setup jobs complete, followed by the zitadel deployment pods becoming Ready.

Check the Helm release status:

helm status zitadel

Access the console at https://zitadel.example.com/ui/console.

What's next

Review the Production Checklist before going live, then use the detailed guides for each concern:

GuideDescription
ConfigurationAll configmap and secret options, autoscaling, security contexts
RoutingIngress, Gateway API, and cloud-native setup
DatabasePostgreSQL TLS modes, credentials, and connection pooling
OperationsUpgrades, manual and automatic scaling, resource limits
CachingRedis/Valkey caching configuration
ObservabilityTraces (OTLP), Prometheus metrics, and log collection
UninstallingRemove ZITADEL from your cluster

Was this page helpful?

On this page