# 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](/content/docs/self-hosting/deploy/kubernetes#stage-1-quickstart/index.html)

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](/content/docs/self-hosting/deploy/kubernetes#prerequisites/index.html)

- A Kubernetes cluster (1.30+)
- An Ingress controller (e.g., Traefik, NGINX) **OR** a Gateway API controller
- [kubectl](https://kubernetes.io/docs/tasks/tools/)
- [Helm](https://helm.sh/docs/intro/install/) 3.x or 4.x

No cluster yet? [k3d](https://k3d.io/) 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](/content/docs/self-hosting/deploy/kubernetes#install-the-full-stack/index.html)

```
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](http://localhost/ui/console?login_hint=zitadel-admin@zitadel.localhost) and log in with password `Password1!`.

The masterkey [encrypts sensitive data at rest](/content/docs/concepts/architecture/secrets/index.html). 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](/content/docs/self-hosting/manage/http2/index.html) for requirements on how traffic must be forwarded to ZITADEL pods via h2c.

### [Swap out components](/content/docs/self-hosting/deploy/kubernetes#swap-out-components/index.html)

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](/content/docs/self-hosting/deploy/kubernetes/caching/index.html)). |

## [Stage 2 — Production Cluster](/content/docs/self-hosting/deploy/kubernetes#stage-2-production-cluster/index.html)

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](/content/docs/self-hosting/manage/database/index.html).

The ZITADEL management console [requires end-to-end HTTP/2 support](/content/docs/self-hosting/manage/http2/index.html). Ensure your routing controller is configured to forward HTTP/2 (h2c) traffic to ZITADEL pods.

### [Create secrets](/content/docs/self-hosting/deploy/kubernetes#create-secrets/index.html)

```
# 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](/content/docs/self-hosting/deploy/kubernetes#configure-values/index.html)

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)](/content/docs/self-hosting/deploy/kubernetes#exposing-zitadel-choose-your-routing-method/index.html)

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)](/content/docs/self-hosting/deploy/kubernetes#option-a-using-standard-ingress-e-g-traefik/index.html)

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](/content/docs/self-hosting/deploy/kubernetes#option-b-using-gateway-api/index.html)

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](/content/docs/self-hosting/deploy/kubernetes#install/index.html)

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

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

### [Verify](/content/docs/self-hosting/deploy/kubernetes#verify/index.html)

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](/content/docs/self-hosting/deploy/kubernetes#whats-next/index.html)

Review the [Production Checklist](/content/docs/self-hosting/manage/productionchecklist/index.html) before going live, then use the detailed guides for each concern:

| Guide | Description |
| --- | --- |
| [Configuration](/content/docs/self-hosting/deploy/kubernetes/configuration/index.html) | All configmap and secret options, autoscaling, security contexts |
| [Routing](/content/docs/self-hosting/deploy/kubernetes/ingress/index.html) | Ingress, Gateway API, and cloud-native setup |
| [Database](/content/docs/self-hosting/deploy/kubernetes/database/index.html) | PostgreSQL TLS modes, credentials, and connection pooling |
| [Operations](/content/docs/self-hosting/deploy/kubernetes/operations/index.html) | Upgrades, manual and automatic scaling, resource limits |
| [Caching](/content/docs/self-hosting/deploy/kubernetes/caching/index.html) | Redis/Valkey caching configuration |
| [Observability](/content/docs/self-hosting/deploy/kubernetes/observability/index.html) | Traces (OTLP), Prometheus metrics, and log collection |
| [Uninstalling](/content/docs/self-hosting/deploy/kubernetes/uninstalling/index.html) | Remove ZITADEL from your cluster |
