Docker BuildKit 构建最终镜像时跳过测试阶段
Dockerfile 写了测试阶段却没有执行:BuildKit 只构建目标需要的阶段。通过显式测试目标或调整发布依赖接入测试,并区分阶段被跳过与缓存复用。
Dockerfile 中写了 FROM build AS test 和测试命令,构建最终镜像却直接成功,日志里完全没有测试阶段。调整 test 在文件中的位置后仍然如此:它虽然写在发布阶段之前,却没有进入最终目标的依赖链。
使用 BuildKit 时,构建器会处理目标阶段需要的阶段,不会按文件顺序执行每一个 FROM。如果 test 和 publish 都从 build 分出来,而最终镜像只复制 publish 的产物,测试分支就可能被跳过。
本文针对 BuildKit 多阶段构建。旧构建器处理前序阶段的方式不同,不能把旧日志中的执行顺序当作当前流程的测试保证。也不需要为了让测试运行而退回旧构建器。
沿最终目标追踪依赖
先核对实际命令使用的 Dockerfile、构建上下文和 --target。未指定目标时通常构建最后一个阶段;CI 包装任务可能另外传入目标,不能只看本地命令。
下面是一个只演示阶段关系的完整 Dockerfile。构建上下文需要已有 app.txt,test -s 仅检查它是否非空,不代表实际应用的测试套件:
FROM alpine:3.22 AS build
WORKDIR /work
COPY app.txt ./app.txt
FROM build AS test
RUN test -s /work/app.txt
FROM build AS publish
RUN mkdir -p /out && cp /work/app.txt /out/app.txt
FROM alpine:3.22 AS final
COPY --from=publish /out/app.txt /app/app.txt
final 需要 publish,publish 需要 build。test 只依赖 build,没有被这条链引用。把它移动到 publish 前面,或把阶段名改成 tests,都不会增加依赖。
实际项目里,RUN dotnet test、RUN npm test 等命令也遵循相同的阶段选择规则。检查的是 FROM、COPY --from 等形成的依赖关系,不是阶段名称中有没有“test”。
单独构建测试目标,并让失败阻止后续构建
如果希望测试阶段保持独立,可以在同一份源码、同一组构建参数上先构建它,再构建发布目标。下面在支持这些选项的 Docker BuildKit 环境中运行,镜像名只是示例:
docker build --target test --progress=plain . \
&& docker build --target final --progress=plain -t example-app:checked .
第一条命令选择 test,不会同时构建 final。&& 让第二条只在第一条成功后执行;如果拆成两个 CI 任务,也要明确设置成功依赖,不能把测试失败设为允许继续。
两次构建之间不能更换源码、依赖锁文件、平台或影响程序行为的构建参数,否则测试通过的输入可能与发布输入不同。测试脚本本身也必须用非零退出状态报告失败,不能在 Dockerfile 里用 || true 把错误吞掉。
这些命令没有推送镜像,也没有部署应用。实际流水线的推送与部署步骤应继续受测试和镜像构建的成功状态约束。
让发布阶段直接继承测试阶段
如果每次发布都必须经过同一套测试,也可以改变依赖关系。对上面的示例,只将发布阶段的起点从 build 改成 test:
FROM test AS publish
RUN mkdir -p /out && cp /work/app.txt /out/app.txt
其他阶段保持原样。此时构建 final 会依赖 publish,再依赖 test 和 build。最终镜像仍只从 /out 复制指定文件,不会因此自动带上整个测试环境。
不过,发布阶段会继承测试阶段的文件系统和配置。真实测试若会改写源文件、生成临时文件或改变依赖,必须检查这些变化是否适合继续用于发布。不要为了建立依赖,随便把测试报告或全部测试目录复制进运行镜像。
需要保留独立分支的项目可以选择前面的两步流程;不要只为了排错就重新组织所有构建阶段。
目标被选中和测试重新执行是两回事
修好依赖后,日志可能显示测试步骤为 CACHED。这说明构建复用了已有结果,不等于测试阶段仍被剪掉。--target test 保证选择该目标,不保证每次重新执行其中的 RUN。
如果本次要求重新运行测试,可以对测试目标禁用构建缓存:
docker build --target test --no-cache --progress=plain . \
&& docker build --target final --progress=plain -t example-app:checked .
这会让测试目标及其需要的构建步骤不复用构建缓存,通常会增加构建时间。它不等于自动刷新所有基础镜像,也不能代替测试环境与外部服务的隔离。没有必要先执行全局缓存清理,更不需要删除运行容器的数据卷。
在隔离的测试分支中,用一个已知会失败的测试核对流程:测试目标应返回失败,后续发布构建应被阻止。随后恢复测试并检查通过路径。普通成功构建只能说明选定目标完成,不能单独证明每个预期测试都执行过。
修改前保存 Dockerfile 和 CI 配置。需要回退依赖改动时,同时保留独立的测试阻断步骤;直接恢复到跳过测试的发布链,并不能恢复质量检查。若测试已失败,应停止发布并处理失败原因,不通过禁用测试来完成这次部署。


