Docker 构建慢,先把「重复下载」和「链路慢」拆开
Docker 构建慢,最常见的误区是一上来就排查网络。实际上构建耗时至少有两类来源:一类是每次都在重复下载依赖、重复执行本来可以复用的步骤,这是缓存没用好;另一类是拉基础镜像、访问 registry 本身慢,这才是链路问题。先把前一类解决,后一类的规模往往会明显缩小。本文对照 Docker 官方文档,按「层顺序、依赖缓存挂载、远程缓存、registry 与代理」四块说明,镜像拉取本身慢到底是不是 IDE 代理导致的,可以再看Docker Desktop 与 Colima 拉取慢的排查。
第一块:Dockerfile 的层顺序决定缓存能不能命中
Docker 按指令顺序逐层构建,某一层的输入变了,它之后的所有层都会失效。官方文档也提示过,COPY、ADD 这类指令的内容变化会让后面的 RUN 缓存失效。所以写法上的原则很直接:变化频率低的放前面,变化频繁的放后面。
为什么要把依赖清单和源码分开 COPY
把整个项目一次 COPY . . 之后再安装依赖,意味着改一行代码就会触发重新安装依赖。更合理的写法是先只拷贝 package.json 和 lockfile,安装依赖之后再拷贝源码,这样只有依赖清单变了才会重新下载。
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
.dockerignore 带来的收益在哪里
.dockerignore 用来把不需要的文件从构建上下文里排除,如 node_modules、本地构建产物、版本控制目录。上下文越小,发送给构建器的时间越短,而且减少了无关文件变化导致缓存失效的概率。
第二块:RUN --mount=type=cache 让包管理器缓存跨构建保留
即使层缓存失效,包管理器自己的下载缓存也可以保留。按官方 Dockerfile 参考,RUN --mount=type=cache 挂载的目录会在多次构建之间持久化,且不会因为缓存内容变化而让该指令失效。它有三种共享模式:shared(默认,允许并发访问)、private(各写入方独立缓存)和 locked(串行访问)。像 apt 这类需要独占访问的工具,官方示例使用 sharing=locked。
# syntax=docker/dockerfile:1
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
上面的 /root/.npm 是 npm 默认缓存目录在容器里的位置,其他工具要换成对应的缓存目录,比如 pip 的缓存目录。这样即使 lockfile 改了,没变的包也不用再次从远端下载,对跨境链路下的构建尤其有效——请求次数少了,遇到丢包重试的机会也少了。依赖安装本身超时的参数调整,可以看npm、pnpm、yarn 的重试与并发参数。
第三块:远程缓存后端,让 CI 和同事机器共用缓存
本地缓存只对你自己的机器有效,CI 每次都是干净环境。BuildKit 支持把缓存导出到远程。官方文档列出的后端包括 inline、registry、local 和 gha 等,用 --cache-to 写入、--cache-from 读取:
docker buildx build \
--cache-from type=registry,ref=registry.example.com/app:buildcache \
--cache-to type=registry,ref=registry.example.com/app:buildcache,mode=max \
-t registry.example.com/app:latest .
注意两个细节:第一,mode=min 是默认值,只缓存最终镜像导出的层,mode=max 连中间阶段的层一起缓存,多阶段构建更容易命中,代价是体积更大。第二,官方说明默认驱动支持的后端有限,遇到某个后端不支持的提示,需要换用 docker-container 驱动的 builder。
| 缓存后端 | 特点 | 适合场景 |
|---|---|---|
| inline | 把缓存嵌入镜像一起推送,不支持 mode | 个人项目,追求简单 |
| registry | 缓存单独存成一个镜像引用 | 团队与 CI 共用 |
| local | 写到本地目录 | 自托管 Runner 挂载卷 |
| gha | 上传到 GitHub Actions 缓存(文档标注为 beta) | GitHub Actions 流水线 |
第四块:registry 访问和代理配置的两个常见误解
registry-mirrors 只对 Docker Hub 有效吗
是的。官方镜像加速文档明确说明,目前只能为 Docker Hub 配置镜像,不能为其他私有 registry 配置。在 /etc/docker/daemon.json 里加 registry-mirrors 后需要重新加载 Docker,而其他仓库(例如代码托管平台自带的容器仓库)的拉取,要靠代理或线路本身。
守护进程代理和构建容器代理为什么要分开配
拉基础镜像是 Docker 守护进程在访问 registry,受守护进程的代理设置影响;而 Dockerfile 里的 RUN npm ci 是在构建容器里运行的,读取的是容器环境里的代理变量。两者互相独立,很多人设置了其中一个就以为都生效了。具体的配置入口随 Docker 版本不同,请对照官方「为 Docker 配置代理」文档。
- 先判断慢在哪一步:用
docker buildx build --progress=plain看每一步的耗时,区分拉基础镜像、安装依赖和编译。 - 缓存命中率低就回到层顺序和 cache mount,不要先调网络。
- 拉基础镜像慢再看 registry 访问,用
curl -w记录连接耗时做多时段对比。 - CI 里再考虑远程缓存,并核对所用 builder 驱动是否支持对应后端。
如果已经把缓存工作做完,拉基础镜像仍然在固定时段变慢,那就是链路稳定性的问题了。NasaCode 针对 git、npm、docker 这类开发请求提供全局代理和分流能力,可以让构建机走同一条稳定出口;是否有明显改善,建议用上面第一步的分步耗时做前后对比,而不是只看构建总时长。
小结
Docker 构建提速的顺序是:先优化层顺序和 .dockerignore,再用 cache mount 保留包管理器缓存,然后在 CI 里用 --cache-from 与 --cache-to 共享缓存,最后才处理 registry 与代理。文中官方说明来自 Docker 文档,具体语法和后端支持情况请以你使用的 Docker 与 buildx 版本为准。









