先记住这个答案
docker build --target <stage> 让构建停在指定阶段。使用 BuildKit 时只构建目标依赖的阶段;传统构建器会执行到目标为止的所有阶段。在 CI 中可分别构建不同目标:测试镜像、生产镜像或调试镜像,实现阶段复用与独立交付。例如先构建 test 阶段跑测试,再构建 prod 阶段发布,避免每次测试都走完所有步骤。
--target可指定阶段名或数字索引。- CI 中可先建测试阶段跑测试再建生产阶段。
- BuildKit 只构建依赖链,旧版会执行到目标的所有阶段。
`--target` 如何控制构建流程
在 Dockerfile 里每个 FROM 开始一个新阶段,可用 AS 命名。docker build --target <name> 告诉构建器只处理达到该阶段的指令链。例如 FROM golang AS build 后接 FROM alpine AS runtime,执行 --target build 时,runtime 阶段及其后的指令全部跳过。
构建缓存按阶段生效:被跳过的步骤不会执行,也不会影响缓存。关键是构建器必须先识别阶段间的依赖关系——COPY --from=stage 会建立显式依赖。若目标阶段不依赖某阶段,BuildKit 会跳过它,而旧版(legacy builder)会盲目执行所有阶段直到目标,这在多阶段时会浪费大量时间。
CI 中构建测试镜像并运行测试
某 Go 项目 Dockerfile 先有 FROM golang:1.21 AS build 编译二进制,再 FROM build AS test 复制源码并跑单元测试,最后 FROM alpine AS prod 放二进制。CI 流水线第一步执行 docker build --target test -t app:test .,该镜像内含测试数据和工具,运行 docker run app:test go test ./... 完成测试。
测试通过后,第二步执行 docker build -t app:prod .(不指定 target,默认最后阶段)生成精简发布镜像。这样测试阶段依赖构建阶段但不会携带生产阶段,测试镜像可独立缓存,且 CI 各步骤清晰分离。若将测试放在最终阶段,每次构建都要等待拷贝和压缩,缓存失效也更频繁。
失效条件与处理代价
--target 对命名阶段敏感:若阶段未命名,只能使用数字索引如 --target 0,但不直观且重排指令后易错。另一个限制是,被跳过的阶段中的指令不会执行,若这些阶段有副作用(如清理外部资源)则无法触发,需另行规划。
更隐蔽的是,旧版构建器(未启用 BuildKit)不会裁剪无关阶段,即便指定 --target 也会执行之前所有阶段,导致 CI 时间未缩短。要解决需开启 BuildKit(DOCKER_BUILDKIT=1 或使用 buildx),它仅构建目标依赖链。代价是旧环境可能不支持,需确认 CI 构建器版本。
容易答错的地方
- 认为 --target 会跳过所有非依赖阶段
- 事实是传统构建器会执行到目标阶段为止的所有阶段,即使它们不在依赖链中。例如 Dockerfile 有 base、stage1、stage2,构建 stage2 时旧版会先构建 stage1,浪费时间和磁盘。BuildKit 才真正跳过。
- 以为 --target 可以随便指向任意阶段
- 目标阶段必须存在,且其所有上游依赖必须明确。若阶段名打错或靠数字索引且阶段重排,会失败或构建错误镜像。应始终为关键阶段命名,并用
docker build --help确认参数。
面试官还会怎么问?
为什么不直接把测试放在最后阶段?
若最终阶段为生产精简镜像,加入测试会混入测试工具和源码,增大镜像并泄露内部数据。单独 --target test 可构建一个携带全部源码的测试镜像,而生产镜像用另一阶段保持洁净。
在 CI 中 --target 与 --cache-from 如何配合?
用 --target test 构建测试镜像时,仍会生成该阶段对应的镜像层,可被 --cache-from 引用。后续构建生产镜像可复用 build 阶段缓存,前提是使用 BuildKit 并导出缓存。具体见缓存共享相关主题。
如何调试特定阶段?
先 --target build 得到含调试工具的镜像,再用 docker run 进入该阶段附加命令。例如编译阶段,可启动容器查看中间文件。调试完再构建最终阶段,但注意调试阶段常需更大体积。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。