Skip to content

Commit e35c2ff

Browse files
committed
Fix self-hosted Zitadel startup flow
1 parent f306aee commit e35c2ff

3 files changed

Lines changed: 80 additions & 20 deletions

File tree

ansible/roles/idp_zitadel/defaults/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ terrarium_zitadel_tf_dir: "{{ terrarium_zitadel_dir }}/terraform"
66
terrarium_zitadel_outputs_path: /etc/terrarium/zitadel-apps.json
77
terrarium_zitadel_api_port: 8082
88
terrarium_zitadel_login_port: 8083
9-
terrarium_zitadel_version: v4.11.0
9+
terrarium_zitadel_version: v4.13.0
1010
terrarium_zitadel_postgres_image: postgres:17.2-alpine
1111
terrarium_zitadel_tofu_image: ghcr.io/opentofu/opentofu:1.10.6
1212
terrarium_zitadel_provider_version: 2.12.3

ansible/roles/idp_zitadel/templates/docker-compose.yml.j2

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
1+
x-zitadel-db-env: &zitadel-db-env
2+
ZITADEL_DATABASE_POSTGRES_DSN: "postgresql://postgres:{{ terrarium_zitadel_postgres_password }}@postgres:5432/zitadel?sslmode=disable"
3+
14
services:
25
zitadel-init:
36
image: ghcr.io/zitadel/zitadel:{{ terrarium_zitadel_version }}
47
restart: "no"
58
user: "0"
69
command: init
7-
environment: &zitadel-env
10+
environment:
11+
<<: *zitadel-db-env
12+
depends_on:
13+
postgres:
14+
condition: service_healthy
15+
networks:
16+
- zitadel
17+
18+
zitadel-setup:
19+
image: ghcr.io/zitadel/zitadel:{{ terrarium_zitadel_version }}
20+
restart: "no"
21+
user: "0"
22+
command: setup --masterkey "{{ terrarium_zitadel_masterkey }}"
23+
environment:
24+
<<: *zitadel-db-env
825
ZITADEL_PORT: "8080"
926
ZITADEL_EXTERNALDOMAIN: "{{ terrarium_auth_domain }}"
1027
ZITADEL_EXTERNALPORT: "443"
1128
ZITADEL_EXTERNALSECURE: "true"
1229
ZITADEL_TLS_ENABLED: "false"
13-
ZITADEL_DATABASE_POSTGRES_DSN: "postgresql://postgres:{{ terrarium_zitadel_postgres_password }}@postgres:5432/zitadel?sslmode=disable"
1430
ZITADEL_LOGSTORE_ACCESS_STDOUT_ENABLED: "true"
1531
ZITADEL_DEFAULTINSTANCE_ORG_HUMAN_USERNAME: "admin"
1632
ZITADEL_DEFAULTINSTANCE_ORG_HUMAN_FIRSTNAME: "Terrarium"
@@ -34,30 +50,29 @@ services:
3450
ZITADEL_OIDC_DEFAULTLOGINURLV2: "https://{{ terrarium_auth_domain }}/ui/v2/login/login?authRequest="
3551
ZITADEL_OIDC_DEFAULTLOGOUTURLV2: "https://{{ terrarium_auth_domain }}/ui/v2/login/logout?post_logout_redirect="
3652
ZITADEL_SAML_DEFAULTLOGINURLV2: "https://{{ terrarium_auth_domain }}/ui/v2/login/login?samlRequest="
37-
healthcheck:
38-
test:
39-
- CMD
40-
- /app/zitadel
41-
- ready
42-
interval: 10s
43-
timeout: 30s
44-
retries: 12
45-
start_period: 20s
53+
volumes:
54+
- "{{ terrarium_zitadel_bootstrap_dir }}:/zitadel/bootstrap:rw"
4655
depends_on:
4756
postgres:
4857
condition: service_healthy
49-
volumes:
50-
- "{{ terrarium_zitadel_bootstrap_dir }}:/zitadel/bootstrap:rw"
58+
zitadel-init:
59+
condition: service_completed_successfully
5160
networks:
5261
- zitadel
5362

5463
zitadel-api:
5564
image: ghcr.io/zitadel/zitadel:{{ terrarium_zitadel_version }}
5665
restart: unless-stopped
5766
user: "0"
58-
command: start-from-setup --masterkey "{{ terrarium_zitadel_masterkey }}"
67+
command: start --masterkey "{{ terrarium_zitadel_masterkey }}"
5968
environment:
60-
<<: *zitadel-env
69+
<<: *zitadel-db-env
70+
ZITADEL_PORT: "8080"
71+
ZITADEL_EXTERNALDOMAIN: "{{ terrarium_auth_domain }}"
72+
ZITADEL_EXTERNALPORT: "443"
73+
ZITADEL_EXTERNALSECURE: "true"
74+
ZITADEL_TLS_ENABLED: "false"
75+
ZITADEL_LOGSTORE_ACCESS_STDOUT_ENABLED: "true"
6176
healthcheck:
6277
test:
6378
- CMD
@@ -72,6 +87,8 @@ services:
7287
condition: service_healthy
7388
zitadel-init:
7489
condition: service_completed_successfully
90+
zitadel-setup:
91+
condition: service_completed_successfully
7592
ports:
7693
- "127.0.0.1:{{ terrarium_zitadel_api_port }}:8080"
7794
volumes:

scripts/terrarium-zitadel-sync.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,66 @@
11
import { existsSync } from "node:fs";
2-
import { configString, loadConfig, readJsonFile, runText, writeIfChanged } from "./lib/common";
2+
import { configString, loadConfig, readJsonFile, runAllowFailure, runText, writeIfChanged } from "./lib/common";
33

44
const PREFIX = "terrariumctl idp sync";
55
const DEFAULT_CONFIG_PATH = process.env.TERRARIUM_CONFIG_PATH ?? "/etc/terrarium/config.yaml";
6+
const DEFAULT_ZITADEL_DIR = "/var/lib/terrarium/zitadel";
67
const DEFAULT_BOOTSTRAP_DIR = "/var/lib/terrarium/zitadel/bootstrap";
78
const DEFAULT_TF_DIR = "/var/lib/terrarium/zitadel/terraform";
89
const DEFAULT_OUTPUTS_PATH = "/etc/terrarium/zitadel-apps.json";
910
const DEFAULT_TOFU_IMAGE = "ghcr.io/opentofu/opentofu:1.10.6";
11+
const WAIT_INTERVAL_MS = 5000;
12+
const WAIT_ATTEMPTS = 36;
1013

1114
async function dockerRun(args: string[]): Promise<string> {
1215
return await runText(["docker", ...args], PREFIX);
1316
}
1417

18+
async function waitForFile(path: string, label: string): Promise<void> {
19+
for (let attempt = 0; attempt < WAIT_ATTEMPTS; attempt += 1) {
20+
if (existsSync(path)) {
21+
return;
22+
}
23+
await Bun.sleep(WAIT_INTERVAL_MS);
24+
}
25+
throw new Error(`timed out waiting for ${label}: ${path}`);
26+
}
27+
28+
async function waitForApiReady(stackDir: string): Promise<void> {
29+
let lastError = "";
30+
for (let attempt = 0; attempt < WAIT_ATTEMPTS; attempt += 1) {
31+
const result = await runAllowFailure(
32+
[
33+
"docker",
34+
"compose",
35+
"--project-name",
36+
"terrarium-zitadel",
37+
"-f",
38+
`${stackDir}/docker-compose.yml`,
39+
"exec",
40+
"-T",
41+
"zitadel-api",
42+
"/app/zitadel",
43+
"ready"
44+
],
45+
{ cwd: stackDir }
46+
);
47+
if (result.exitCode === 0) {
48+
return;
49+
}
50+
lastError = result.stderr.trim() || result.stdout.trim() || "container is not ready yet";
51+
await Bun.sleep(WAIT_INTERVAL_MS);
52+
}
53+
throw new Error(`timed out waiting for ZITADEL API readiness: ${lastError}`);
54+
}
55+
1556
export async function idpSyncCmd(configPath = DEFAULT_CONFIG_PATH): Promise<void> {
1657
const config = loadConfig(configPath, PREFIX);
1758
if (configString(config, "terrarium_idp_mode") !== "zitadel_self_hosted") {
1859
return;
1960
}
2061

2162
const authDomain = configString(config, "terrarium_auth_domain");
63+
const zitadelDir = configString(config, "terrarium_zitadel_dir", DEFAULT_ZITADEL_DIR);
2264
const bootstrapDir = configString(config, "terrarium_zitadel_bootstrap_dir", DEFAULT_BOOTSTRAP_DIR);
2365
const tfDir = configString(config, "terrarium_zitadel_tf_dir", DEFAULT_TF_DIR);
2466
const outputsPath = configString(config, "terrarium_zitadel_outputs_path", DEFAULT_OUTPUTS_PATH);
@@ -27,13 +69,14 @@ export async function idpSyncCmd(configPath = DEFAULT_CONFIG_PATH): Promise<void
2769
if (!authDomain) {
2870
throw new Error("terrarium_auth_domain is empty");
2971
}
30-
if (!existsSync(`${bootstrapDir}/admin-sa.json`)) {
31-
throw new Error(`missing bootstrap machine key: ${bootstrapDir}/admin-sa.json`);
32-
}
3372
if (!existsSync(tfDir)) {
3473
throw new Error(`terraform directory not found: ${tfDir}`);
3574
}
3675

76+
await waitForFile(`${bootstrapDir}/admin-sa.json`, "bootstrap machine key");
77+
await waitForFile(`${bootstrapDir}/login-client.pat`, "login client PAT");
78+
await waitForApiReady(zitadelDir);
79+
3780
const commonArgs = [
3881
"run",
3982
"--rm",

0 commit comments

Comments
 (0)