ZITADEL v4 单节点 Docker Compose 部署:服务架构、TLS 覆盖层组合与 Traefik 路由不变量
【免费下载链接】zitadelZITADEL - Identity infrastructure, simplified for you.项目地址: https://gitcode.com/GitHub_Trending/zi/zitadel
本文围绕仓库中deploy/compose目录的 AI 协作指令文档 AGENTS.md 展开,系统讲解 ZITADEL 官方提供的生产级单节点 Docker Compose 部署方案:四个核心服务与两个可选 profile 服务的组合方式、四种 Traefik TLS 模式的独立可组合性、基于h2c的 gRPC/REST 统一路由模型,以及一套保证路由正确性的 CI 冒烟测试流水线。读完后你将理解该部署栈中每个 Compose 文件的职责边界、必须遵守的部署不变量(external domain/port/secure 三件套一致性),并能直接使用仓库给出的命令启动、校验和测试整套栈。
一、部署栈总览:四个核心服务 + 两个可选服务
AGENTS.md 对该目录的定位非常明确:这是一套"production-aware"的单节点 Docker Compose 部署。整套栈由以下服务构成:
| 类型 | 服务 | 说明 |
|---|---|---|
| 核心 | zitadel-api | Go API 服务,监听容器内 8080 |
| 核心 | zitadel-login | Next.js 登录 UI,监听容器内 3000 |
| 核心 | postgres | 数据持久化 |
| 核心 | proxy | Traefik 反向代理,发布 80/443 |
| 可选 | redis | 通过cacheprofile 启用 |
| 可选 | otel-collector | 通过observabilityprofile 启用 |
流量走向可以从 README.md 的架构图直接看出:所有浏览器流量先经过 Traefik(80/443),再按路由规则分发到zitadel-login(登录 UI)或zitadel-api(API/协议端点),API 再访问 PostgreSQL:
┌─────────────────────────┐ Browser ──► │ Traefik (proxy) │ │ Port 80 / 443 │ └───┬──────────┬──────────┘ │ │ ┌──────────▼──┐ ┌───▼──────────┐ │ zitadel-api │ │ zitadel-login │ │ Go :8080 │ │ Next.js :3000 │ └──────┬───────┘ └──────────────┘ │ ┌──────▼───────┐ │ PostgreSQL │ └──────────────┘两个关键架构决策直接体现在 docker-compose.yml 中:
- Login 与 API 分离部署。
zitadel-login是独立的 Next.js 进程(ghcr.io/zitadel/zitadel-login:${ZITADEL_VERSION}),通过ZITADEL_API_URL: http://zitadel-api:8080在服务端直连 API,而不是经浏览器跨域访问。它同时挂载zitadel-bootstrap卷(只读)以读取 bootstrap PAT。 - 健康检查驱动的启动顺序。
postgres就绪 →zitadel-api健康(/app/zitadel ready探针)→zitadel-login健康(/ui/v2/login/healthy探针)→proxy最后启动,全部通过depends_on.condition: service_healthy串联(见 docker-compose.yml 与 L83-L85)。
redis与otel-collector均声明在profiles之下(L205-L228),不显式启用 profile 时不会影响默认栈——这是文档明确列出的不变量之一。
二、文件结构与职责边界
AGENTS.md 第 2 节给出一张文件约定表,README.md 对其有相同口径的补充。两者合并后的完整文件职责如下:
| 文件 | 用途 | 修改安全性 |
|---|---|---|
| docker-compose.yml | 基础栈——所有模式都从它起步,必须能单独配合.env.example工作 | 可以,但需谨慎 |
| docker-compose.mode-letsencrypt.yml | TLS 覆盖层:ACME HTTP challenge,自带letsencrypt卷 | 可以 |
| docker-compose.mode-external-tls.yml | TLS 覆盖层:上游负载均衡器终结 TLS,启用转发头信任 | 可以 |
| docker-compose.mode-local-tls.yml | TLS 覆盖层:自签证书,挂载./certs/与traefik-local-tls.yml | 可以 |
| docker-compose.prodlike.yml | init/setup/start 三段拆分覆盖层,使用 YAML 锚点共享 DB 环境 | 可以 |
| docker-compose.test.yml | CI 测试覆盖层:本地构建镜像、不暴露直连端口 | 可以 |
.env.example | 面向用户的配置模板,版本号在此锁定 | 可以——升级版本时需同步更新 |
.env.test | CI 专用配置,仅内部使用 | 内部 |
| otel-collector-config.yaml | OTEL Collector 管道配置 | 可以 |
| traefik-local-tls.yml | 本地 TLS 的 Traefik 动态配置 | 可以 |
| project.json | NX 工程定义(test-config/test-run/test-e2e/test/test-full/stop) | NX 自动管理 |
这里有一条值得牢记的组合性规则:每个docker-compose.mode-*.yml覆盖层必须能够"只与基础文件组合"独立工作,禁止把模式相关配置合并进基础 docker-compose.yml。NX 的test-config目标正是通过逐组合执行docker compose config --quiet来机械化验证这一点的(见 project.json)。
三、Traefik 路由规则:优先级表与 h2c 统一转发模型
这是该部署栈最核心的技术点。Traefik 通过 Docker label 为${ZITADEL_DOMAIN}建立四组路由(webHTTP 与websecureHTTPS 两个入口点拥有完全相同的规则集):
| 优先级 | 规则 | 目标 | 中间件 |
|---|---|---|---|
| 400 | Path(/) | zitadel-login | replacepath=/ui/v2/login/ |
| 250 | PathPrefix(/ui/v2/login) | zitadel-login | — |
| 200 | PathPrefix(/api) | zitadel-api | stripprefix=/api |
| 100 | 其余一切(OIDC、SAML、gRPC、gRPC-web、API v2 REST 等) | zitadel-api(h2c后端) | — |
对应源码见 docker-compose.yml(API 侧 label)与 L159-L183(Login 侧 label)。有三个设计要点需要展开:
3.1 为什么不需要独立的 gRPC 路由器
基础文件中有一条显式注释(L96-L97):
# Note: no dedicated gRPC router needed. All gRPC and Connect-RPC traffic # is handled by the catch-all router below since the backend already uses h2c.关键在于 service 级 labelloadbalancer.server.scheme=h2c(L91):Traefik 的后端连接统一走明文 HTTP/2,因此gRPC(proto/json over h2c)、gRPC-web、Connect-RPC、以及 gRPC-gateway 的 REST/JSON全部由同一条 catch-all 路由透传,无需按Content-Type: application/grpc*头区分。/api前缀则通过zitadel-strip-api中间件剥离(stripprefix.prefixes=/api、forceSlash=false,L93-L94)后进入同一条 catch-all 路径。
这一设计有测试级证据:tests/wiring.spec.ts 头部注释列出了完整的"API 协议 × 传输"矩阵——V1 的AdminService/Healthz覆盖 REST JSON(HTTP/1.1 + h2c)、gRPC-web proto/json(HTTP/1.1)、gRPC proto/json(h2c)共 6 种组合;V2 的SessionService/ListSessions则覆盖 gRPC-gateway REST 映射、Connect unary(application/json、application/proto)与原生 gRPC(application/grpc+proto/grpc+json,h2c only)各 × HTTP/1.1 与 h2c。所有流量都从 Traefik 的 catch-all 路由器进入。
3.2/api别名与规范路径并存
不变量明确写着:/api前缀只是便捷别名,规范的路由根路径(/.well-known/、/oauth/v2/等)必须保留。两者并存的原因在 README.md 中给出:
/api别名存在是为 DX——工具可以使用https://auth.example.com/api/...这样的统一前缀;- OIDC/SAML 协议要求
/.well-known/openid-configuration、/oauth/v2/...等端点保持在根路径,任何"只允许/api"的重写模型都会破坏协议合规性。
因此文档将"严格/api-only 重写模型"列为被明确否决的替代方案(见第六节)。冒烟检查命令curl -sS http://localhost:8888/.well-known/openid-configuration(来自 AGENTS.md 第 5 节)正是直接命中这条根路径规范端点来验证的。
3.3 根路径改写与登录 UI
Path(/)的 400 优先级路由器把站点根路径经replacepath中间件改写到/ui/v2/login/(L159),保证用户直接访问域名根时落到新版登录 UI。Login 容器同时设置NEXT_PUBLIC_BASE_PATH: /ui/v2/login与CUSTOM_REQUEST_HEADERS: Host:${ZITADEL_DOMAIN},X-Forwarded-Proto:${ZITADEL_PUBLIC_SCHEME},前者决定 Next.js 的资源基路径,后者用于 Login 服务端代理请求 API 时携带正确的 Host 头(API 按 Host 做多实例解析,见下节)。
四、External 三件套不变量:最常见的部署故障根源
AGENTS.md 第 3 节与 README.md 都把同一条不变量标注为"单一最常见部署问题":
ZITADEL_EXTERNALDOMAIN、ZITADEL_EXTERNALPORT、ZITADEL_EXTERNALSECURE必须与实际公网端点一致。不一致会导致 "Instance not found" 错误。
机制可以从基础文件的环境变量推导:API 收到请求后按Host头(加上端口/协议语义)去实例表中查找逻辑实例;如果容器内声明的外部 URL 与用户浏览器实际访问的 URL 对不上(例如域名写auth.example.com而端口写成8080,或ZITADEL_EXTERNALSECURE与 TLS 终结方式不匹配),查找必然失败。
这三个变量在基础文件中注入(docker-compose.yml):
ZITADEL_EXTERNALDOMAIN: ${ZITADEL_DOMAIN} ZITADEL_EXTERNALPORT: ${ZITADEL_EXTERNALPORT} ZITADEL_EXTERNALSECURE: ${ZITADEL_EXTERNALSECURE}而 Login UI 的基址(ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI、ZITADEL_OIDC_DEFAULTLOGINURLV2、ZITADEL_OIDC_DEFAULTLOGOUTURLV2、ZITADEL_SAML_DEFAULTLOGINURLV2)由ZITADEL_PUBLIC_SCHEME+ 上述变量拼接而成(L52-L55)。TLS 覆盖层则会整体覆盖这组 URL 为https://${ZITADEL_DOMAIN}/...并把ZITADEL_EXTERNALPORT硬置为443、ZITADEL_EXTERNALSECURE: true——例如 docker-compose.mode-letsencrypt.yml 中对zitadel-api与zitadel-login的 environment 覆盖。
五、四种运行模式详解
5.1 本地开发模式(基础文件单独使用)
AGENTS.md 第 5 节给出的启动命令:
cp .env.example .env && docker compose up -d --wait.env.example 文件头部明确标注"INSECURE DEFAULTS — for local development only",上线前必须更换ZITADEL_MASTERKEY、POSTGRES_ADMIN_PASSWORD等值。几个关键取值:
ZITADEL_DOMAIN=localhost、PROXY_HTTP_PUBLISHED_PORT=8080、ZITADEL_EXTERNALPORT=8080、ZITADEL_EXTERNALSECURE=false——三者一致地描述"HTTP、8080 端口、localhost"这一公网端点;ZITADEL_MASTERKEY必须恰好 32 字符(示例值MasterkeyNeedsToHave32Characters即满足);ZITADEL_DATABASE_POSTGRES_DSN=postgresql://postgres:postgres@postgres:5432/zitadel?sslmode=disable——注释特别说明:配置 DSN 后 ZITADEL 不会创建/切换非特权用户,DSN 中的用户将被直接使用,生产环境建议配置为仅具备所需权限的非超级用户角色;- 镜像版本在此统一锁定(
ZITADEL_VERSION=v4.16.0、TRAEFIK_IMAGE=traefik:v3.7.7、POSTGRES_IMAGE=postgres:17.10-alpine、REDIS_IMAGE=redis:7.4.9-alpine、OTEL_COLLECTOR_IMAGE=otel/opentelemetry-collector-contrib:0.156.0),升级流程是"改.env版本号 →pull→up -d --wait"。
基础文件中 API 的启动命令是start-from-init --masterkey "${ZITADEL_MASTERKEY}"(L35),即初始化与启动合并为单容器完成;bootstrap 相关环境变量(ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH、ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_*、LOGIN_CLIENT_PAT_EXPIRATION)会在初始化时创建名为login-client的 IAM 机器用户,并把其 PAT 写入共享卷zitadel-bootstrap的/zitadel/bootstrap/login-client.pat,供 Login 容器以ZITADEL_SERVICE_USER_TOKEN_FILE消费(L128-L131)。
5.2 Let's Encrypt 模式
docker compose --env-file .env -f docker-compose.yml -f docker-compose.mode-letsencrypt.yml up -d --waitdocker-compose.mode-letsencrypt.yml 在基础命令之上追加了 ACME 配置(--certificatesresolvers.le.acme.httpchallenge=true,使用web入口点完成 HTTP challenge),ports用 YAML 合并键!override覆盖为80:80与443:443,并新增letsencrypt命名卷存放acme.json。LETSENCRYPT_EMAIL在 .env.example 中提供。
5.3 External TLS 模式(上游 LB 终结)
# 与 Let's Encrypt 模式同样的 -f 组合方式,换成 mode-external-tls 覆盖层docker-compose.mode-external-tls.yml 的要点:
- 只发布
80:80(443 由上游负载均衡器终结); - 通过
--entrypoints.web.forwardedHeaders.trustedIPs=${TRAEFIK_TRUSTED_IPS}信任来自上游的X-Forwarded-*头,TRAEFIK_TRUSTED_IPS在 .env.example 中默认设置为三大私有网段 CIDR(10.0.0.0/8,172.16.0.0/12,192.168.0.0/16),注释提醒需改成实际的 LB/反向代理网段; - API 侧同样覆盖为
EXTERNALPORT=443/EXTERNALSECURE=true与https://登录 URL。
5.4 Local TLS 模式(自签证书)
docker-compose.mode-local-tls.yml 的差异:
- 挂载
./certs目录(只读)与 traefik-local-tls.yml 作为 Traefik 动态配置文件; - 动态配置内容极其简洁——一个默认 TLS store 指向
/certs/local.crt与/certs/local.key:
tls: stores: default: defaultCertificate: certFile: /certs/local.crt keyFile: /certs/local.key- 启用
web入口点到websecure的强制 HTTPS 重定向(--entrypoints.web.http.redirections.entrypoint.to=websecure); - 80/443 双端口经
!override发布。
5.5 Production-like 模式:init / setup / start 三段拆分
docker compose --env-file .env -f docker-compose.yml -f docker-compose.prodlike.yml up -d --waitdocker-compose.prodlike.yml 把"一个长驻容器"拆成三个短生命周期步骤,便于在故障时单独重放某一段:
zitadel-init:command: init,restart: "no",只做数据库初始化;zitadel-setup:command: setup --masterkey ...,执行首实例引导(创建组织、login-client机器用户与 PAT 等,环境变量组与基础文件的zitadel-api相同,通过 YAML 锚点&zitadel-db-env/*zitadel-db-env共享 DSN);依赖zitadel-init以service_completed_successfully完成后才启动;zitadel-api:command从start-from-init覆盖为start --masterkey ...,仅承担日常运行,依赖 setup 成功完成。
这与单容器模式的start-from-init语义等价,只是把阶段边界显式化。
5.6 可选 profile:Redis 缓存与 OTEL 可观测性
两者默认关闭,启用方式是给docker compose up追加--profile:
cache:.env.example 中ZITADEL_CACHES_CONNECTORS_REDIS_ENABLED=false、ZITADEL_CACHES_CONNECTORS_REDIS_URL=redis://redis:6379/0,以及三个空的*_CONNECTOR变量;启用时需打开开关并把 connector 切到 redis。基础文件中的redis服务还显式关闭了持久化(--save ""、--appendonly no),语义是纯缓存。observability:API 默认ZITADEL_INSTRUMENTATION_TRACE_EXPORTER_TYPE=none;启用后 collector 在 4317(gRPC)/4318(HTTP)接收 trace。otel-collector-config.yaml 的管道是otlp → batch → debug(trace 默认打到 stdout),要转发到自有后端(Grafana Tempo、Jaeger、OpenObserve 等)则取消otlpexporter 的注释并设置OTEL_BACKEND_ENDPOINT,同时把exporters改为[debug, otlp]。此外 .env.example 预留了一组 Login 侧 OTEL 变量(LOGIN_OTEL_SERVICE_NAME等),注释标明当前 login 镜像可能忽略它们,属于"future-ready placeholders"。
六、CI 测试体系:所有流量必须穿过 Traefik
AGENTS.md 第 3 节最后一条不变量规定:CI 测试必须走 Traefik,测试覆盖层不得暴露容器直连端口,所有冒烟流量经代理以端到端验证路由。这一条在 docker-compose.test.yml 中落实得很直接:
- 仅把
zitadel-api/zitadel-login的镜像换成本地构建的zitadel/zitadel:local、zitadel/zitadel-login:local(注释说明由 NX 的@zitadel/api:pack+@zitadel/login:pack产出); - 不覆盖任何端口发布——基础文件中
proxy唯一发布的是${PROXY_HTTP_PUBLISHED_PORT}:80,.env.test 把它设为8888,测试流量全部走localhost:8888; - 额外注入
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD等首实例变量,播种已知管理员密码与一个zitadel-admin-sa机器用户(PAT 写入/zitadel/bootstrap/admin.pat,供外部验收测试消费管理 API)。
NX 目标(project.json)与职责:
| 目标 | 行为 | 需要 Docker |
|---|---|---|
test-config | 对全部 5 种覆盖层组合执行docker compose config --quiet语法校验 | 否 |
test-run | 先构建本地镜像(依赖@zitadel/api:pack、@zitadel/login:pack),再up --force-recreate --wait启动zitadel-compose-test栈 | 是 |
test-e2e | Playwright 套件(wiring.spec.ts+smoke.spec.ts)打localhost:8888 | 是(栈须已运行) |
test | 轻量版,只委托test-config,对nx affected安全 | 否 |
test-full | 全流水线:test-config→test-run→ Playwright → 无论成败都执行stop清理 | 是 |
stop | down --volumes拆除测试栈并删除卷 | 是 |
两个 Playwright 测试的定位(摘自 tests/smoke.spec.ts 与 tests/wiring.spec.ts 头部注释):
- wiring 测试回答"每个服务与协议经代理是否可达"——覆盖 Login UI、Console(Angular SPA)、OIDC discovery/keys、SAML metadata、根路径重定向,以及上文 3.1 节完整的 API 协议×传输矩阵;
- smoke 测试回答"整条链是否工作"——一次成功的用户名/密码登录即证明
浏览器 → Traefik(8888) → zitadel-login → zitadel-api → postgres每一环都通。测试凭据读自SMOKE_TEST_ADMIN_USERNAME/SMOKE_TEST_ADMIN_PASSWORD(.env.test 中为zitadel-admin@zitadel.localhost/Password1!,用户名格式为<username>@<org-domain>.<external-domain>)。
七、关键不变量与被否决的替代方案
本节是 AGENTS.md 第 3、4 节的完整继承,也是后续维护者(包括 AI Agent)修改该目录时的硬约束。
必须保持的不变量:
- 四种 TLS 模式保持独立可组合,模式配置永不合并进基础文件;
- gRPC 路由不使用
/grpc路径前缀(源码实现上靠h2c后端方案统一透传,见 3.1 节); - External 三件套(
ZITADEL_EXTERNALDOMAIN/EXTERNALPORT/EXTERNALSECURE)与实际公网端点一致; cache/observabilityprofile 为可选,不得影响默认栈行为;/api别名与规范根路径共存,不得移除规范路由;- 镜像版本在
.env.example中锁定,升级版本必须同步更新该文件; - CI 测试必须走 Traefik,测试覆盖层不暴露直连端口。
被考虑过并被明确否决的设计("不要重新提议"):
| 替代方案 | 否决理由 |
|---|---|
| 单容器合并 API + Login | 不符合 v4 架构——Login 是独立的 Next.js 进程 |
/grpc路径前缀路由 | gRPC 客户端/工具不使用路径前缀,存在兼容性风险 |
严格/api-only 重写模型 | 破坏 OIDC/SAML 规范协议路径 |
Login 使用network_mode: service: | 脆弱、端口冲突、与 Traefik 路由不兼容 |
| 合并的 TLS 配置 | 每种模式必须可独立组合且无副作用 |
README.md 还补充了第 6 条被否决项:基于HeaderRegexp(Content-Type, ^application/grpc.*)的专用 gRPC 路由器——因为h2c后端方案已原生覆盖 gRPC、gRPC-web 与 Connect-RPC 三种协议,专用路由器是冗余的。
八、常用命令速查与术语约定
来自 AGENTS.md 第 5 节的命令表,配合本仓库当前文件结构可直接执行(均在deploy/compose/目录下):
| 任务 | 命令 |
|---|---|
| 启动(本地开发) | cp .env.example .env && docker compose up -d --wait |
| 启动(Let's Encrypt) | docker compose --env-file .env -f docker-compose.yml -f docker-compose.mode-letsencrypt.yml up -d --wait |
| 启动(生产近似) | docker compose --env-file .env -f docker-compose.yml -f docker-compose.prodlike.yml up -d --wait |
| 校验全部配置 | 对每个覆盖层执行docker compose --env-file .env.example -f docker-compose.yml -f <overlay> config > /dev/null |
| 运行 CI 冒烟测试 | pnpm nx run @zitadel/compose:test-full |
| 冒烟检查 | curl -sS http://localhost:8888/.well-known/openid-configuration |
术语约定(遵循根目录 AGENTS.md 术语表):
- Instance= ZITADEL 的逻辑租户/分区,面向用户文本中绝不使用 "example";
- System= 整个 ZITADEL 安装/部署。
小结
deploy/compose目录的这套部署方案的工程精髓在于**"基础栈 + 正交覆盖层"的组合模型**:一个必须可独立工作的 docker-compose.yml,四个互不依赖的 TLS/prodlike 覆盖层,两份职责分明的 env 文件,外加一条由test-config(静态校验)到test-full(经 Traefik 的全协议矩阵端到端测试)机械守护的组合性契约。对部署者而言,最需要内化的是 external 三件套一致性与"规范路径不可移动"两条规则;对维护者而言,AGENTS.md 中的不变量清单与"被否决替代方案"表就是防止架构漂移的第一道防线。
【免费下载链接】zitadelZITADEL - Identity infrastructure, simplified for you.项目地址: https://gitcode.com/GitHub_Trending/zi/zitadel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考