Files
zitadel/apps/docs/content/self-hosting/deploy/kubernetes/index.mdx
T
Federico CoppedeandGitHub b4f2da2171 docs: Add Gateway API routing to Kubernetes deployment guide (#12168)
## Description
This PR updates the Kubernetes deployment documentation to address user
feedback regarding our routing instructions. A user noted that standard
`Ingress` is becoming outdated and requested configuration examples for
the modern Gateway API.

To support both existing and modern clusters, I have updated the guide
to provide two clear pathways for exposing ZITADEL.

## Changes Included
* **Prerequisites updated:** Mentioned Gateway API controllers alongside
standard Ingress controllers.
* **Refactored Stage 2 (Production):** Split the routing configuration
into "Option A: Standard Ingress" and "Option B: Gateway API".
* **Added YAML example:** Provided a sample `HTTPRoute` resource to
route traffic to the `zitadel` and `zitadel-login` backend services.
* **Terminology updates:** Broadened terms like "Ingress" to "Routing"
or "Routing controller" where applicable.
2026-05-18 20:03:27 +05:30

267 lines
9.2 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Deploy ZITADEL on Kubernetes
sidebar_label: Overview
description: Deploy ZITADEL on Kubernetes using Helm — from a local 5-minute quickstart to a hardened production cluster.
---
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](https://kubernetes.io/docs/tasks/tools/)
- [Helm](https://helm.sh/docs/intro/install/) 3.x or 4.x
<Callout type="info">
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:
```bash
k3d cluster create zitadel --port "80:80@loadbalancer"
```
</Callout>
### Install the full stack
```bash
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:
```yaml
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.
```bash
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/[email protected]](http://localhost/ui/[email protected]) and log in with password `Password1!`.
<Callout type="warn">
The masterkey [encrypts sensitive data at rest](/concepts/architecture/secrets). The `quickstart-values.yaml` file contains a **dev placeholder** masterkey — generate a real one before any non-local deployment:
```bash
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.
</Callout>
<Callout type="info">
The stack runs: your ingress/gateway controller → **ZITADEL API** (Go) + **ZITADEL Login** (Next.js) → **PostgreSQL** (bundled).
See [HTTP/2](/self-hosting/manage/http2) for requirements on how traffic must be forwarded to ZITADEL pods via h2c.
</Callout>
### 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](/self-hosting/deploy/kubernetes/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+
<Callout type="warn">
**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](/self-hosting/manage/database).
</Callout>
<Callout type="warn">
The ZITADEL management console [requires end-to-end HTTP/2 support](/self-hosting/manage/http2). Ensure your routing controller is configured to forward HTTP/2 (h2c) traffic to ZITADEL pods.
</Callout>
### Create secrets
```bash
# 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:[email protected]:5432/postgres?sslmode=verify-full"
```
### Configure values
Save the base configuration below as `values.yaml`. Replace `zitadel.example.com` with your domain:
```yaml
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: "[email protected]"
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:
```yaml
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`:
```yaml
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
```bash
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:
```bash
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:
```bash
helm status zitadel
```
Access the console at `https://zitadel.example.com/ui/console`.
## What's next
Review the [Production Checklist](/self-hosting/manage/productionchecklist) before going live, then use the detailed guides for each concern:
| Guide | Description |
|-------|-------------|
| [Configuration](/self-hosting/deploy/kubernetes/configuration) | All configmap and secret options, autoscaling, security contexts |
| [Routing](/self-hosting/deploy/kubernetes/ingress) | Ingress, Gateway API, and cloud-native setup |
| [Database](/self-hosting/deploy/kubernetes/database) | PostgreSQL TLS modes, credentials, and connection pooling |
| [Operations](/self-hosting/deploy/kubernetes/operations) | Upgrades, manual and automatic scaling, resource limits |
| [Caching](/self-hosting/deploy/kubernetes/caching) | Redis/Valkey caching configuration |
| [Observability](/self-hosting/deploy/kubernetes/observability) | Traces (OTLP), Prometheus metrics, and log collection |
| [Uninstalling](/self-hosting/deploy/kubernetes/uninstalling) | Remove ZITADEL from your cluster |