feat: add DSN/URL connection string support for PostgreSQL and Redis (#11729)

This commit is contained in:
Mridang Agarwalla
2026-03-20 15:21:15 +00:00
committed by GitHub
parent dad4565d5d
commit 471ed4a5d7
25 changed files with 810 additions and 302 deletions
@@ -15,94 +15,66 @@ Caching is an experimental beta feature. See the [Caches](/self-hosting/manage/c
Zitadel supports Redis or any Redis-compatible store (for example, Valkey) in standalone mode. Sentinel and Redis Cluster are not supported because the cache connector expects a single endpoint and does not implement cluster routing or sentinel failover logic.
<Callout type="note">
Zitadel supports server-auth TLS (`EnableTLS: true`) but does not support client certificates/mTLS for cache connections.
Zitadel supports server-auth TLS (`rediss://` scheme) but does not support client certificates/mTLS for cache connections.
</Callout>
Each cache uses its own Redis database index. Set `DBOffset` so Zitadel-owned indexes do not overlap with other applications. Zitadel issues `FLUSHDB` on its cache databases, so sharing an index with other apps is unsafe.
## Connect without authentication (development only)
Pass the Redis URL as an environment variable:
```yaml
zitadel:
configmapConfig:
Caches:
Connectors:
Redis:
Enabled: true
Addr: "redis-master.caching.svc.cluster.local:6379"
DBOffset: 10
Instance:
Connector: "redis"
MaxAge: 1h
LastUseAge: 10m
Organization:
Connector: "redis"
MaxAge: 1h
LastUseAge: 10m
env:
- name: ZITADEL_CACHES_CONNECTORS_REDIS_URL
value: "redis://redis-master.caching.svc.cluster.local:6379"
- name: ZITADEL_CACHES_CONNECTORS_REDIS_ENABLED
value: "true"
```
Use this only on isolated clusters where the Redis endpoint does not require authentication.
## Connect with password from a Kubernetes Secret
Keep credentials in a secret while the rest of the cache configuration stays in values:
```yaml
zitadel:
configSecretName: zitadel-cache-credentials
configmapConfig:
Caches:
Connectors:
Redis:
Enabled: true
Addr: "redis.database.svc.cluster.local:6379"
DBOffset: 10
Instance:
Connector: "redis"
MaxAge: 1h
LastUseAge: 10m
Organization:
Connector: "redis"
MaxAge: 1h
LastUseAge: 10m
```
Create the secret with your Redis ACL user and password:
Store the Redis URL (with credentials) in a secret and pass it as an environment variable:
```bash
kubectl create secret generic zitadel-cache-credentials \
--from-literal=config.yaml="
Caches:
Connectors:
Redis:
Username: cache-user # Omit for default user
Password: your-redis-password
"
--from-literal=url="redis://cache-user:[email protected]:6379"
```
```yaml
zitadel:
env:
- name: ZITADEL_CACHES_CONNECTORS_REDIS_URL
valueFrom:
secretKeyRef:
name: zitadel-cache-credentials
key: url
- name: ZITADEL_CACHES_CONNECTORS_REDIS_ENABLED
value: "true"
```
## Connect with TLS (Redis or Valkey)
Enable TLS when your endpoint requires encryption. This uses server-auth TLS with the container's trust store.
Use the `rediss://` scheme (note the double `s`) when your endpoint requires encryption. This uses server-auth TLS with the container's trust store.
```bash
kubectl create secret generic zitadel-cache-credentials \
--from-literal=url="rediss://cache-user:[email protected]:6380"
```
```yaml
zitadel:
configSecretName: zitadel-cache-credentials
configmapConfig:
Caches:
Connectors:
Redis:
Enabled: true
Addr: "redis.example.com:6380"
EnableTLS: true
DBOffset: 10
Instance:
Connector: "redis"
MaxAge: 1h
LastUseAge: 10m
Organization:
Connector: "redis"
MaxAge: 1h
LastUseAge: 10m
env:
- name: ZITADEL_CACHES_CONNECTORS_REDIS_URL
valueFrom:
secretKeyRef:
name: zitadel-cache-credentials
key: url
- name: ZITADEL_CACHES_CONNECTORS_REDIS_ENABLED
value: "true"
```
Reuse the secret from the previous section for credentials if needed. Custom certificate authorities must be added to the container's trust bundle (for example, by mounting a CA file into the pod); client certificate authentication is not supported.
Custom certificate authorities must be added to the container's trust bundle (for example, by mounting a CA file into the pod); client certificate authentication is not supported.
@@ -162,36 +162,23 @@ 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:
Create a secret containing the database DSN:
```bash
kubectl apply --filename zitadel-config-secret.yaml
kubectl create secret generic zitadel-config-secret \
--from-literal=dsn="postgresql://zitadel:[email protected]:5432/zitadel?sslmode=disable"
```
Reference it in your values:
Pass it to Zitadel as an environment variable:
```yaml
zitadel:
configSecretName: zitadel-config-secret
configSecretKey: config.yaml
env:
- name: ZITADEL_DATABASE_POSTGRES_DSN
valueFrom:
secretKeyRef:
name: zitadel-config-secret
key: dsn
```
## Scaling and Availability
@@ -14,66 +14,45 @@ Zitadel requires PostgreSQL 14 or later. The chart supports multiple ways to con
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"
Store the DSN in a Kubernetes Secret and pass it as an environment variable:
```bash
kubectl create secret generic zitadel-db-credentials \
--from-literal=dsn="postgresql://zitadel:[email protected]:5432/zitadel?sslmode=disable"
```
The `SSL.Mode: "disable"` setting turns off TLS entirely. Traffic between Zitadel and PostgreSQL is unencrypted.
```yaml
zitadel:
env:
- name: ZITADEL_DATABASE_POSTGRES_DSN
valueFrom:
secretKeyRef:
name: zitadel-db-credentials
key: dsn
```
The `sslmode=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
"
--from-literal=dsn="postgresql://zitadel:[email protected]:5432/zitadel?sslmode=require"
```
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.
```yaml
zitadel:
env:
- name: ZITADEL_DATABASE_POSTGRES_DSN
valueFrom:
secretKeyRef:
name: zitadel-db-credentials
key: dsn
```
The `sslmode=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
@@ -81,23 +60,14 @@ This method connects to PostgreSQL with full TLS verification using certificates
```yaml
zitadel:
configSecretName: zitadel-db-credentials
env:
- name: ZITADEL_DATABASE_POSTGRES_DSN
valueFrom:
secretKeyRef:
name: zitadel-db-credentials
key: dsn
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:
@@ -107,18 +77,11 @@ kubectl create secret generic postgres-ca-cert \
--from-file=ca.crt=/path/to/your/ca-certificate.crt
```
Create a secret for the database passwords:
Create a secret containing the DSN with full verification:
```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
"
--from-literal=dsn="postgresql://zitadel:[email protected]:5432/zitadel?sslmode=verify-full&sslrootcert=/db-ssl-ca-crt/ca.crt"
```
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.
The `sslmode=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.
@@ -76,7 +76,7 @@ The bundled PostgreSQL subchart is for quickstart use only. You can replace it i
| Component | How to replace |
|-----------|---------------|
| **Database** | Set `postgresql.enabled: false` and configure `zitadel.configmapConfig.Database` pointing to your own PostgreSQL |
| **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 |
| **Ingress controller** | Update `ingress.className` (and `login.ingress.className`) to match your controller's IngressClass |
| **Caching** | Add Redis by configuring `zitadel.configmapConfig.Caches` (see [Caching](/self-hosting/deploy/kubernetes/caching)) |
@@ -106,16 +106,9 @@ The ZITADEL management console [requires end-to-end HTTP/2 support](/self-hostin
kubectl create secret generic zitadel-masterkey \
--from-literal=masterkey="$(tr -dc A-Za-z0-9 </dev/urandom | head -c 32)"
# Database credentials
# Database credentials — store the full DSN
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
"
--from-literal=dsn="postgresql://zitadel:[email protected]:5432/postgres?sslmode=verify-full"
```
### Configure values
@@ -127,26 +120,18 @@ replicaCount: 2
zitadel:
masterkeySecretName: zitadel-masterkey
configSecretName: zitadel-db-credentials
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
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:
@@ -132,13 +132,22 @@ When scaling Zitadel horizontally, ensure your PostgreSQL database can handle th
Example connection pooling setup with PgBouncer:
```bash
kubectl create secret generic zitadel-db-credentials \
--from-literal=dsn="postgresql://zitadel:[email protected]:6432/zitadel?sslmode=disable"
```
```yaml
zitadel:
env:
- name: ZITADEL_DATABASE_POSTGRES_DSN
valueFrom:
secretKeyRef:
name: zitadel-db-credentials
key: dsn
configmapConfig:
Database:
Postgres:
Host: "pgbouncer.database.svc.cluster.local"
Port: 6432
MaxOpenConns: 20
MaxIdleConns: 10
```
@@ -40,7 +40,7 @@ LATEST=$(curl -i https://github.com/zitadel/zitadel/releases/latest | grep locat
## Run Zitadel
```bash
ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=root ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=postgres ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ZITADEL_EXTERNALSECURE=false zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://root:postgres@localhost:5432/postgres?sslmode=disable ZITADEL_EXTERNALSECURE=false zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
```
<DefaultUser components={props.components} />
@@ -62,7 +62,7 @@ ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZIT
### Setup Zitadel with a service account
```bash
ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=root ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ZITADEL_EXTERNALSECURE=false ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH=/tmp/zitadel-admin-sa.json ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE=1 zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://root:postgres@localhost:5432/postgres?sslmode=disable ZITADEL_EXTERNALSECURE=false ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH=/tmp/zitadel-admin-sa.json ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE=1 zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
# then you can move your machine key
mv /tmp/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json
@@ -45,7 +45,7 @@ LATEST=$(curl -i https://github.com/zitadel/zitadel/releases/latest | grep locat
## Run ZITADEL
```bash
ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=$(whoami) ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=postgres ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ZITADEL_EXTERNALSECURE=false zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
ZITADEL_DATABASE_POSTGRES_DSN="postgresql://root:postgres@localhost:5432/postgres?sslmode=disable" ZITADEL_EXTERNALSECURE=false zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
```
<DefaultUser components={props.components} />
@@ -57,7 +57,7 @@ ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZIT
### Setup ZITADEL with a service account
```bash
ZITADEL_DATABASE_POSTGRES_HOST=localhost ZITADEL_DATABASE_POSTGRES_PORT=5432 ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=$(whoami) ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable ZITADEL_EXTERNALSECURE=false ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH=/tmp/zitadel-admin-sa.json ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE=1 zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
ZITADEL_DATABASE_POSTGRES_DSN="postgresql://root:postgres@localhost:5432/postgres?sslmode=disable" ZITADEL_EXTERNALSECURE=false ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH=/tmp/zitadel-admin-sa.json ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME=zitadel-admin-sa ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME=Admin ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE=1 zitadel start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
# then you can move your machine key
mv /tmp/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json
@@ -71,6 +71,25 @@ Drawbacks:
- Increased operational overhead: need to run a Redis instance as part of your infrastructure.
- When running multiple servers of ZITADEL in different regions, network roundtrip time might impact performance, neutralizing the benefit of a cache.
Connect to Redis by providing a URL via environment variable. Use `redis://` for plain connections or `rediss://` for TLS:
```bash
export ZITADEL_CACHES_CONNECTORS_REDIS_ENABLED=true
export ZITADEL_CACHES_CONNECTORS_REDIS_URL=redis://user:password@localhost:6379
```
If you prefer configuration files, set the URL in your YAML config:
```yaml
Caches:
Connectors:
Redis:
Enabled: true
URL: redis://user:password@localhost:6379
```
Pool, timeout, retry, and circuit breaker settings can be configured alongside the URL as overlays. The DB number in the URL path is ignored for cache DB selection — ZITADEL derives the actual DB from `DBOffset` + purpose.
#### Circuit breaker
A [circuit breaker](https://learn.microsoft.com/en-us/previous-versions/msp-n-p/dn589784(v=pandp.10)?redirectedfrom=MSDN) is provided for the Redis connector, to prevent a single point of failure in the case persistent errors. When the circuit breaker opens, the cache is temporary disabled and ignored. ZITADEL will continue to operate using queries to the database.
@@ -80,7 +99,7 @@ Caches:
Connectors:
Redis:
Enabled: true
Addr: localhost:6379
URL: redis://user:password@localhost:6379
# Many other options...
CircuitBreaker:
# Interval when the counters are reset to 0.
@@ -226,14 +245,24 @@ Caches:
MaxAge: 1s
```
When running many instances on multiple servers:
When running many instances on multiple servers, set the Redis URL and enable the connector:
```bash
export ZITADEL_CACHES_CONNECTORS_REDIS_ENABLED=true
export ZITADEL_CACHES_CONNECTORS_REDIS_URL=redis://user:[email protected]:6379
export ZITADEL_CACHES_INSTANCE_CONNECTOR=redis
export ZITADEL_CACHES_MILESTONES_CONNECTOR=redis
export ZITADEL_CACHES_ORGANIZATION_CONNECTOR=redis
```
Or in a configuration file:
```yaml
Caches:
Connectors:
Redis:
Enabled: true
# Other connection options
URL: redis://user:[email protected]:6379
Instance:
Connector: "redis"
@@ -51,16 +51,12 @@ zitadel start-from-init \
### Configure by Environment Variables
The environment variable approach uses a single DSN string instead of the individual field-based configuration shown above.
```bash
# Set runtime environment variables
export ZITADEL_DATABASE_POSTGRES_HOST=my.database
export ZITADEL_DATABASE_POSTGRES_PORT=5432
export ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel
export ZITADEL_DATABASE_POSTGRES_USER_USERNAME=my_zitadel_db_user
export ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=Secret_DB_User_Password
export ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable
export ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=root
export ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable
# Set the PostgreSQL connection via a single DSN
# Use the "postgres" maintenance database so ZITADEL can create its own database during initialization
export ZITADEL_DATABASE_POSTGRES_DSN=postgresql://my_zitadel_db_user:[email protected]:5432/postgres?sslmode=disable
export ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME="root"
export ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD="RootPassword1!"
@@ -93,22 +93,18 @@ There are three ways to pass the masterkey to the `zitadel` binary:
<Tabs
groupId="installation-target"
default="linuxunix"
values={[
{ label: "Linux and Unix", value: "linuxunix" },
{ label: "Docker Compose", value: "compose" },
{ label: "Kubernetes (Helm)", value: "k8s" },
]}
defaultIndex={1}
items={['Linux and Unix','Docker Compose', 'Kubernetes (Helm)']}
>
<Tab value="linuxunix">
<Tab value="Linux and Unix">
<LinuxUnix components={props.components} />
<Login components={props.components} />
</Tab>
<Tab value="compose">
<Tab value="Docker Compose">
<Compose components={props.components} />
<Login components={props.components} />
</Tab>
<Tab value="k8s">
<Tab value="Kubernetes (Helm)">
<Helm components={props.components} />
</Tab>
</Tabs>
@@ -22,35 +22,29 @@ If you are using Zitadel v2 and want to use a PostgreSQL database, you can [over
Currently, versions 14 to 18 are supported.
Postgres can be configured as follows:
Connect ZITADEL to your PostgreSQL database by providing a DSN (connection URL) via the environment variable:
```bash
export ZITADEL_DATABASE_POSTGRES_DSN=postgresql://zitadel:password@localhost:5432/zitadel?sslmode=disable
```
If you prefer configuration files, set the DSN in your YAML config:
```yaml
Database:
postgres:
Host: localhost
Port: 5432
Database: zitadel
AwaitInitialConn: 5m
DSN: postgresql://zitadel:password@localhost:5432/zitadel?sslmode=disable
MaxOpenConns: 15
MaxIdleConns: 10
MaxConnLifetime: 1h
MaxConnIdleTime: 5m
Options:
User:
Username: zitadel
Password: zitadel
SSL:
Mode: disable # Use 'require' or 'verify-full' for production
RootCert:
Cert:
Key:
Admin:
Username: postgres
Password: postgres
SSL:
Mode: disable # Use 'require' or 'verify-full' for production
RootCert:
Cert:
Key:
```
Pool and tuning settings can be configured alongside the DSN as overlays.
SSL is configured through DSN query parameters. For example, to require TLS with certificate verification:
```bash
postgresql://zitadel:[email protected]:5432/zitadel?sslmode=verify-full&sslrootcert=/path/to/ca.crt
```
### Managed PostgreSQL / No Admin Access
@@ -83,17 +77,10 @@ Adjust `pg_hba.conf` if needed to allow the zitadel user to connect.
**Schema bootstrapping step — Bootstrap schemas and tables (run as the service user)**
Set only the service user credentials — no `Admin.*` variables are needed for this command.
For managed/remote PostgreSQL, set `SSL_MODE` to `require` or `verify-full` to encrypt the connection. `disable` is only appropriate for local development.
Provide the DSN with the service user credentials. For managed/remote PostgreSQL, set `sslmode` to `require` or `verify-full` to encrypt the connection. `disable` is only appropriate for local development.
```bash
ZITADEL_DATABASE_POSTGRES_HOST=<your-host> \
ZITADEL_DATABASE_POSTGRES_PORT=5432 \
ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel \
ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel \
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=<strong-password> \
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=require \
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://zitadel:<strong-password>@<your-host>:5432/zitadel?sslmode=require \
zitadel init schema
```
@@ -104,12 +91,7 @@ For production deployments, configure TLS to encrypt authentication traffic. See
</Callout>
```bash
ZITADEL_DATABASE_POSTGRES_HOST=<your-host> \
ZITADEL_DATABASE_POSTGRES_PORT=5432 \
ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel \
ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel \
ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=<strong-password> \
ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=require \
ZITADEL_DATABASE_POSTGRES_DSN=postgresql://zitadel:<strong-password>@<your-host>:5432/zitadel?sslmode=require \
zitadel start-from-setup --masterkey "<32-character-key>"
```
@@ -117,7 +99,7 @@ See the [phases guide](/self-hosting/manage/updating_scaling#separating-init-and
## Zitadel Credentials
The [init phase](/self-hosting/manage/updating_scaling#separating-init-and-setup-from-the-runtime) of ZITADEL creates the ZITADEL database user (`Database.*.User.Username` & `Database.*.User.Password`) if it does not exist and admin credentials are provided. **Init does not update or deprecate existing users or passwords.** If you need to change the ZITADEL database user or rotate its password, you must do this manually.
The [init phase](/self-hosting/manage/updating_scaling#separating-init-and-setup-from-the-runtime) of ZITADEL creates the ZITADEL database user if it does not exist and admin credentials are provided. **Init does not update or deprecate existing users or passwords.** If you need to change the ZITADEL database user or rotate its password, you must do this manually.
<Callout type="warn">
The `init` command is designed to be run **only once**, during the initial setup of ZITADEL. Running `init` again after changing the database user or credentials does **not** migrate database object ownerships, nor does it reassign permissions on schemas, tables, or other objects to a new user. You must update these permissions manually if you switch users.
@@ -125,19 +125,28 @@ please create an issue in our [github repository](https://github.com/zitadel/zit
Depending on your environment, you maybe would want to tweak some settings about how ZITADEL interacts with the database in the database section of your ZITADEL configuration. Read more about your [database configuration options](/self-hosting/manage/database).
Set the database connection via environment variable:
```bash
export ZITADEL_DATABASE_POSTGRES_DSN=postgresql://zitadel:password@localhost:5432/zitadel?sslmode=disable
export ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS=10
export ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS=5
export ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME=30m
export ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME=5m
```
Or in a configuration file:
```yaml
Database:
postgres:
Host: localhost
Port: 5432
Database: zitadel
DSN: postgresql://zitadel:password@localhost:5432/zitadel?sslmode=disable
//highlight-start
MaxOpenConns: 10
MaxIdleConns: 5
MaxConnLifetime: 30m
MaxConnIdleTime: 5m
//highlight-end
Options: ""
```
You also might want to configure how [projections](/concepts/eventstore/implementation#projections) are computed. These are the default values:
@@ -56,17 +56,17 @@ when you install ZITADEL from scratch.
`zitadel init` performs two steps that require different privileges:
**Provisioning step** (requires a PostgreSQL superuser via `Database.postgres.Admin.Username`):
- If it doesn't exist already, it creates the unprivileged user configured in `Database.postgres.User.Username`.
Subsequent phases connect to the database with this user's credentials only.
- If it doesn't exist already, it creates a database with the configured database name.
**Provisioning step** (requires a PostgreSQL superuser):
- If it doesn’t exist already, it creates the unprivileged database user.
Subsequent phases connect to the database with this user’s credentials only.
- If it doesn’t exist already, it creates a database with the configured database name.
- If not already done, it grants the necessary permissions to the unprivileged user.
**Schema bootstrapping step** (requires database owner privileges, uses the service user):
- If they don't exist already, it creates all schemas (`eventstore`, `projections`, `system`) and base tables.
- If they don’t exist already, it creates all schemas (`eventstore`, `projections`, `system`) and base tables.
<Callout type="note">
**Manual provisioning:** If you handle the provisioning step yourself (e.g. on a managed PostgreSQL service where you don't have superuser access),
**Manual provisioning:** If you handle the provisioning step yourself (e.g. on a managed PostgreSQL service where you don’t have superuser access),
you can skip it entirely by running only the schema bootstrapping step:
```bash