From c36b0ab2e2a0a2632cffae4fe6ac086a126bc2a8 Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Tue, 29 Apr 2025 16:12:34 +0200 Subject: [PATCH] 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:]' ./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 --- .../deploy/loadbalancing-example/.gitignore | 1 + .../loadbalancing-example/docker-compose.yaml | 153 +++++++++++++++--- .../example-traefik.yaml | 57 ++----- .../example-zitadel-config.yaml | 31 ++-- .../example-zitadel-init-steps.yaml | 12 +- .../loadbalancing-example.mdx | 35 ++-- 6 files changed, 183 insertions(+), 106 deletions(-) create mode 100644 docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore 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'`