diff --git a/docs/docs/self-hosting/deploy/.gitignore b/docs/docs/self-hosting/deploy/.gitignore
index 83754bbee4..aba9338c1f 100644
--- a/docs/docs/self-hosting/deploy/.gitignore
+++ b/docs/docs/self-hosting/deploy/.gitignore
@@ -1 +1 @@
-login-client-pat
+*.pat
\ No newline at end of file
diff --git a/docs/docs/self-hosting/deploy/_defaultuser.mdx b/docs/docs/self-hosting/deploy/_defaultuser.mdx
index aee45b83c0..885b5f9a3e 100644
--- a/docs/docs/self-hosting/deploy/_defaultuser.mdx
+++ b/docs/docs/self-hosting/deploy/_defaultuser.mdx
@@ -1,8 +1,6 @@
-Open your favorite internet browser and navigate to [http://localhost:8080/ui/console](http://localhost:8080/ui/console).
-This is the default IAM admin users login:
-- **username**: *zitadel-admin@zitadel.localhost*
-- **password**: *Password1!*
+Open your favorite internet browser and navigate to http://localhost:8080/ui/console?login_hint=zitadel-admin@zitadel.localhost.
+Enther the password *Password1!* to log in.
:::info
-
-In the above username, replace localhost with your configured external domain, if any. e.g. with *zitadel-admin@zitadel.sso.my.domain.tld*
+In the above login hint in the URL, replace localhost with your configured external domain, if any. e.g. with *zitadel-admin@zitadel.sso.my.domain.tld*
+:::
\ No newline at end of file
diff --git a/docs/docs/self-hosting/deploy/compose.mdx b/docs/docs/self-hosting/deploy/compose.mdx
index f47a33da16..8496a9b53f 100644
--- a/docs/docs/self-hosting/deploy/compose.mdx
+++ b/docs/docs/self-hosting/deploy/compose.mdx
@@ -5,61 +5,45 @@ sidebar_label: Docker Compose
import CodeBlock from '@theme/CodeBlock';
import DockerComposeSource from '!!raw-loader!./docker-compose.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 Disclaimer from './_disclaimer.mdx'
+import DefaultUser from './_defaultuser.mdx'
+import Next from './_next.mdx'
+import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx';
-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 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.3.2 and Docker Compose version v2.38.2
-By executing the commands below, you will download the following files:
+## Docker compose
+
+By executing the commands below, you will download the following file:
docker-compose.yaml
{DockerComposeSource}
-
- example-zitadel-config.yaml
- {ExampleZitadelConfigSource}
-
-
- example-zitadel-secrets.yaml
- {ExampleZitadelSecretsSource}
-
-
- example-zitadel-init-steps.yaml
- {ExampleZitadelInitStepsSource}
-
```bash
# Download the docker compose example configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/docker-compose.yaml
-# Download and adjust the example configuration file containing standard configuration.
-wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/example-zitadel-config.yaml
-
-# Download and adjust the example configuration file containing secret configuration.
-wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/example-zitadel-secrets.yaml
-
-# Download and adjust the example configuration file containing database initialization configuration.
-wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/example-zitadel-init-steps.yaml
-
-# Make sure you have the latest version of the images
+# Make sure you have the latest image versions
docker compose pull
-# Run the containers
+# Run the PostgreSQL database, the Zitadel API and the Zitadel login.
docker compose up
```
-Open your favorite internet browser at https://localhost/ui/console?login_hint=zitadel-admin@zitadel.localhost.
-Your browser warns you about the insecure self-signed TLS certificate. As localhost resolves to your local machine, you can safely proceed.
-Use the password *Password1!* to log in.
+
-Read more about [the login process](/guides/integrate/login/oidc/login-users).
\ No newline at end of file
+:::info
+If you ran these commands for an existing instance that still uses the login v1, [create a login client for it to the now running v2 login](/self-hosting/manage/login-client#create-login-client).
+Move the login client PAT to `./login-client.pat` and restart the login container.
+```bash
+docker compose restart login
+```
+Now, [enable the Login UI for all users](/self-hosting/manage/login-client#require-login-v2)
+:::
+
+
+
+
diff --git a/docs/docs/self-hosting/deploy/docker-compose.yaml b/docs/docs/self-hosting/deploy/docker-compose.yaml
index 23d651efc9..ba27bfb645 100644
--- a/docs/docs/self-hosting/deploy/docker-compose.yaml
+++ b/docs/docs/self-hosting/deploy/docker-compose.yaml
@@ -1,117 +1,98 @@
services:
-
- db:
- image: postgres:17-alpine
+ zitadel:
restart: unless-stopped
+ image: ghcr.io/zitadel/zitadel:latest
+ command: start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled
environment:
- - POSTGRES_USER=root
- - POSTGRES_PASSWORD=postgres
- networks:
- - 'storage'
+ ZITADEL_EXTERNALSECURE: false
+ ZITADEL_TLS_ENABLED: false
+ ZITADEL_DATABASE_POSTGRES_HOST: db
+ 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: postgres
+ ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres
+ ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable
+ # By configuring a login client, the setup job creates a user of type machine with the role IAM_LOGIN_CLIENT.
+ # It writes a PAT to the path specified in ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH.
+ # The PAT is passed to the login container via the environment variable ZITADEL_SERVICE_USER_TOKEN_FILE.
+ ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH: /current-dir/login-client.pat
+ ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: false
+ ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME: login-client
+ ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME: Automatically Initialized IAM_LOGIN_CLIENT
+ ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE: '2029-01-01T00:00:00Z'
+ ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: true
+ ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI: http://localhost:3000/ui/v2/login
+ ZITADEL_OIDC_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?authRequest=
+ ZITADEL_OIDC_DEFAULTLOGOUTURLV2: http://localhost:3000/ui/v2/login/logout?post_logout_redirect=
+ ZITADEL_SAML_DEFAULTLOGINURLV2: http://localhost:3000/ui/v2/login/login?samlRequest=
+ # By configuring a machine, the setup job creates a user of type machine with the role IAM_OWNER.
+ # It writes a personal access token (PAT) to the path specified in ZITADEL_FIRSTINSTANCE_PATPATH.
+ # The PAT can be used to provision resources with [Terraform](/docs/guides/manage/terraform-provider), for example.
+ ZITADEL_FIRSTINSTANCE_PATPATH: /current-dir/admin.pat
+ ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: admin
+ ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Automatically Initialized IAM_OWNER
+ ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE: 1
+
healthcheck:
- test: [ "CMD-SHELL", "pg_isready", "-d", "db_prod" ]
+ test:
+ - CMD
+ - /app/zitadel
+ - ready
interval: 10s
timeout: 60s
retries: 5
start_period: 10s
volumes:
- - 'data:/var/lib/postgresql/data:rw'
-
- zitadel-init:
- restart: 'no'
+ - .:/current-dir:delegated
+ ports:
+ - 8080:8080
+ - 3000:3000
networks:
- - 'storage'
- image: 'ghcr.io/zitadel/zitadel:v4.0.0-rc.2'
- command: [ init, --config, /example-zitadel-config.yaml, --config, /example-zitadel-secrets.yaml ]
+ - zitadel
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'
- image: 'ghcr.io/zitadel/zitadel:v4.0.0-rc.2'
- command: [ setup, --config, /current-dir/example-zitadel-config.yaml, --config, /current-dir/example-zitadel-secrets.yaml, --steps, /current-dir/example-zitadel-init-steps.yaml, --masterkey, MasterkeyNeedsToHave32Characters ]
- depends_on:
- zitadel-init:
- condition: 'service_completed_successfully'
- restart: false
- volumes:
- - '.:/current-dir:rw'
-
- zitadel:
- restart: 'unless-stopped'
- networks:
- - 'backend'
- - 'storage'
- labels:
- - "traefik.http.routers.zitadel.rule=!PathPrefix(`/ui/v2/login`)"
- - "traefik.http.routers.zitadel.tls=true" # Traefik uses a self-signed certificate
- - "traefik.http.services.zitadel.loadbalancer.passhostheader=true"
- - "traefik.http.services.zitadel.loadbalancer.server.scheme=h2c"
- - "traefik.http.services.zitadel.loadbalancer.server.port=8080"
- image: 'ghcr.io/zitadel/zitadel:v4.0.0-rc.2'
- command: [ start, --config, /example-zitadel-config.yaml, --config, /example-zitadel-secrets.yaml, --masterkey, MasterkeyNeedsToHave32Characters ]
- 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'
- healthcheck:
- test: [ "CMD", "/app/zitadel", "ready", "--config", "/example-zitadel-config.yaml", "--config", "/example-zitadel-secrets.yaml" ]
- interval: 10s
- timeout: 60s
- retries: 5
- start_period: 10s
+ condition: service_healthy
login:
- restart: 'unless-stopped'
- labels:
- - "traefik.http.routers.login.rule=PathPrefix(`/ui/v2/login`)"
- - "traefik.http.routers.login.tls=true" # Traefik uses a self-signed certificate
- - "traefik.http.services.login.loadbalancer.passhostheader=true"
- - "traefik.http.services.login.loadbalancer.server.port=3000"
- image: 'ghcr.io/zitadel/zitadel-login:v4.0.0-rc.2'
+ restart: unless-stopped
+ image: ghcr.io/zitadel/zitadel-login:latest
# If you can't use the network_mode service:zitadel, you can pass the environment variable CUSTOM_REQUEST_HEADERS=Host:localhost instead.
- network_mode: service:zitadel
environment:
- ZITADEL_API_URL=http://localhost:8080
- NEXT_PUBLIC_BASE_PATH=/ui/v2/login
- - ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client-pat
+ - ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client.pat
user: "${UID:-1000}"
+ network_mode: service:zitadel
volumes:
- - '.:/current-dir:ro'
- depends_on:
- zitadel-setup:
- condition: 'service_completed_successfully'
- restart: false
-
- traefik:
- image: traefik:latest
- command: --providers.docker --api.insecure=true --entrypoints.websecure.address=:443 --log.level=DEBUG --accesslog
- networks:
- - 'backend'
- ports:
- - "443:443"
- - "8080:8080"
- volumes:
- - /var/run/docker.sock:/var/run/docker.sock
+ - .:/current-dir:ro
depends_on:
zitadel:
- condition: 'service_healthy'
- login:
- condition: 'service_started'
+ condition: service_healthy
+ restart: false
+
+ db:
+ restart: unless-stopped
+ image: postgres:17-alpine
+ environment:
+ PGUSER: postgres
+ POSTGRES_PASSWORD: postgres
+ healthcheck:
+ test:
+ - CMD-SHELL
+ - pg_isready
+ - -d
+ - zitadel
+ - -U
+ - postgres
+ interval: 10s
+ timeout: 30s
+ retries: 5
+ start_period: 20s
+ networks:
+ - zitadel
networks:
- storage:
- backend:
-
-
-volumes:
- data:
+ zitadel:
diff --git a/docs/docs/self-hosting/deploy/example-zitadel-config.yaml b/docs/docs/self-hosting/deploy/example-zitadel-config.yaml
deleted file mode 100644
index baacd6cefe..0000000000
--- a/docs/docs/self-hosting/deploy/example-zitadel-config.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml
-
-ExternalSecure: true
-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'
-
-# 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/example-zitadel-init-steps.yaml b/docs/docs/self-hosting/deploy/example-zitadel-init-steps.yaml
deleted file mode 100644
index 373c6ae744..0000000000
--- a/docs/docs/self-hosting/deploy/example-zitadel-init-steps.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml
-FirstInstance:
- LoginClientPatPath: '/current-dir/login-client-pat'
- Org:
- # We want to authenticate immediately at the console without changing the password
- Human.PasswordChangeRequired: false
- LoginClient:
- Machine:
- Username: 'login-client'
- Name: 'Automatically Initialized IAM Login Client'
- Pat.ExpirationDate: '2029-01-01T00:00:00Z'
\ No newline at end of file
diff --git a/docs/docs/self-hosting/deploy/example-zitadel-secrets.yaml b/docs/docs/self-hosting/deploy/example-zitadel-secrets.yaml
deleted file mode 100644
index 242da43b24..0000000000
--- a/docs/docs/self-hosting/deploy/example-zitadel-secrets.yaml
+++ /dev/null
@@ -1,12 +0,0 @@
-# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml
-
-# If not using the docker compose example, adjust these values for connecting ZITADEL to your PostgreSQL
-Database:
- postgres:
- User:
- # If the user doesn't exist already, it is created
- Username: 'zitadel_user'
- Password: 'zitadel'
- Admin:
- Username: 'root'
- Password: 'postgres'
diff --git a/docs/docs/self-hosting/manage/login-client.mdx b/docs/docs/self-hosting/manage/login-client.mdx
new file mode 100644
index 0000000000..977fa4b8c6
--- /dev/null
+++ b/docs/docs/self-hosting/manage/login-client.mdx
@@ -0,0 +1,47 @@
+---
+title: Connect your Self-Hosted Login UI to Zitadel
+sidebar_label: Create a Login Client
+---
+
+To enable your self-hosted Login UI to connect to the Zitadel API, it needs a token for a user with the IAM_LOGIN_CLIENT role.
+On new installations, the Zitadel setup job can be configured to automatically write a Personal Access Token (PAT) for the login client.
+Check out [one of the deployment examples](https://zitadel.com/docs/self-hosting/deploy/overview) to learn how to do this.
+
+However, if you want to replace the v1 login of an existing installation by a self-hosted v2 login, the setup job won't execute these steps.
+In that case, you can create a new PAT for the login client manually.
+
+## Create a Login Client User{#create-login-client}
+
+In the following URLs, replace the base URL and the user ID according to your environment.
+
+1. Create a new machine user, for example at http://localhost:8080/ui/console/users/create-machine
+2. Create a PAT, for example at http://localhost:8080/ui/console/users/332169800719532035?new=true&id=pat
+3. Save the PAT to a file, for example `/path/on/your/host/login-client.pat`
+4. Make sure the user has the `Iam Login Client` role (internally called `IAM_LOGIN_CLIENT`), for example at http://localhost:8080/ui/console/instance/members
+
+# Configure the Login UI
+
+Make sure your Login UI has the environment variable `ZITADEL_SERVICE_USER_TOKEN` set with your PAT.
+If you run the Login UI with Docker, you can also mount the file into the container and reference it by passing the environment variable `ZITADEL_SERVICE_USER_TOKEN_FILE`.
+For example:
+
+```bash
+docker run -p 3000:3000 -v /path/on/your/host/login-client.pat:/path/in/container/login-client.pat:ro -e ZITADEL_SERVICE_USER_TOKEN_FILE=/path/in/container/login-client.pat ghcr.io/zitadel/zitadel-login:latest
+```
+
+# Enable the Login UI for all users{#require-login-v2}
+
+:::caution
+Before doing this, make sure you have a working PAT for an Iam Owner user.
+In case something goes wrong and you lock yourself out from the login screen, you can revert the changes.
+Create a machine user PAT like you created the [login client PAT above](#create-login-client), but give the user the Iam Owner role (internally called `IAM_OWNER`).
+:::
+
+Enable the `Login V2` feature flag, for example at the bottom of http://localhost:8080/ui/console/instance?id=features.
+Enter the base URI of your Login UI, for example `http://localhost:3000/ui/v2/login`.
+
+# Test
+
+That's it!
+Click your users avatar in the top right corner of the console and select `Log in With Another Account`.
+You should see the new Login UI.
diff --git a/docs/sidebars.js b/docs/sidebars.js
index acca7b1659..aa11908a44 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -1094,6 +1094,7 @@ module.exports = {
items: [
"self-hosting/manage/production",
"self-hosting/manage/productionchecklist",
+ "self-hosting/manage/login-client",
"self-hosting/manage/configure/configure",
{
type: "category",