PostHog 的 Depot 容器构建实战:depot build / depot bake 用法、缓存机制与 CI 迁移指南
PostHog 仓库通过 .agents/skills/depot-container-builds/SKILL.md 沉淀了一套 Depot 远程容器构建技能文档,覆盖 depot build、depot bake 的完整用法、Docker Compose 集成、从 docker build/docker buildx 的迁移方式,以及常见错误的修复对照表。本文以该文档为主体,结合仓库中真实的 depot.json、主构建工作流 和 Dockerfile 源码,讲清楚 Depot 远程构建的运行机制、输出模式选择、缓存优化原则,以及 PostHog 团队在实战中踩过的坑(这些坑同时记录在 docs/internal/ci-things-already-tried.md)。读完本文,你可以掌握:如何在 CI 中用 Depot 替代本地 docker build、如何配置多镜像并行 bake、如何避免把镜像构建慢上加慢的常见配置错误。
一、Depot 在 PostHog 中的定位与运行原理
Depot 把 Docker 镜像构建放到远端的高性能构建机上执行——官方规格为 16 CPU、32 GB RAM、带 NVMe SSD 缓存的临时 EC2 实例。depot build 是 docker build / docker buildx build 的"即插即用"替代,depot bake 则替代 docker buildx bake。
PostHog 仓库中这一能力的接入方式可以从三处源码证据得到印证:
-
项目标识文件:仓库根目录的 depot.json(以及 rust/depot.json)内容为
{"id": "x19jffd9zf"},即 Depot CLI 用来确定构建归属的项目 ID。 -
CI 工作流:.github/workflows/container-images-cd.yml 中同时使用了
depot/setup-action(安装 CLI)和depot/build-push-action两个 Action,关键参数写得很直白:uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0 with: context: . # match the CI build's context so master reuses the shared Depot layer cache buildx-fallback: false # the fallback is so slow it's better to just fail push: true两条注释值得注意:
context: .是为了让 master 分支复用 CI 已写入的共享 Depot 层缓存(构建上下文必须一致,层缓存才能命中);buildx-fallback: false表示 Depot 失败时宁愿报错也回退到本地 buildx,因为回退太慢。 -
技能文档来源:.agents/skills/depot-container-builds/UPSTREAM.md 说明该 SKILL.md 是从 Depot 官方的 depot/skills 仓库 vendored(同步)而来,PostHog 的容器镜像 CI/CD 正是围绕
depot/build-push-action与depot/setup-action构建的,并给出了定期重新同步上游的脚本。
Depot 的核心运行概念(来自技能文档的 Key Concepts 部分):
- 构建在临时 EC2 实例上远程执行,镜像默认只留在远端缓存里,不会自动落到本地;
- 需要落地时用
--load(下载到本地 Docker daemon)、--push(推到镜像仓库)、--save(存入 Depot 的临时 registry); - 层缓存基于持久化 NVMe SSD 全自动生效,无需任何手动缓存配置;
- 多平台构建使用原生 CPU 构建机并行处理 amd64 与 arm64,不做 QEMU 模拟;
- 同一 Depot 项目下所有团队成员共享同一份层缓存。
二、多组织用户的项目选择(Project Selection)
容器构建命令的目标是"项目"而非"组织"。技能文档特别警告:如果预期的项目不可见,或构建时意外弹出项目选择提示,通常说明当前默认 org 设错了。排查顺序:
depot org show # 查看当前 org ID
depot org list # 列出用户所属的所有 org
depot org switch <org-id> # 可选:切换默认 org
也可以在构建时用 --project <project-id> 显式指定项目(见下文 depot build 模式),绕开默认 org 的歧义——这一点在多服务共享 compose 文件的场景中尤其有用(PostHog 的 depot.json 就固定了项目 ID,CI 中不再依赖环境默认值)。
三、depot build:核心模式与关键参数
3.1 常用构建模式
技能文档给出了 10 种基本模式,覆盖构建、下载、推送、多平台、暂存、Lint、密钥与 SSH 转发:
# 远程构建(镜像留在远端缓存,本地拿不到)
depot build -t repo/image:tag .
# 构建 + 下载到本地 Docker daemon
depot build -t repo/image:tag . --load
# 构建 + 直接推送到镜像仓库(快——不经过本地网络中转)
depot build -t repo/image:tag . --push
# 多平台构建(原生 CPU 并行,无模拟)
depot build --platform linux/amd64,linux/arm64 -t repo/image:tag . --push
# 存入 Depot 临时 registry(默认 7 天保留)
depot build --save .
depot build --save --save-tag my-tag .
# 关闭 provenance 元数据(修复仓库中显示 "unknown/unknown" 平台的问题)
depot build -t repo/image:tag --push --provenance=false .
# 构建前先 Lint Dockerfile
depot build -t repo/image:tag . --lint
# 携带构建密钥
depot build --secret id=mysecret,src=./secret.txt -t repo/image:tag .
# 转发 SSH agent(用于构建期拉私有仓库依赖)
depot build --ssh default -t repo/image:tag .
# 显式指定 Depot 项目
depot build --project <project-id> -t repo/image:tag .
3.2 关键 Flag 全表
| Flag | 说明 |
|---|---|
--load | 把镜像下载到本地 Docker daemon |
--push | 推送到镜像仓库 |
--save | 存入 Depot 临时 registry |
--save-tag | 为 Depot Registry 指定自定义 tag |
--platform | 目标平台(linux/amd64、linux/arm64 或两者) |
--build-platform | 强制指定构建执行架构(默认 dynamic) |
--project | Depot 项目 ID |
--token | Depot API token |
--lint | 构建前 Lint Dockerfile |
--provenance | 控制 provenance 凭证(设为 false 可修复 unknown/unknown) |
--no-cache | 本次构建禁用缓存 |
-f / --file | 指定 Dockerfile 路径 |
-t / --tag | 镜像名与 tag |
--target | 只构建特定 stage |
--build-arg | 设置构建期变量 |
--secret | 暴露密钥,格式 id=name[,src=path] |
--ssh | 暴露 SSH agent |
--output / -o | 自定义输出,如 type=local,dest=path |
注意 --build-arg BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 是一个特殊用法:Depot 远程构建的上下文默认不带 .git 目录,如果 Dockerfile 里的构建脚本(如 git describe 生成版本号)依赖它,就必须显式加这个 build-arg(见下文常见错误表)。
四、depot bake:多镜像并行构建
depot bake 是 docker buildx bake 的替代命令,用于一次并行构建多个镜像:
depot bake # 按默认文件查找顺序执行
depot bake -f docker-bake.hcl # 指定 HCL 文件
depot bake -f docker-compose.yml --load # 构建 compose 服务并加载到本地
depot bake --save --save-tag myrepo/app:v1 # 存入 Depot Registry
depot bake --print # 只打印解析后的配置,不实际构建
默认文件查找顺序:compose.yaml → compose.yml → docker-compose.yml → docker-compose.yaml → docker-bake.json → docker-bake.override.json → docker-bake.hcl → docker-bake.override.hcl。--print 是排错利器:它只输出解析后的 target 列表而不触发构建,可以在 CI 调试阶段确认平台、tag、args 是否符合预期。
4.1 HCL bake 文件示例
技能文档中的完整示例保留了变量、分组、多平台 target 与 target 间共享上下文(contexts = { app = "target:app" } 表示 worker 复用 app 的构建结果作 base):
variable "TAG" {
default = "latest"
}
group "default" {
targets = ["app", "worker"]
}
target "app" {
dockerfile = "Dockerfile"
platforms = ["linux/amd64", "linux/arm64"]
tags = ["myrepo/app:${TAG}"]
args = { NODE_VERSION = "20" }
}
target "worker" {
dockerfile = "Dockerfile.worker"
tags = ["myrepo/worker:${TAG}"]
contexts = { app = "target:app" } # Share base between targets
}
变量可以在命令行覆盖:TAG=v2.0 depot bake。
4.2 Docker Compose 中的逐服务项目 ID
同一个 compose 文件里的不同服务可以绑定到不同的 Depot 项目,用 x-depot 扩展字段声明:
services:
api:
build:
dockerfile: ./Dockerfile.api
x-depot:
project-id: abc123
web:
build:
dockerfile: ./Dockerfile.web
x-depot:
project-id: def456
depot bake -f docker-compose.yml 会按服务各自的 x-depot.project-id 路由到对应项目的缓存。
五、Docker Compose 集成的两种方式
技能文档给出"推荐"与"零改动"两条路线,效率差异明确:
# 推荐:并行构建所有服务后统一加载(共享构建会话,效率最高)
depot bake -f docker-compose.yml --load
docker compose up
# 替代:零代码改动(效率较低,每个服务是独立构建)
depot configure-docker
docker compose build
depot configure-docker 会注册一个 Docker CLI 插件,把本地 docker build 静默路由到 Depot——日志中出现 [depot] 前缀即可确认请求确实走了 Depot。这条路适合过渡期,但每个服务独立构建、无法共享 build 会话,不如 bake 一次并行高效。
六、从 Docker 迁移到 Depot
迁移成本极低,基本是单行替换:
# docker build → depot build(flag 相同,一行替换)
depot build -t my-image .
# docker buildx bake → depot bake
depot bake -f docker-bake.hcl
# 零代码改动方案:通过 Docker 插件接管
depot configure-docker
docker build . # 请求被路由到 Depot(日志中找 [depot] 前缀确认)
迁移时必须删除的参数——Depot 自动处理缓存,保留这些参数反而制造故障:
--cache-from type=gha:会触发 "services aren't available" 错误;--cache-to type=gha:同样的问题;- 其他任何手工的 BuildKit 缓存配置(GHA cache 是 GitHub Actions 专属的 buildx 缓存通道,在 Depot 远程构建环境中没有对应服务)。
七、常见错误对照表(技能文档原表全量继承)
| 错误现象 | 修复 |
|---|---|
使用了 --cache-from type=gha 或 --cache-to type=gha | 删掉。Depot 在 NVMe SSD 上自动缓存 |
多平台镜像在仓库里显示 unknown/unknown 平台 | 加 --provenance=false |
depot build 后期望本地就有镜像 | 加 --load 下载,或 --push 推到仓库 |
构建上下文里没有 .git 目录 | 加 --build-arg BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 |
| 构建挂起或 "failed to mount" 错误 | 在项目设置中重置缓存,或执行 depot cache reset |
| 拉取基础镜像报 "401 Unauthorized" | Docker Hub 限流——用 docker login 认证,或改用 public.ecr.aws/docker/library/ 镜像源 |
| "Keep alive ping failed" / OOM | 在项目设置里调大 builder 规格或启用自动扩缩 |
八、PostHog 实战教训:缓存挂载不是越多越好
技能文档开头要求"改 CI 之前先查 things already tried"。该文档中 Docker 相关条目恰好为 Depot 用法提供了反向佐证:
-
BuildKit cache mounts 全量套用被否决(rejected, 2025-10):照 Depot 官方指南给 apt/pip/uv/node/Playwright 全部加了 cache mount 后,温缓存下后端构建从 52.7s 变慢到 57.5s(约 9%),前端从 55.5s 到 62.2s(约 12%)。原因是 cache mount 即使缓存命中也有 5–7 秒开销,而约 95% 的 PR 只改代码不改依赖,"为罕见场景加速、让常见场景变慢"。
-
窄化版本保留下来:最终只给个别昂贵步骤保留挂载。这一点可以直接在 Dockerfile 里验证——当前主 Dockerfile 只剩几个精准挂载:
RUN --mount=type=cache,id=pnpm,target=/tmp/pnpm-store-v24 \ ... # L59、L147 RUN --mount=type=cache,id=npm,target=/root/.npm \ ... # L152 RUN --mount=type=cache,id=uv-libxmlsec1.2.37-2,target=/root/.cache/uv \ ... # L198其中
id=uv-libxmlsec1.2.37-2是踩坑后的产物:uv 缓存里残留了对旧版libxmlsec1编译的 wheel 导致构建失败,后来的修复是把系统库版本写进 cache ID,库版本一变缓存自动失效。结论:只为测量过的高开销步骤加 cache mount,并把缓存产物所编译依赖的系统库版本编入 cache ID。 -
Docker Hub 限流的迷惑性诊断(2026-08):CI 大面积报
toomanyrequests: ... unauthenticated pull rate limit,但docker login全程成功。真因是 Docker Hub 订阅过期——过期后 Docker 签发匿名 token 但仍接受登录。技能文档错误表中"401 Unauthorized → 先认证"的修复方向与之呼应,但 PostHog 的教训补充了:如果认证没问题,先查订阅状态再怀疑密钥。
九、Builder 规格与计费
| 规格 | CPU | 内存 | 每分钟价格 | 适用计划 |
|---|---|---|---|---|
| Default | 16 | 32 GB | $0.004 | 所有计划 |
| Large | 32 | 64 GB | $0.008 | Startup 及以上 |
| Extra Large | 64 | 128 GB | $0.016 | Startup 及以上 |
按秒计费;depot bake 无论包含多少 target 都只按一次构建计费——这对多服务/多镜像仓库是一笔可观的节省。
十、Depot Registry:临时镜像暂存与二次分发
Depot 的临时 registry(默认 7 天保留)适合"先存后取"的 CI 编排——构建一次,多个下游 job 拉取,或直接再推送到正式仓库:
# 存入 Depot Registry
depot build --save -t myapp .
# 拉取已保存的镜像
depot pull --project <id> <build-id>
# 把已保存镜像推送到其他仓库
depot push --project <id> -t registry/image:tag <build-id>
# 用 Docker 直接登录 Depot Registry
docker login registry.depot.dev -u x-token -p <depot-token>
# Registry 地址格式:registry.depot.dev/<project-id>:<tag>
十一、特殊输出格式:estargz 与 zstd
对容器启动速度敏感的部署场景(Fargate、K8s),技能文档给出了两种压缩输出的完整 --output 参数:
# estargz:支持 lazy-pulling,容器启动更快
depot build --output "type=image,name=repo/image:tag,push=true,compression=estargz,oci-mediatypes=true,force-compression=true" .
# zstd 压缩:Fargate/K8s 启动更快
depot build --output type=image,name=repo/image:tag,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true,push=true .
两者的取舍:estargz 的价值在于运行时按需拉取(需要容器运行时支持),zstd 的价值在于压缩率高、解压快(通用性好)。选择哪种取决于运行侧是否支持对应的拉取路径。
十二、小结:Depot 工作流速查
- 本地/CI 构建:
depot build,按需求加--load/--push/--save;多平台直接--platform linux/amd64,linux/arm64,原生 CPU 并行、无模拟开销; - 多镜像:
depot bake(HCL 或 compose 文件),bake 一次计费一次;--print先行排错; - 零改动过渡:
depot configure-docker后沿用docker build/docker compose build,日志见[depot]前缀即生效; - 迁移纪律:删掉
--cache-from/--cache-to type=gha等手工 GHA 缓存参数;缓存问题用depot cache reset而非换回本地 buildx; - 改 CI 前先检索:PostHog 把"已尝试过什么、为什么失败/回滚"沉淀在 docs/internal/ci-things-already-tried.md,其中 Docker 构建条目(cache mount 开销、cache ID 编入系统库版本)是 Depot 调优最直接的参照;
- 可追溯性:仓库内 Depot 项目固定于 depot.json,CI 统一经 container-images-cd.yml 的
build-push-action执行并禁用 buildx 回退,保证 master 与 CI 共享同一份 Depot 层缓存。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



