docs: added docs for provisioning zitadel on Kubernetes (#11410)

This commit is contained in:
Mridang Agarwalla
2026-01-28 11:03:25 +00:00
committed by GitHub
parent 3fc0ef2506
commit 01004649b6
10 changed files with 1121 additions and 9 deletions
@@ -1,8 +0,0 @@
---
title: Set up ZITADEL on Kubernetes
sidebar_label: Kubernetes
---
For getting started with an easily testable insecure setup with Postgres, follow the [Insecure Postgres Example](https://github.com/zitadel/zitadel-charts/tree/main/examples/1-postgres-insecure).
For more information and configuration examples, go to the [ZITADEL charts repo](https://github.com/zitadel/zitadel-charts).
@@ -0,0 +1,315 @@
---
title: Configuration
sidebar_label: Configuration
sidebar_position: 3
---
# Configuration
This guide covers the major configuration options for the Zitadel Helm chart. For a complete list of options, see the [values.yaml](https://github.com/zitadel/zitadel-charts/blob/main/charts/zitadel/values.yaml) in the chart repository.
## Global Settings
Global settings affect multiple components of the deployment.
### Replica Count
Set the number of Zitadel replicas:
```yaml
replicaCount: 2
```
### Container Images
#### Zitadel Image
Configure the Zitadel container image:
```yaml
image:
repository: ghcr.io/zitadel/zitadel
tag: "v4.9.1"
pullPolicy: IfNotPresent
```
#### Login Image
Configure the Login container image:
```yaml
login:
image:
repository: ghcr.io/zitadel/login
tag: "v4.9.1"
pullPolicy: IfNotPresent
```
### Pod Security Context
Configure security settings for the pods. These settings apply globally to all pods in the deployment:
```yaml
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
privileged: false
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
```
## Zitadel Settings
### ExternalDomain
The domain where Zitadel is accessible. This is used for generating URLs, cookies, and OIDC endpoints.
```yaml
zitadel:
configmapConfig:
ExternalDomain: "zitadel.example.com"
ExternalPort: 443
ExternalSecure: true
```
| Setting | Description |
|---------|-------------|
| `ExternalDomain` | The public domain name (no protocol or port) |
| `ExternalPort` | The public port (443 for HTTPS, 80 for HTTP) |
| `ExternalSecure` | Whether the external connection uses HTTPS |
### TLS
TLS is terminated at the ingress controller. The Zitadel containers do not handle TLS termination:
```yaml
zitadel:
configmapConfig:
TLS:
Enabled: false
```
### FirstInstance / Bootstrapping
Configure the initial Zitadel instance created during setup.
```yaml
zitadel:
configmapConfig:
FirstInstance:
Org:
Human:
UserName: "admin"
Password: "SecurePassword123!"
FirstName: "Zitadel"
LastName: "Admin"
Email: "[email protected]"
PasswordChangeRequired: false
```
#### Machine User for System API
For programmatic access, configure a system user with RSA keys:
```yaml
zitadel:
configmapConfig:
SystemAPIUsers:
systemuser:
KeyData: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
```
Generate the RSA private key:
```bash
openssl genrsa -out system-user-private.pem 2048
```
Extract the public key:
```bash
openssl rsa -in system-user-private.pem -pubout -out system-user-public.pem
```
## Secrets
### Referenced Secrets
Reference Kubernetes Secrets for sensitive values:
```yaml
zitadel:
masterkeySecretName: zitadel-masterkey
configSecretName: zitadel-config-secret
```
Create the masterkey secret:
```bash
kubectl create secret generic zitadel-masterkey \
--from-literal=masterkey="$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)"
```
Create a config secret for sensitive values. Save this as `zitadel-config-secret.yaml`:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: zitadel-config-secret
type: Opaque
stringData:
config.yaml: |
Database:
Postgres:
User:
Password: "secure-app-password"
Admin:
Password: "secure-admin-password"
```
Apply the secret:
```bash
kubectl apply --filename zitadel-config-secret.yaml
```
Reference it in your values:
```yaml
zitadel:
configSecretName: zitadel-config-secret
configSecretKey: config.yaml
```
## Scaling and Availability
Zitadel is designed to run as a stateless application, which makes horizontal scaling straightforward. For production deployments, you should run multiple replicas to ensure availability during node failures, deployments, and other disruptions.
### Replica Count
Running at least two replicas ensures that your Zitadel deployment remains available if one pod fails or is evicted. For larger deployments with higher traffic, you may want to run more replicas.
```yaml
replicaCount: 2
```
### Autoscaling
The chart supports the [Horizontal Pod Autoscaler](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) (HPA) to automatically scale the number of Zitadel replicas based on resource utilization. When autoscaling is enabled, the HPA overrides the `replicaCount` value.
Enable autoscaling with CPU-based scaling:
```yaml
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPU: 80
```
You can also scale based on memory utilization:
```yaml
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetMemory: 80
```
For more advanced scaling, you can use custom metrics exposed by Zitadel. This requires a metrics server such as [Prometheus](https://prometheus.io/) and a metrics adapter such as [prometheus-adapter](https://github.com/kubernetes-sigs/prometheus-adapter) running in your cluster.
The following example scales when the average number of goroutines per pod exceeds 150. The `go_goroutines` metric is a good proxy for concurrent load. You should observe your application's baseline to find a suitable value for your workload.
```yaml
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
metrics:
- type: Pods
pods:
metric:
name: "go_goroutines"
target:
type: AverageValue
averageValue: "150"
```
You can also configure the [scaling behavior](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#configurable-scaling-behavior) to control how quickly the HPA scales up or down:
```yaml
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPU: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
```
### Pod Disruption Budget
A [Pod Disruption Budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) ensures that a minimum number of pods remain available during voluntary disruptions such as node drains, cluster upgrades, or deployment rollouts. Without a PDB, Kubernetes may evict all your pods simultaneously during maintenance.
```yaml
podDisruptionBudget:
enabled: true
minAvailable: 1
```
With `minAvailable: 1`, Kubernetes guarantees that at least one Zitadel pod is always running. If you have more replicas, you can increase this value accordingly.
### Pod Anti-Affinity
Pod anti-affinity rules tell Kubernetes to schedule Zitadel pods on different nodes. This prevents a single node failure from taking down all your Zitadel replicas.
```yaml
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- zitadel
topologyKey: kubernetes.io/hostname
```
The `preferredDuringSchedulingIgnoredDuringExecution` rule is a soft preference. Kubernetes will try to spread pods across nodes, but will still schedule pods on the same node if no other nodes are available. For stricter requirements, you can use `requiredDuringSchedulingIgnoredDuringExecution` instead.
## Next Steps
- [Configuring the Ingress](./ingress) — Set up ingress for Zitadel and Login containers
- [Configuring the Database](./database) — Connect to PostgreSQL
- [Configuring Observability](./observability) — Collect traces, metrics, and logs
- [Operations](./operations) — Learn about upgrades and scaling
- [Uninstalling](./uninstalling) — Remove Zitadel from your cluster
@@ -0,0 +1,128 @@
---
title: Database
sidebar_label: Database
sidebar_position: 5
---
# Configuring the Database
## PostgreSQL
Zitadel requires PostgreSQL 14 or later. The chart supports multiple ways to connect to PostgreSQL depending on your security requirements.
### Connecting Without TLS (Not Recommended)
This method connects to PostgreSQL without encryption. Only use this for testing or when the database is on a private network with no risk of interception.
```yaml
zitadel:
configmapConfig:
Database:
Postgres:
Host: "postgres.database.svc.cluster.local"
Port: 5432
Database: "zitadel"
User:
Username: "zitadel"
Password: "your-password"
SSL:
Mode: "disable"
Admin:
Username: "postgres"
Password: "your-admin-password"
SSL:
Mode: "disable"
```
The `SSL.Mode: "disable"` setting turns off TLS entirely. Traffic between Zitadel and PostgreSQL is unencrypted.
### Connecting with Credentials and TLS
This method connects to PostgreSQL with TLS encryption but without certificate verification. Use this when you trust the network path but want encryption in transit.
```yaml
zitadel:
configSecretName: zitadel-db-credentials
configmapConfig:
Database:
Postgres:
Host: "postgres.database.svc.cluster.local"
Port: 5432
Database: "zitadel"
User:
Username: "zitadel"
SSL:
Mode: "require"
Admin:
Username: "postgres"
SSL:
Mode: "require"
```
Create a secret for the database passwords:
```bash
kubectl create secret generic zitadel-db-credentials \
--from-literal=config.yaml="
Database:
Postgres:
User:
Password: your-secure-app-password
Admin:
Password: your-secure-admin-password
"
```
The `SSL.Mode: "require"` setting enforces TLS but does not verify the server certificate. This protects against passive eavesdropping but not against man-in-the-middle attacks.
### Connecting with Certificates
This method connects to PostgreSQL with full TLS verification using certificates. Use this for production deployments where you need to verify the database server's identity.
```yaml
zitadel:
configSecretName: zitadel-db-credentials
dbSslCaCrt: "ca.crt"
dbSslCaCrtSecret: "postgres-ca-cert"
configmapConfig:
Database:
Postgres:
Host: "postgres.database.svc.cluster.local"
Port: 5432
Database: "zitadel"
User:
Username: "zitadel"
SSL:
Mode: "verify-full"
Admin:
Username: "postgres"
SSL:
Mode: "verify-full"
```
Create a secret containing the CA certificate:
```bash
kubectl create secret generic postgres-ca-cert \
--from-file=ca.crt=/path/to/your/ca-certificate.crt
```
Create a secret for the database passwords:
```bash
kubectl create secret generic zitadel-db-credentials \
--from-literal=config.yaml="
Database:
Postgres:
User:
Password: your-secure-app-password
Admin:
Password: your-secure-admin-password
"
```
The `SSL.Mode: "verify-full"` setting enforces TLS and verifies that the server certificate is signed by the CA and that the server hostname matches the certificate. This provides full protection against eavesdropping and man-in-the-middle attacks.
The `dbSslCaCrtSecret` references the Kubernetes Secret containing the CA certificate, and `dbSslCaCrt` specifies the key within that secret.
@@ -0,0 +1,62 @@
---
title: Kubernetes
sidebar_label: Overview
sidebar_position: 1
---
# Zitadel Helm Chart
Deploy Zitadel, the open-source identity and access management platform, on Kubernetes using Helm.
## What's in the Chart
By default, this chart installs a highly available Zitadel deployment. The chart deploys:
- A Zitadel **init job** that prepares the database schema
- A Zitadel **setup job** that bootstraps the first instance and admin user
- A Zitadel **deployment** with configurable replicas for high availability
The execution order is orchestrated using Helm hooks on installations and upgrades.
## Architecture Overview
Zitadel is deployed as two separate containers within your Kubernetes cluster. The main Zitadel container handles all API traffic, the console UI, and OIDC endpoints, while the Login container serves the login UI at the `/ui/v2/login` path. Both containers are stateless and can be scaled horizontally to handle increased load.
Traffic enters the cluster through an ingress controller such as Traefik or NGINX. The chart creates two separate Ingress resources, one for each container, both typically sharing the same domain and TLS certificate. TLS termination happens at the ingress controller, which forwards unencrypted HTTP/2 traffic to the pods. cert-manager or the ingress controller's built-in ACME support can be used to automatically obtain and renew TLS certificates from Let's Encrypt.
Zitadel stores all persistent data in PostgreSQL. The database holds user accounts, organizations, projects, applications, sessions, and all other identity data. Zitadel uses two database connections: an admin connection for schema migrations during startup and an application connection for normal operations. Both connections should use TLS in production environments.
For observability, Zitadel pushes traces to an OpenTelemetry Collector using the OTLP protocol over gRPC. Prometheus scrapes metrics from the `/debug/metrics` endpoint on the Zitadel pods. Both containers write structured logs to stdout, which must be collected by a log collector such as Fluent Bit or Vector. The Login container does not support tracing or metrics, only log collection. All observability data can be forwarded to a unified backend such as Grafana, OpenObserve, or your preferred monitoring stack.
## Prerequisites at a Glance
| Requirement | Minimum Version |
|-------------|-----------------|
| Kubernetes | 1.30+ |
| Helm | 3.x or 4.x |
| kubectl | 1.30+ |
| PostgreSQL | 14+ |
You also need:
- An ingress controller (Traefik, NGINX, or cloud-native)
- A domain with DNS configured
- TLS certificates (via cert-manager, ACME, or manual)
## Quick Links
| Resource | Link |
|----------|------|
| Helm Chart Repository | [charts.zitadel.com](https://charts.zitadel.com) |
| Source Code | [github.com/zitadel/zitadel-charts](https://github.com/zitadel/zitadel-charts) |
| Zitadel Documentation | [zitadel.com/docs](https://zitadel.com/docs) |
| Artifact Hub | [artifacthub.io/packages/helm/zitadel/zitadel](https://artifacthub.io/packages/helm/zitadel/zitadel) |
## Next Steps
- [Installation](./installation) — Get Zitadel running
- [Configuration](./configuration) — Customize your deployment
- [Configuring the Ingress](./ingress) — Set up ingress for Zitadel and Login containers
- [Configuring the Database](./database) — Connect to PostgreSQL
- [Operations](./operations) — Upgrades and scaling
- [Uninstalling](./uninstalling) — Remove Zitadel from your cluster
@@ -0,0 +1,60 @@
---
title: Ingress
sidebar_label: Ingress
sidebar_position: 4
---
# Configuring the Ingress
The chart creates standard [Kubernetes Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) resources. There are separate Ingress resources for the Zitadel container and the Login container. TLS must be terminated at the ingress controller; the Zitadel and Login containers do not handle TLS termination.
Both ingresses typically use the same domain and TLS certificate. The Login ingress serves the `/ui/v2/login` path while the Zitadel ingress serves everything else.
## Zitadel Ingress
The Zitadel ingress handles all API traffic, the console UI, and OIDC endpoints. The example below uses [Traefik](https://traefik.io/traefik/) as the ingress controller:
```yaml
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
hosts:
- host: zitadel.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: zitadel-tls
hosts:
- zitadel.example.com
```
The `tls.secretName` references a Kubernetes Secret containing your TLS certificate. You can create this secret manually, or have your ingress controller issue certificates automatically. For automatic certificate issuance with [Traefik ACME](https://doc.traefik.io/traefik/https/acme/), add the `traefik.ingress.kubernetes.io/router.tls.certresolver` annotation. For [cert-manager](https://cert-manager.io/), add the `cert-manager.io/cluster-issuer` annotation.
## Login Ingress
The Login ingress handles the login UI. It must be configured separately:
```yaml
login:
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
hosts:
- host: zitadel.example.com
paths:
- path: /ui/v2/login
pathType: Prefix
tls:
- secretName: zitadel-tls
hosts:
- zitadel.example.com
```
The `tls.secretName` references a Kubernetes Secret containing your TLS certificate. You can use the same secret as the Zitadel ingress since both use the same domain. For automatic certificate issuance with [Traefik ACME](https://doc.traefik.io/traefik/https/acme/), add the `traefik.ingress.kubernetes.io/router.tls.certresolver` annotation. For [cert-manager](https://cert-manager.io/), add the `cert-manager.io/cluster-issuer` annotation.
@@ -0,0 +1,235 @@
---
title: Installation
sidebar_label: Installation
sidebar_position: 2
---
# Installation
This guide walks you through deploying Zitadel on Kubernetes using Helm.
## Prerequisites
You need a running Kubernetes cluster. Supported versions:
- Kubernetes 1.30.13+
- Kubernetes 1.31.9+
- Kubernetes 1.32.5+
- Kubernetes 1.33.1+
### An Ingress Controller
Zitadel requires an ingress controller to route traffic to the Zitadel and Login pods. The chart creates standard Kubernetes Ingress resources that work with any ingress controller, including Traefik, NGINX, and cloud-native options like AWS ALB or GCP Cloud Load Balancer.
TLS must be terminated at the ingress controller. The Zitadel and Login containers do not handle TLS termination. Your ingress controller is responsible for obtaining and renewing TLS certificates (via ACME, cert-manager, or manually), terminating TLS connections, and forwarding traffic to the Zitadel pods.
Without an ingress controller and valid TLS certificates, Zitadel will not work. The chart configures the ingress to use HTTP/2 for communication between the ingress controller and Zitadel pods.
### A PostgreSQL Database
Zitadel requires PostgreSQL 14 or later. You can use a managed PostgreSQL service such as Amazon RDS, Google Cloud SQL, or Azure Database for PostgreSQL, or run PostgreSQL in your cluster using a Helm chart or operator.
The database must be accessible from the Zitadel pods. Zitadel uses two database users: an admin user for schema migrations and an application user for normal operations. See the [Database](./database) page for connection options.
### Helm
Install Helm 3.x or 4.x following the [official instructions](https://helm.sh/docs/intro/install/).
Verify your installation:
```bash
helm version
```
### kubectl
Install kubectl following the [Kubernetes documentation](https://kubernetes.io/docs/tasks/tools/).
Verify connectivity to your cluster:
```bash
kubectl cluster-info
```
## Add the Helm Repository
Add the Zitadel Helm repository:
```bash
helm repo add zitadel https://charts.zitadel.com
```
Update your local repository cache:
```bash
helm repo update
```
Verify the repository was added:
```bash
helm search repo zitadel
```
You should see output similar to:
```
NAME CHART VERSION APP VERSION DESCRIPTION
zitadel/zitadel 9.x.x v4.x.x A Helm chart for Zitadel
```
## Install
### Step 1: Create Secrets
Create the masterkey secret:
```bash
kubectl create secret generic zitadel-masterkey \
--from-literal=masterkey="$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)"
```
Create the database credentials secret:
```bash
kubectl create secret generic zitadel-db-credentials \
--from-literal=config.yaml="
Database:
Postgres:
User:
Password: your-secure-app-password
Admin:
Password: your-secure-admin-password
"
```
### Step 2: Configure Values
Create a `values.yaml` file. Replace `zitadel.example.com` with your actual domain:
```yaml
replicaCount: 2
zitadel:
masterkeySecretName: zitadel-masterkey
configSecretName: zitadel-db-credentials
configmapConfig:
ExternalDomain: "zitadel.example.com"
ExternalSecure: true
ExternalPort: 443
TLS:
Enabled: false
Database:
Postgres:
Host: "postgres.database.svc.cluster.local"
Port: 5432
Database: "zitadel"
User:
Username: "zitadel"
SSL:
Mode: "verify-full"
Admin:
Username: "postgres"
SSL:
Mode: "verify-full"
FirstInstance:
Org:
Human:
UserName: "admin"
FirstName: "Zitadel"
LastName: "Admin"
Email: "[email protected]"
Password: "YourSecurePassword123!"
PasswordChangeRequired: true
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
podDisruptionBudget:
enabled: true
minAvailable: 1
```
### Step 3: Install the Chart
```bash
helm install zitadel zitadel/zitadel --values values.yaml
```
Install Zitadel:
```bash
helm install my-zitadel zitadel/zitadel --values values.yaml
```
## Verify Deployment
Watch the pods come up:
```bash
kubectl get pods --watch
```
You should see:
1. `my-zitadel-init-*` job completes
2. `my-zitadel-setup-*` job completes
3. `my-zitadel-*` deployment pods become Ready
Check the deployment status:
```bash
helm status my-zitadel
```
Verify all pods are running:
```bash
kubectl get pods --selector app.kubernetes.io/name=zitadel
```
You should see output similar to:
```
NAME READY STATUS RESTARTS AGE
my-zitadel-xxxxxxxxx-xxxxx 1/1 Running 0 5m
my-zitadel-xxxxxxxxx-xxxxx 1/1 Running 0 5m
```
## Access the UI
Access Zitadel at:
```
https://zitadel.example.com/ui/console
```
## First Login
Access the console at `https://zitadel.example.com/ui/console` and log in with the admin credentials configured in your `values.yaml`.
## Next Steps
- [Configuration](./configuration) — Customize Zitadel settings
- [Configuring the Ingress](./ingress) — Set up ingress for Zitadel and Login containers
- [Configuring the Database](./database) — Connect to PostgreSQL
- [Operations](./operations) — Learn about upgrades and scaling
- [Uninstalling](./uninstalling) — Remove Zitadel from your cluster
@@ -0,0 +1,84 @@
---
title: Observability
sidebar_label: Observability
sidebar_position: 6
---
# Configuring Observability
This guide covers how to collect traces, metrics, and logs from the Zitadel and Login containers.
## Zitadel Container
### Traces
Zitadel can push traces to an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) using the [OTLP protocol](https://opentelemetry.io/docs/specs/otlp/) over gRPC:
```yaml
env:
- name: ZITADEL_TRACING_TYPE
value: "otel"
- name: ZITADEL_TRACING_ENDPOINT
value: "otelcol-opentelemetry-collector.monitoring.svc.cluster.local:4317"
- name: ZITADEL_TRACING_SERVICENAME
value: "zitadel"
```
The `ZITADEL_TRACING_ENDPOINT` should point to your OpenTelemetry Collector's gRPC receiver, typically listening on port 4317. The collector can then forward traces to your preferred backend such as [Jaeger](https://www.jaegertracing.io/), [Grafana Tempo](https://grafana.com/oss/tempo/), or [OpenObserve](https://openobserve.ai/).
You can deploy an OpenTelemetry Collector in your cluster using the [OpenTelemetry Operator](https://github.com/open-telemetry/opentelemetry-operator) or the [OpenTelemetry Collector Helm chart](https://github.com/open-telemetry/opentelemetry-helm-charts/tree/main/charts/opentelemetry-collector).
### Metrics
Zitadel exposes [Prometheus](https://prometheus.io/) metrics at `/debug/metrics`. Metrics must be scraped by Prometheus. Pushing metrics is not supported.
Enable the metrics endpoint:
```yaml
metrics:
enabled: true
serviceMonitor:
enabled: false
```
Set `serviceMonitor.enabled` to `true` if you are using the [Prometheus Operator](https://prometheus-operator.dev/). This creates a [ServiceMonitor](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.ServiceMonitor) resource that configures Prometheus to scrape the Zitadel pods automatically.
For standard Prometheus without the Operator, add scrape annotations to the pods:
```yaml
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/debug/metrics"
```
Your Prometheus configuration must include a [scrape job](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) that discovers pods with these annotations. If you are using [kube-prometheus-stack](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack), pod annotation discovery is typically enabled by default.
### Logs
Zitadel writes structured JSON logs to stdout. Logs must be collected by a log collector running in your cluster. Pushing logs is not supported.
Common log collectors include:
- [Fluent Bit](https://fluentbit.io/) — Lightweight log processor and forwarder
- [Vector](https://vector.dev/) — High-performance observability data pipeline
- [Fluentd](https://www.fluentd.org/) — Unified logging layer
- [Promtail](https://grafana.com/docs/loki/latest/clients/promtail/) — Log collector for Grafana Loki
Configure your log collector to tail container logs from the Zitadel pods and forward them to your preferred backend such as [Grafana Loki](https://grafana.com/oss/loki/), [Elasticsearch](https://www.elastic.co/elasticsearch/), or [OpenObserve](https://openobserve.ai/).
## Login Container
### Traces
The login container does not support tracing. Traces cannot be collected from the login container.
### Metrics
The login container does not expose metrics. Prometheus metrics are not available for the login container.
### Logs
The login container writes logs to stdout. Logs must be collected by a log collector running in your cluster. Pushing logs is not supported.
Configure your log collector to tail container logs from the login pods. The login container runs as a separate deployment, so ensure your log collector is configured to collect from all pods in the namespace or specifically from pods with the login container labels.
@@ -0,0 +1,151 @@
---
title: Operations
sidebar_label: Operations
sidebar_position: 6
---
# Operations
This guide covers day-2 operations for Zitadel on Kubernetes.
## Upgrades
### General Upgrade Process
1. Review the release notes for your target version
2. Back up your database
3. Update your `values.yaml` with any required changes
4. Upgrade the Helm release:
```bash
helm repo update
helm upgrade my-zitadel zitadel/zitadel --values values.yaml --version <target-version>
```
5. Monitor the upgrade. Watch the pods:
```bash
kubectl get pods --watch
```
Check the Helm release status:
```bash
helm status my-zitadel
```
## Scaling
### Manual Scaling
Adjust the replica count in your values:
```yaml
replicaCount: 3
```
Or scale directly:
```bash
kubectl scale deployment my-zitadel --replicas=3
```
### Horizontal Pod Autoscaler
Enable HPA for automatic scaling based on resource utilization:
```yaml
zitadel:
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
```
This creates an HPA that:
- Maintains at least 2 replicas
- Scales up to 10 replicas
- Targets 80% CPU and memory utilization
View HPA status:
```bash
kubectl get hpa
```
Get detailed HPA information:
```bash
kubectl describe hpa my-zitadel
```
### Resource Requests and Limits
Configure appropriate resource allocations:
```yaml
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
```
**Recommendations by deployment size:**
| Size | CPU Request | CPU Limit | Memory Request | Memory Limit |
|------|-------------|-----------|----------------|--------------|
| Small (dev) | 100m | 500m | 256Mi | 512Mi |
| Medium | 250m | 1000m | 512Mi | 1Gi |
| Large | 500m | 2000m | 1Gi | 2Gi |
### Pod Disruption Budget
Ensure availability during voluntary disruptions by using `minAvailable`:
```yaml
podDisruptionBudget:
enabled: true
minAvailable: 1
```
Alternatively, use `maxUnavailable`:
```yaml
podDisruptionBudget:
enabled: true
maxUnavailable: 1
```
This ensures at least one pod remains available during node drains, upgrades, or other voluntary disruptions.
### Database Scaling Considerations
When scaling Zitadel horizontally, ensure your PostgreSQL database can handle the increased connection load:
- Each Zitadel pod opens multiple connections
- Consider using PgBouncer for connection pooling
- Monitor database connection usage
Example connection pooling setup with PgBouncer:
```yaml
zitadel:
configmapConfig:
Database:
Postgres:
Host: "pgbouncer.database.svc.cluster.local"
Port: 6432
MaxOpenConns: 20
MaxIdleConns: 10
```
## Next Steps
- [Configuration](./configuration) — Review all configuration options
- [Uninstalling](./uninstalling) — Remove Zitadel from your cluster
@@ -0,0 +1,60 @@
---
title: Uninstalling
sidebar_label: Uninstalling
sidebar_position: 7
---
# Uninstalling
This guide covers how to remove Zitadel from your Kubernetes cluster.
## Uninstall the Helm Release
```bash
helm uninstall my-zitadel
```
## Clean Up Helm Hooks
> ⚠️ **Important:** The Zitadel chart uses Helm hooks, which are [not garbage collected by helm uninstall](https://helm.sh/docs/topics/charts_hooks/#hook-resources-are-not-managed-with-corresponding-releases).
Remove hook resources manually:
```bash
for k8sresourcetype in job configmap secret rolebinding role serviceaccount; do
kubectl delete $k8sresourcetype \
--selector app.kubernetes.io/name=zitadel,app.kubernetes.io/managed-by=Helm
done
```
## Complete Namespace Teardown
To remove everything including the namespace and any Persistent Volume Claims:
```bash
kubectl delete namespace zitadel
```
> ⚠️ **Warning:** This permanently deletes all data in the namespace, including any PVCs. Ensure you have backups if needed.
## Verify Removal
Confirm all resources have been removed.
Check for remaining Zitadel resources:
```bash
kubectl get all --selector app.kubernetes.io/name=zitadel
```
Check for remaining secrets:
```bash
kubectl get secrets --selector app.kubernetes.io/name=zitadel
```
If you did not delete the namespace, check for remaining PVCs:
```bash
kubectl get pvc --selector app.kubernetes.io/name=zitadel
```
+26 -1
View File
@@ -742,7 +742,32 @@ module.exports = {
"self-hosting/deploy/macos",
"self-hosting/deploy/devcontainer",
"self-hosting/deploy/compose",
"self-hosting/deploy/kubernetes",
{
type: "category",
label: "Kubernetes",
link: {
type: "doc",
id: "self-hosting/deploy/kubernetes/index",
},
items: [
"self-hosting/deploy/kubernetes/installation",
{
type: "category",
label: "Configuration",
link: {
type: "doc",
id: "self-hosting/deploy/kubernetes/configuration",
},
items: [
"self-hosting/deploy/kubernetes/ingress",
"self-hosting/deploy/kubernetes/database",
"self-hosting/deploy/kubernetes/observability",
],
},
"self-hosting/deploy/kubernetes/operations",
"self-hosting/deploy/kubernetes/uninstalling",
],
},
{
type: "category",
label: "Manage",