diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore b/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore
new file mode 100644
index 0000000000..bd98bacd66
--- /dev/null
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore
@@ -0,0 +1 @@
+.env-file
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml
index d1d8c95bb2..013fc2aa22 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml
@@ -1,48 +1,157 @@
services:
- traefik:
+ db:
+ image: postgres:17-alpine
+ restart: unless-stopped
+ environment:
+ - POSTGRES_USER=root
+ - POSTGRES_PASSWORD=postgres
networks:
- - 'zitadel'
- image: "traefik:latest"
- ports:
- - "80:80"
- - "443:443"
+ - 'storage'
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"]
+ interval: 10s
+ timeout: 60s
+ retries: 5
+ start_period: 10s
volumes:
- - "./example-traefik.yaml:/etc/traefik/traefik.yaml"
+ - 'data:/var/lib/postgresql/data:rw'
- zitadel:
- restart: 'always'
+ zitadel-init:
+ restart: 'no'
networks:
- - 'zitadel'
+ - 'storage'
image: 'ghcr.io/zitadel/zitadel:latest'
- command: 'start-from-init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml --steps /example-zitadel-init-steps.yaml --masterkey "${ZITADEL_MASTERKEY}" --tlsMode external'
+ command: 'init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml'
depends_on:
db:
condition: 'service_healthy'
volumes:
- './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
- './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro'
+
+ zitadel-setup:
+ restart: 'no'
+ networks:
+ - 'storage'
+ # We use the debug image so we have the environment to
+ # - create the .env file for the login to authenticate at Zitadel
+ # - set the correct permissions for the .env-file folder
+ image: 'ghcr.io/zitadel/zitadel:latest-debug'
+ user: root
+ entrypoint: '/bin/sh'
+ command:
+ - -c
+ - >
+ /app/zitadel setup
+ --config /example-zitadel-config.yaml
+ --config /example-zitadel-secrets.yaml
+ --steps /example-zitadel-init-steps.yaml
+ --masterkey ${ZITADEL_MASTERKEY} &&
+ mv /pat /.env-file/pat || exit 0 &&
+ echo ZITADEL_SERVICE_USER_TOKEN=$(cat /.env-file/pat) > /.env-file/.env &&
+ chown -R 1001:${GID} /.env-file &&
+ chmod -R 770 /.env-file
+ environment:
+ - GID
+ depends_on:
+ zitadel-init:
+ condition: 'service_completed_successfully'
+ restart: false
+ volumes:
+ - './.env-file:/.env-file:rw'
+ - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
+ - './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro'
- './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro'
- db:
- image: postgres:17-alpine
- restart: always
- environment:
- - POSTGRES_USER=root
- - POSTGRES_PASSWORD=postgres
+ zitadel:
+ restart: 'unless-stopped'
networks:
- - 'zitadel'
+ - 'backend'
+ - 'storage'
+ image: 'ghcr.io/zitadel/zitadel:latest'
+ command: >
+ start --config /example-zitadel-config.yaml
+ --config /example-zitadel-secrets.yaml
+ --masterkey ${ZITADEL_MASTERKEY}
+ depends_on:
+ zitadel-setup:
+ condition: 'service_completed_successfully'
+ restart: true
+ volumes:
+ - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
+ - './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro'
+ ports:
+ - "8080:8080"
healthcheck:
- test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"]
+ test: [
+ "CMD", "/app/zitadel", "ready",
+ "--config", "/example-zitadel-config.yaml",
+ "--config", "/example-zitadel-secrets.yaml"
+ ]
interval: 10s
timeout: 60s
retries: 5
- start_period: 10s
+ start_period: 10s
+
+ # The use-new-login service configures Zitadel to use the new login v2 for all applications.
+ # It also gives the setupped machine user the necessary IAM_LOGIN_CLIENT role.
+ use-new-login:
+ restart: 'on-failure'
+ user: "1001"
+ networks:
+ - 'backend'
+ image: 'badouralix/curl-jq:alpine'
+ entrypoint: '/bin/sh'
+ command:
+ - -c
+ - >
+ curl -X PUT -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/v2/features/instance -d '{"loginV2": {"required": true}}' &&
+ LOGIN_USER=$(curl --fail-with-body -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/auth/v1/users/me | jq -r '.user.id') &&
+ curl -X PUT -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/admin/v1/members/$${LOGIN_USER} -d '{"roles": ["IAM_OWNER", "IAM_LOGIN_CLIENT"]}'
volumes:
- - 'data:/var/lib/postgresql/data:rw'
+ - './.env-file:/.env-file:ro'
+ depends_on:
+ zitadel:
+ condition: 'service_healthy'
+ restart: false
+
+ login:
+ restart: 'unless-stopped'
+ networks:
+ - 'backend'
+ image: 'ghcr.io/zitadel/login:main'
+ environment:
+ - ZITADEL_API_URL=http://zitadel:8080
+ - CUSTOM_REQUEST_HEADERS=Host:127.0.0.1.sslip.io
+ - NEXT_PUBLIC_BASE_PATH="/ui/v2/login"
+ user: "${UID:-1000}"
+ volumes:
+ - './.env-file:/.env-file:ro'
+ depends_on:
+ zitadel:
+ condition: 'service_healthy'
+ restart: false
+
+ traefik:
+ restart: 'unless-stopped'
+ networks:
+ - 'backend'
+ image: "traefik:latest"
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - "./example-traefik.yaml:/etc/traefik/traefik.yaml"
+ depends_on:
+ zitadel:
+ condition: 'service_healthy'
+ login:
+ condition: 'service_started'
networks:
- zitadel:
+ storage:
+ backend:
volumes:
data:
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml
index c16f74a46d..a3af425172 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml
@@ -4,66 +4,37 @@ log:
accessLog: {}
entrypoints:
- web:
- address: ":80"
-
websecure:
address: ":443"
-tls:
- stores:
- default:
- # generates self-signed certificates
- defaultCertificate:
-
providers:
file:
filename: /etc/traefik/traefik.yaml
http:
- middlewares:
- zitadel:
- headers:
- isDevelopment: false
- allowedHosts:
- - 'my.domain'
- customRequestHeaders:
- authority: 'my.domain'
- redirect-to-https:
- redirectScheme:
- scheme: https
- port: 443
- permanent: true
-
routers:
- # Redirect HTTP to HTTPS
- router0:
+ login:
entryPoints:
- - web
- middlewares:
- - redirect-to-https
- rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)'
- service: zitadel
- # The actual ZITADEL router
- router1:
+ - websecure
+ service: login
+ rule: 'Host(`127.0.0.1.sslip.io`) && PathPrefix(`/ui/v2/login`)'
+ tls: {}
+ zitadel:
entryPoints:
- websecure
service: zitadel
- middlewares:
- - zitadel
- rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)'
- tls:
- domains:
- - main: "my.domain"
- sans:
- - "*.my.domain"
- - "my.domain"
+ rule: 'Host(`127.0.0.1.sslip.io`) && !PathPrefix(`/ui/v2/login`)'
+ tls: {}
- # Add the service
services:
+ login:
+ loadBalancer:
+ servers:
+ - url: http://login:3000
+ passHostHeader: true
zitadel:
loadBalancer:
servers:
- # h2c is the scheme for unencrypted HTTP/2
- url: h2c://zitadel:8080
passHostHeader: true
+
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml
index 392bf1148e..fadd39373d 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml
@@ -1,26 +1,29 @@
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml
-Log:
- Level: 'info'
-# Make ZITADEL accessible over HTTPs, not HTTP
ExternalSecure: true
-ExternalDomain: my.domain
+ExternalDomain: 127.0.0.1.sslip.io
ExternalPort: 443
+# Traefik terminates TLS. Inside the Docker network, we use plain text.
+TLS.Enabled: false
+
# If not using the docker compose example, adjust these values for connecting ZITADEL to your PostgreSQL
Database:
postgres:
Host: 'db'
Port: 5432
Database: zitadel
- User:
- SSL:
- Mode: 'disable'
- Admin:
- SSL:
- Mode: 'disable'
+ User.SSL.Mode: 'disable'
+ Admin.SSL.Mode: 'disable'
-LogStore:
- Access:
- Stdout:
- Enabled: true
+# By default, ZITADEL should redirect to /ui/v2/login
+OIDC:
+ DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2
+ DefaultLogoutURLV2: "/ui/v2/login/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2
+SAML.DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
+
+# Access logs allow us to debug Network issues
+LogStore.Access.Stdout.Enabled: true
+
+# Skipping the MFA init step allows us to immediately authenticate at the console
+DefaultInstance.LoginPolicy.MfaInitSkipLifetime: "0s"
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml
index 804e3d18d8..be63164ced 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml
@@ -1,8 +1,12 @@
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml
FirstInstance:
+ PatPath: '/pat'
Org:
- Name: 'My Org'
+ # We want to authenticate immediately at the console without changing the password
Human:
- # use the loginname root@my-org.my.domain
- Username: 'root'
- Password: 'RootPassword1!'
+ PasswordChangeRequired: false
+ Machine:
+ Machine:
+ Username: 'login-container'
+ Name: 'Login Container'
+ Pat.ExpirationDate: '2029-01-01T00:00:00Z'
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx
index d5e3984568..88cd4c7700 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx
@@ -1,5 +1,5 @@
---
-title: A ZITADEL Load Balancing Example
+title: A Zitadel Load Balancing Example
---
import CodeBlock from '@theme/CodeBlock';
@@ -8,16 +8,16 @@ import ExampleTraefikSource from '!!raw-loader!./example-traefik.yaml'
import ExampleZITADELConfigSource from '!!raw-loader!./example-zitadel-config.yaml'
import ExampleZITADELSecretsSource from '!!raw-loader!./example-zitadel-secrets.yaml'
import ExampleZITADELInitStepsSource from '!!raw-loader!./example-zitadel-init-steps.yaml'
-import NoteInstanceNotFound from '../troubleshooting/_note_instance_not_found.mdx';
-With this example configuration, you create a near production environment for ZITADEL with [Docker Compose](https://docs.docker.com/compose/).
-
-The stack consists of three long-running containers:
-- A [Traefik](https://doc.traefik.io/traefik/) reverse proxy with upstream HTTP/2 enabled, issuing a self-signed TLS certificate.
-- A secure ZITADEL container configured for a custom domain. As we terminate TLS with Traefik, we configure ZITADEL for `--tlsMode external`.
+The stack consists of four long-running containers and a couple of short-lived containers:
+- A [Traefik](https://doc.traefik.io/traefik/) reverse proxy container with upstream HTTP/2 enabled, issuing a self-signed TLS certificate.
+- A Login container that is accessible via Traefik at `/ui/v2/login`
+- A Zitadel container that is accessible via Traefik at all other paths than `/ui/v2/login`.
- An insecure [PostgreSQL](https://www.postgresql.org/docs/current/index.html).
-The setup is tested against Docker version 20.10.17 and Docker Compose version v2.2.3
+The Traefik container and the login container call the Zitadel container via the internal Docker network at `h2c://zitadel:8080`
+
+The setup is tested against Docker version 28.0.4 and Docker Compose version v2.34.0
By executing the commands below, you will download the following files:
@@ -64,22 +64,11 @@ tr -dc A-Za-z0-9 ./zitadel-masterkey
export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)"
# Run the database and application containers
-docker compose up --detach
+docker compose up --detach --wait
```
-Make `127.0.0.1` available at `my.domain`. For example, this can be achieved with an entry `127.0.0.1 my.domain` in the `/etc/hosts` file.
-
-Open your favorite internet browser at [https://my.domain/ui/console/](https://my.domain/ui/console/).
-You can safely proceed, if your browser warns you about the insecure self-signed TLS certificate.
-This is the IAM admin users login according to your configuration in the [example-zitadel-init-steps.yaml](./example-zitadel-init-steps.yaml):
-- **username**: *root@my-org.my.domain*
-- **password**: *RootPassword1!*
+Open your favorite internet browser at https://127.0.0.1.sslip.io/ui/console?login_hint=zitadel-admin@zitadel.127.0.0.1.sslip.io.
+Your browser warns you about the insecure self-signed TLS certificate. As 127.0.0.1.sslip.io resolves to your localhost, you can safely proceed.
+Use the password *Password1!* to log in.
Read more about [the login process](/guides/integrate/login/oidc/login-users).
-
-
-
-## Troubleshooting
-
-You can connect to the database like this: `docker exec -it loadbalancing-example-db-1 psql --host localhost`
-For example, to show all login names: `docker exec -it loadbalancing-example-db-1 psql -d zitadel --host localhost -c 'select * from projections.login_names3'`