披露:本文由 Appaloft 团队撰写,介绍的是我们自己的实现与审计结果。
部署流程可以顺利结束,用户拿到的却仍是错误版本。任务是绿色,进程还活着,健康检查也返回 HTTP 200,但旧容器或陈旧代理目标仍在对外提供服务。
最近几份公开报告把这些边界呈现得很清楚:7 月 10 日的 Dokploy Swarm 报告称,固定镜像只修改配置后,界面显示 “Deployed”,task 却没有重建;7 月 12 日的 Dokploy 报告称 Compose 容器始终健康,但 Traefik 选中了陈旧或错误的网络地址并返回 502;7 月 11 日的 Coolify 报告称自托管生产服务达到重启上限后停止;7 月 15 日的 Dokploy 报告称平台自身更新后在多个 readiness 状态间循环,用户看到的是 Bad Gateway。
这些是用户报告,不是我们独立完成的根因审计,而且分属不同产品与层级。我们没有把它们当成竞品比较材料,而是把它们变成了 Appaloft 的审计清单。
“已部署”至少要证明四件事
- Artifact 已应用。 目标镜像、revision 或配置进入了新的 workload generation。
- 进程持续存活。 进程不会刚启动就进入重启循环,或达到上限后停止。
- 服务健康。 当前 workload 通过内部健康策略。
- 路由可达且属于当前部署。 真实域名、TLS 和代理路径服务的是这个 deployment,而不只是“某个能返回 200 的服务”。
第四件事不能从容器 label 推断。容器可以正确标识自己,edge proxy 却仍然指向旧 workload。
我们在 Appaloft 审计里发现了什么
配置变更未生效的 Swarm 问题,在 Appaloft 没有出现相同的 no-op 路径。Appaloft 会创建带独立 deployment identity 与安全配置 fingerprint 的候选 stack/service,验证后再提升路由。Deployment Proof 会把实际 generation 和 fingerprint 与已接受的计划对照,因此健康的旧 task 会得到 stale,而不是 verified。
已经停止或不健康的 workload 原本就会产生 failed health evidence,覆盖了 restart-limit 报告里的 workload 侧症状。平台自身更新后控制面起不来,则是另一个 platform-readiness 边界;workload Deployment Proof 不宣称解决它。
路由问题确实暴露了 Appaloft 的一个缺陷。原先 Docker readback 会把当前容器的 appaloft.deployment-id label 当作“路由指向这个 deployment”的证据。它其实只是 workload identity,不是 route evidence。Traefik 或 Caddy 如果仍指向旧目标,这个结论就是错的。
我们修改了契约:
- Appaloft 管理的 Caddy 与 Traefik serve route 会在响应中写入
X-Appaloft-Deployment-Id; deployments.proof会通过真实 managed domain 请求配置的公网 health path;- 即便 HTTP 状态是 200,只要 deployment identity 缺失或不一致,access evidence 就失败;
- 只做跳转的 alias 不会冒充 workload identity。
这个 header 只包含不透明的 deployment id,不包含配置或 secret。配置证据仍然只使用 SHA-256 fingerprint,原始值不会进入 proof response。
一个查询,五种结论
Deployment Proof 和其他 Appaloft 能力共用 operation catalog:
appaloft deployments proof <deploymentId>
HTTP 入口是 GET /api/deployments/{deploymentId}/proof。Generated SDK 和 MCP tool 暴露同一个 deployments.proof operation,Web Console 的 deployment detail 页面也渲染同一份 DTO。
这五个 verdict 刻意把“缺证据”和“已验证”分开:
verified:artifact、workload、配置、health、access 和 recovery 等必要证据与部署意图一致;partially-verified:已有证据能对上,但某项必要 readback 暂时拿不到;unverified:runtime 证据不足,无法作出判断;stale:runtime 可能仍然健康,但已经不是这次 deployment 对应的 workload;failed:观测结果和部署目标直接冲突,例如 health 失败,或 access route 指向了错误 workload。
“读不到 artifact digest”和“读到了另一个 digest”不是同一种故障,处理方式也不该一样。
测试专门覆盖那些“不舒服”的场景
已有的真实 Docker smoke 会先部署 v1,通过正式 Resource 配置 operation 修改 APP_VERSION,重新部署 v2,再从外部把当前容器替换成一个仍然健康、但携带旧 deployment/config label 的 v1 容器。Health 依旧通过,proof 仍会把 workload 判为 stale。
我们又增加了一个使用真实 Traefik 容器的 smoke。路由返回 HTTP 200,并标记自己服务的是 dep_v1;proof 核验的是 dep_v2。结果明确失败:
{
"httpStatus": 200,
"expectedDeployment": "dep_v2",
"observedDeployment": "dep_v1",
"accessStatus": "failed",
"reasonCode": "public_route_deployment_identity_mismatch"
}
Provider tests 还会确认 Caddy 与 Traefik 只给 serve route 加标记;adapter tests 则覆盖 identity 一致、缺失、不一致、不可访问和非 2xx 响应。
证据从哪里来
Local Docker 和 Docker Compose adapter 会按 Resource label 找到容器,然后读取 image identity、workload label、启动时间、health 和脱敏后的 configuration fingerprint。Docker Swarm 则读取对应 service identity 与 update generation。Managed public-route evidence 现在独立来自代理响应,不再来自容器 inspect。
Application layer 不知道 Docker command。它只向 runtime evidence port 请求标准化后的观测数据,再把这些数据与 deployment state 对照。Verdict 逻辑因此可以独立测试,其他 runtime adapter 以后也能提供同等证据,不必改 query contract。
Generic SSH 暂时没有足够 readback 能力时,会明确返回 evidence gap。Direct-port workload 也不会冒充自己完成了 managed-proxy identity check。我们宁愿把边界写出来,也不愿从 execution record 猜一个成功。
AI 和 GitHub check 也得遵守这条规则
Appaloft skill 现在要求 agent 在观察完 deployment progress 后读取 deployments.proof。Agent 可以描述 terminal state,但只有 verdict 为 verified 时,才能说部署成功。
Cloud 的 GitHub check summary 也用同一映射。绿色 workflow 是一份输入证据,不是最终结论。Control plane 仍需核对“请求部署什么”和“runtime 正在跑什么”。
CLI、Web Console、MCP client 和自动化 check 因此得到的是同一个答案,不再各自解释一遍状态。
命令和 verdict 的完整说明见部署生命周期文档。如果想继续了解 provenance,可以读一次 GitHub Actions 部署应该留下哪些证据。
后续 runtime adapter 想拿到 verified,也得交出自己的 readback 证据。暂不支持的部分会继续显示为缺口,不会藏在一个绿色状态后面。