docs(self-hosting): add login to lb example (#9496)

# Which Problems Are Solved

We have no docs for self-hosting the login using the standard login as a
standalone docker container.

# How the Problems Are Solved

A common self-hosting case is to publish the login at the same domain as
Zitadel behind a reverse proxy.
That's why we extend the load balancing example.
We refocus the example from *making TLS work* to *running multiple
services behind the proxy and connect them using an internal network and
DNS*. I decided this together with @fforootd.

For authenticating with the login application, we have to set up a
service user and give it the role IAM_LOGIN_CLIENT. We do so in the
use-new-login "job" container as `zitadel setup` only supports Zitadel
users with the role IAM_ADMIN AFAIR.

The login application relies on a healthy Zitadel API on startup, which
is why we fix the containers readiness reports.

# Additional Changes

- We deploy the init and setup jobs independently, because this better
reflects our production recommendatinons.
It gives more control over the upgrade process.
- We use the ExternalDomain *127.0.0.1.sslip.io* instead of *my.domain*,
because this doesn't require changing the local DNS resolution by
changing */etc/hosts* for local tests.

# Testing

The commands in the preview docs use to the configuration files on main.
This is fine when the PR is merged but not for testing the PR.
Replace the used links to make them point to the PRs changed files.
Instead of the commands in the preview docs, use these: 

```bash
# Download the docker compose example configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml

# Download the Traefik example configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml

# Download and adjust the example configuration file containing standard configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml

# Download and adjust the example configuration file containing secret configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-secrets.yaml

# Download and adjust the example configuration file containing database initialization configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml

# A single ZITADEL instance always needs the same 32 bytes long masterkey
# Generate one to a file if you haven't done so already and pass it as environment variable
LC_ALL=C tr -dc '[:graph:]' </dev/urandom | head -c 32 > ./zitadel-masterkey
export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)"

# Run the database and application containers
docker compose up --detach --wait
```

# Additional Context

- Closes https://github.com/zitadel/DevOps/issues/111
- Depends on https://github.com/zitadel/typescript/pull/412
- Contributes to road map item
https://github.com/zitadel/zitadel/issues/9481
This commit is contained in:
Elio Bischof
2025-04-29 16:12:34 +02:00
committed by GitHub
parent d930a09cb0
commit c36b0ab2e2
6 changed files with 183 additions and 106 deletions

View File

@@ -0,0 +1 @@
.env-file

View File

@@ -1,48 +1,157 @@
services: services:
traefik: db:
image: postgres:17-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=postgres
networks: networks:
- 'zitadel' - 'storage'
image: "traefik:latest" healthcheck:
ports: test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"]
- "80:80" interval: 10s
- "443:443" timeout: 60s
retries: 5
start_period: 10s
volumes: volumes:
- "./example-traefik.yaml:/etc/traefik/traefik.yaml" - 'data:/var/lib/postgresql/data:rw'
zitadel: zitadel-init:
restart: 'always' restart: 'no'
networks: networks:
- 'zitadel' - 'storage'
image: 'ghcr.io/zitadel/zitadel:latest' 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: depends_on:
db: db:
condition: 'service_healthy' condition: 'service_healthy'
volumes: volumes:
- './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro' - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
- './example-zitadel-secrets.yaml:/example-zitadel-secrets.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' - './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro'
db: zitadel:
image: postgres:17-alpine restart: 'unless-stopped'
restart: always
environment:
- POSTGRES_USER=root
- POSTGRES_PASSWORD=postgres
networks: 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: 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 interval: 10s
timeout: 60s timeout: 60s
retries: 5 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: 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: networks:
zitadel: storage:
backend:
volumes: volumes:
data: data:

View File

@@ -4,66 +4,37 @@ log:
accessLog: {} accessLog: {}
entrypoints: entrypoints:
web:
address: ":80"
websecure: websecure:
address: ":443" address: ":443"
tls:
stores:
default:
# generates self-signed certificates
defaultCertificate:
providers: providers:
file: file:
filename: /etc/traefik/traefik.yaml filename: /etc/traefik/traefik.yaml
http: http:
middlewares:
zitadel:
headers:
isDevelopment: false
allowedHosts:
- 'my.domain'
customRequestHeaders:
authority: 'my.domain'
redirect-to-https:
redirectScheme:
scheme: https
port: 443
permanent: true
routers: routers:
# Redirect HTTP to HTTPS login:
router0:
entryPoints: entryPoints:
- web - websecure
middlewares: service: login
- redirect-to-https rule: 'Host(`127.0.0.1.sslip.io`) && PathPrefix(`/ui/v2/login`)'
rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)' tls: {}
service: zitadel zitadel:
# The actual ZITADEL router
router1:
entryPoints: entryPoints:
- websecure - websecure
service: zitadel service: zitadel
middlewares: rule: 'Host(`127.0.0.1.sslip.io`) && !PathPrefix(`/ui/v2/login`)'
- zitadel tls: {}
rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)'
tls:
domains:
- main: "my.domain"
sans:
- "*.my.domain"
- "my.domain"
# Add the service
services: services:
login:
loadBalancer:
servers:
- url: http://login:3000
passHostHeader: true
zitadel: zitadel:
loadBalancer: loadBalancer:
servers: servers:
# h2c is the scheme for unencrypted HTTP/2
- url: h2c://zitadel:8080 - url: h2c://zitadel:8080
passHostHeader: true passHostHeader: true

View File

@@ -1,26 +1,29 @@
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml # 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 ExternalSecure: true
ExternalDomain: my.domain ExternalDomain: 127.0.0.1.sslip.io
ExternalPort: 443 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 # If not using the docker compose example, adjust these values for connecting ZITADEL to your PostgreSQL
Database: Database:
postgres: postgres:
Host: 'db' Host: 'db'
Port: 5432 Port: 5432
Database: zitadel Database: zitadel
User: User.SSL.Mode: 'disable'
SSL: Admin.SSL.Mode: 'disable'
Mode: 'disable'
Admin:
SSL:
Mode: 'disable'
LogStore: # By default, ZITADEL should redirect to /ui/v2/login
Access: OIDC:
Stdout: DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2
Enabled: true 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"

View File

@@ -1,8 +1,12 @@
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml # All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml
FirstInstance: FirstInstance:
PatPath: '/pat'
Org: Org:
Name: 'My Org' # We want to authenticate immediately at the console without changing the password
Human: Human:
# use the loginname root@my-org.my.domain PasswordChangeRequired: false
Username: 'root' Machine:
Password: 'RootPassword1!' Machine:
Username: 'login-container'
Name: 'Login Container'
Pat.ExpirationDate: '2029-01-01T00:00:00Z'

View File

@@ -1,5 +1,5 @@
--- ---
title: A ZITADEL Load Balancing Example title: A Zitadel Load Balancing Example
--- ---
import CodeBlock from '@theme/CodeBlock'; 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 ExampleZITADELConfigSource from '!!raw-loader!./example-zitadel-config.yaml'
import ExampleZITADELSecretsSource from '!!raw-loader!./example-zitadel-secrets.yaml' import ExampleZITADELSecretsSource from '!!raw-loader!./example-zitadel-secrets.yaml'
import ExampleZITADELInitStepsSource from '!!raw-loader!./example-zitadel-init-steps.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 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.
The stack consists of three long-running containers: - A Login container that is accessible via Traefik at `/ui/v2/login`
- A [Traefik](https://doc.traefik.io/traefik/) reverse proxy with upstream HTTP/2 enabled, issuing a self-signed TLS certificate. - A Zitadel container that is accessible via Traefik at all other paths than `/ui/v2/login`.
- A secure ZITADEL container configured for a custom domain. As we terminate TLS with Traefik, we configure ZITADEL for `--tlsMode external`.
- An insecure [PostgreSQL](https://www.postgresql.org/docs/current/index.html). - 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: By executing the commands below, you will download the following files:
@@ -64,22 +64,11 @@ tr -dc A-Za-z0-9 </dev/urandom | head -c 32 > ./zitadel-masterkey
export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)" export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)"
# Run the database and application containers # 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://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.
Open your favorite internet browser at [https://my.domain/ui/console/](https://my.domain/ui/console/). Use the password *Password1!* to log in.
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@<span></span>my-org.my.domain*
- **password**: *RootPassword1!*
Read more about [the login process](/guides/integrate/login/oidc/login-users). Read more about [the login process](/guides/integrate/login/oidc/login-users).
<NoteInstanceNotFound/>
## 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'`