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.yamlEdit 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: traefikIf 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 --waitThat'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 32Once 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:
| Component | How to replace |
|---|---|
| Database | Set 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 / Ingress | Update ingress.className to match your controller, or disable the Helm ingress (ingress.enabled: false) to manage your own Gateway API HTTPRoute resources. |
| Caching | Add 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: 1Exposing 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.comOption 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 --waitVerify
Watch the pods come up:
kubectl get pods --watchYou 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 zitadelAccess 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:
| Guide | Description |
|---|---|
| Configuration | All configmap and secret options, autoscaling, security contexts |
| Routing | Ingress, Gateway API, and cloud-native setup |
| Database | PostgreSQL TLS modes, credentials, and connection pooling |
| Operations | Upgrades, manual and automatic scaling, resource limits |
| Caching | Redis/Valkey caching configuration |
| Observability | Traces (OTLP), Prometheus metrics, and log collection |
| Uninstalling | Remove ZITADEL from your cluster |
Was this page helpful?