news 2026/9/14 10:08:02

ZITADEL v4 单节点 Docker Compose 部署:服务架构、TLS 覆盖层组合与 Traefik 路由不变量

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ZITADEL v4 单节点 Docker Compose 部署:服务架构、TLS 覆盖层组合与 Traefik 路由不变量

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-apiGo API 服务,监听容器内 8080
核心zitadel-loginNext.js 登录 UI,监听容器内 3000
核心postgres数据持久化
核心proxyTraefik 反向代理,发布 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 中:

  1. 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。
  2. 健康检查驱动的启动顺序postgres就绪 →zitadel-api健康(/app/zitadel ready探针)→zitadel-login健康(/ui/v2/login/healthy探针)→proxy最后启动,全部通过depends_on.condition: service_healthy串联(见 docker-compose.yml 与 L83-L85)。

redisotel-collector均声明在profiles之下(L205-L228),不显式启用 profile 时不会影响默认栈——这是文档明确列出的不变量之一。

二、文件结构与职责边界

AGENTS.md 第 2 节给出一张文件约定表,README.md 对其有相同口径的补充。两者合并后的完整文件职责如下:

文件用途修改安全性
docker-compose.yml基础栈——所有模式都从它起步,必须能单独配合.env.example工作可以,但需谨慎
docker-compose.mode-letsencrypt.ymlTLS 覆盖层:ACME HTTP challenge,自带letsencrypt可以
docker-compose.mode-external-tls.ymlTLS 覆盖层:上游负载均衡器终结 TLS,启用转发头信任可以
docker-compose.mode-local-tls.ymlTLS 覆盖层:自签证书,挂载./certs/traefik-local-tls.yml可以
docker-compose.prodlike.ymlinit/setup/start 三段拆分覆盖层,使用 YAML 锚点共享 DB 环境可以
docker-compose.test.ymlCI 测试覆盖层:本地构建镜像、不暴露直连端口可以
.env.example面向用户的配置模板,版本号在此锁定可以——升级版本时需同步更新
.env.testCI 专用配置,仅内部使用内部
otel-collector-config.yamlOTEL Collector 管道配置可以
traefik-local-tls.yml本地 TLS 的 Traefik 动态配置可以
project.jsonNX 工程定义(test-config/test-run/test-e2e/test/test-full/stopNX 自动管理

这里有一条值得牢记的组合性规则:每个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 两个入口点拥有完全相同的规则集):

优先级规则目标中间件
400Path(/)zitadel-loginreplacepath=/ui/v2/login/
250PathPrefix(/ui/v2/login)zitadel-login
200PathPrefix(/api)zitadel-apistripprefix=/api
100其余一切(OIDC、SAML、gRPC、gRPC-web、API v2 REST 等)zitadel-apih2c后端)

对应源码见 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=/apiforceSlash=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/jsonapplication/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/loginCUSTOM_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_EXTERNALDOMAINZITADEL_EXTERNALPORTZITADEL_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_BASEURIZITADEL_OIDC_DEFAULTLOGINURLV2ZITADEL_OIDC_DEFAULTLOGOUTURLV2ZITADEL_SAML_DEFAULTLOGINURLV2)由ZITADEL_PUBLIC_SCHEME+ 上述变量拼接而成(L52-L55)。TLS 覆盖层则会整体覆盖这组 URL 为https://${ZITADEL_DOMAIN}/...并把ZITADEL_EXTERNALPORT硬置为443ZITADEL_EXTERNALSECURE: true——例如 docker-compose.mode-letsencrypt.yml 中对zitadel-apizitadel-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_MASTERKEYPOSTGRES_ADMIN_PASSWORD等值。几个关键取值:

  • ZITADEL_DOMAIN=localhostPROXY_HTTP_PUBLISHED_PORT=8080ZITADEL_EXTERNALPORT=8080ZITADEL_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.0TRAEFIK_IMAGE=traefik:v3.7.7POSTGRES_IMAGE=postgres:17.10-alpineREDIS_IMAGE=redis:7.4.9-alpineOTEL_COLLECTOR_IMAGE=otel/opentelemetry-collector-contrib:0.156.0),升级流程是"改.env版本号 →pullup -d --wait"。

基础文件中 API 的启动命令是start-from-init --masterkey "${ZITADEL_MASTERKEY}"(L35),即初始化与启动合并为单容器完成;bootstrap 相关环境变量(ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATHZITADEL_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 --wait

docker-compose.mode-letsencrypt.yml 在基础命令之上追加了 ACME 配置(--certificatesresolvers.le.acme.httpchallenge=true,使用web入口点完成 HTTP challenge),ports用 YAML 合并键!override覆盖为80:80443:443,并新增letsencrypt命名卷存放acme.jsonLETSENCRYPT_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=truehttps://登录 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 --wait

docker-compose.prodlike.yml 把"一个长驻容器"拆成三个短生命周期步骤,便于在故障时单独重放某一段:

  1. zitadel-initcommand: initrestart: "no",只做数据库初始化;
  2. zitadel-setupcommand: setup --masterkey ...,执行首实例引导(创建组织、login-client机器用户与 PAT 等,环境变量组与基础文件的zitadel-api相同,通过 YAML 锚点&zitadel-db-env/*zitadel-db-env共享 DSN);依赖zitadel-initservice_completed_successfully完成后才启动;
  3. zitadel-apicommandstart-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=falseZITADEL_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:localzitadel/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-e2ePlaywright 套件(wiring.spec.ts+smoke.spec.ts)打localhost:8888是(栈须已运行)
test轻量版,只委托test-config,对nx affected安全
test-full全流水线:test-configtest-run→ Playwright → 无论成败都执行stop清理
stopdown --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)修改该目录时的硬约束。

必须保持的不变量:

  1. 四种 TLS 模式保持独立可组合,模式配置永不合并进基础文件;
  2. gRPC 路由不使用/grpc路径前缀(源码实现上靠h2c后端方案统一透传,见 3.1 节);
  3. External 三件套(ZITADEL_EXTERNALDOMAIN/EXTERNALPORT/EXTERNALSECURE)与实际公网端点一致;
  4. cache/observabilityprofile 为可选,不得影响默认栈行为;
  5. /api别名与规范根路径共存,不得移除规范路由;
  6. 镜像版本在.env.example中锁定,升级版本必须同步更新该文件;
  7. 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 10:06:57

ABAP环境Fiori Launchpad Pages与Spaces落地实践与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 10:05:51

Windows Terminal主题自动切换:一套配置搞定深浅色联动

Windows Terminal主题自动切换&#xff1a;一套配置搞定深浅色联动 【免费下载链接】terminal The new Windows Terminal and the original Windows console host, all in the same place! 项目地址: https://gitcode.com/GitHub_Trending/term/terminal 晚上十点盯着终…

作者头像 李华
网站建设 2026/9/14 10:03:07

AI编码范式迁移:OPC UA如何成为工业AI生态锚点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 10:01:43

智能系统设计时代:汽车电子从工具应用到数据驱动范式跃迁

1. 项目概述&#xff1a;这不是一次软件功能更新&#xff0c;而是一场设计范式的迁移“Cadence 汪晓煜&#xff1a;数智赋能&#xff0c;汽车电子走向智能系统设计时代”——这个标题里没有出现任何具体命令、参数或报错信息&#xff0c;但它比所有“cadence allegro 17.4安装步…

作者头像 李华