我们把每次部署的上传从 523MB 压到了 6MB
我们每次部署上传的东西,大部分构建根本不用。查清这件事只要一条命令;剩下的提速来自把正确的层缓存到正确的地方。

我们把每次部署的上传从 523MB 压到了 6MB 📦
每次部署都从向构建服务上传 523 兆字节开始。不是镜像 —— 是上下文,也就是在任何构建步骤运行之前被送过去的那个仓库打包。在家用宽带上,这是在构建甚至还没开始之前就要等的好几分钟,每一次都是。
现在它大约 6MB。应用本身什么都没变。
先测量再优化,因为答案反直觉
第一条有用的命令,是告诉你实际在发送什么的那条:
# 会被上传的是什么,从大到小?
tar --exclude-from=.dockerignore -cf - . 2>/dev/null | wc -c
du -sh ./* | sort -rh | head -20
# 构建服务自己报告的数字是权威 —— 盯住那行 context 大小。我们的大头是营销视频、高分辨率图片和 3D 模型资源 —— 这些东西构建确实需要它们出现在最终镜像里,但构建完全不需要每次部署都重新接收它们作为输入,因为它们已经好几个月没变过了。
这个区分就是全部诀窍。构建需要这些文件存在于产物中。它不需要它们每次都新鲜送达。
三个改动,按收益排序
1. 排除构建不读的东西。 先摘显而易见的:版本控制元数据、本地依赖树、上一次的构建输出、测试夹具、文档、编辑器目录。光是 .git 常常就是最大的一项,而构建几乎从不需要它。
要写精确,因为这里一条过宽的规则,正是某个需要的文件悄悄消失、然后在完全另一个地方产生令人困惑的错误的方式。
2. 把大的静态资源结转过来,而不是重新上传。 对那些很少变动且体积大的资源目录,构建可以从上一次发布的镜像里拷贝,而不是从你的机器上接收:
# 从上一次发布的镜像里取未变动的重资源。
FROM registry/app:latest AS prevassets
FROM node:22-slim AS runtime
COPY --from=prevassets /app/public/video /app/public/video
COPY --from=prevassets /app/public/models /app/public/models这是产生了大部分降幅的那个改动。它有一个真实的隐患,值得直说:上一个镜像现在是一个构建依赖了。 删掉 :latest 标签,或者一条会清理掉它的保留策略,会让此后每一次构建都失败。你写的任何保留规则,都必须显式保护构建会去读的那些标签。
3. 把依赖安装缓存到镜像仓库。 使用镜像仓库后端的缓存并开启最大模式,这样中间层也会被缓存,而不只是最终层:
docker buildx build \
--cache-to type=registry,ref=registry/app:buildcache,mode=max \
--cache-from type=registry,ref=registry/app:buildcache \
-t registry/app:latest --push .配合"先拷清单再拷源码",一个纯代码改动就能完整复用已安装的依赖层。剩下的墙钟时间是在这里省掉的。
我们要提醒的几点
缓存标签和 latest 标签现在是基础设施。 你镜像仓库里有两个标签成了每次构建的承重输入。把这件事写进文档。一条看起来很合理的、删除无标签或老旧镜像的清理策略,迟早会碰到它们,而失败表现为一条构建错误 —— 和上周某人改的保留设置之间没有任何显而易见的联系。
更小的上下文可能把"缺文件"藏到很久以后。 如果某条排除规则拿掉了构建只在冷门路径上才读的东西,构建会成功,而产物会微妙地不对。裁剪之后,要验证输出,而不只是"构建通过了" —— 列出你期望出现在最终镜像里的目录,挨个检查。
别优化错阶段。 我们差点花一天去调多阶段构建的层顺序,而真正的成本是家用宽带上的上传时间。把各阶段分开测量 —— 上传、装依赖、编译、推送 —— 然后去做最大的那个。你最感兴趣的那个阶段,很少就是慢的那个阶段。
通用教训
构建系统让"把所有东西都发过去、让构建自己挑"变得非常容易,也让"永远注意不到『所有东西』已经变成多大"变得非常容易。没有任何东西会警告你;上下文每加一个功能就长几兆,直到那个数字荒谬为止。
现在就值得查一下你部署得最频繁的那个东西:上下文多大,其中构建实际读取的占多少?如果这两个数字差得远,修它只要一个下午,而你会在这个项目余下的每一次部署里都感受到它。