最近在公司做 CI/CD 流水线升级,发现每次提交代码后,Docker 构建动不动就卡上三五分钟。同事老张一边等构建完成,一边泡咖啡:“这哪是开发,简直是煎熬。” 其实,容器化构建慢不是无解难题,找准瓶颈,对症下药,速度提升一倍都不难。
分层缓存用到位,别让重复劳动拖后腿
Docker 的镜像分层机制本意是提升复用性,但很多人写 Dockerfile 时不注意顺序,导致缓存失效。比如把 npm install 放在源码拷贝之后,只要改一行代码,依赖就得重装一遍。
正确的做法是先拷贝锁定文件,再安装依赖,最后拷贝源码:
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
这样只要 package.json 没变,yarn 安装步骤就能命中缓存,省下大把时间。
多阶段构建减少冗余,别把编译器打进运行镜像
很多项目直接用包含 Node.js、Python 等完整环境的基础镜像打包应用,结果运行时带着 GCC、make 这些根本用不上的工具。不仅体积大,构建也慢。
用多阶段构建,前一阶段负责编译,后一阶段只复制产物:
FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
最终镜像轻了,拉取和启动自然更快。
利用 BuildKit 并行加速
默认的构建引擎比较“老实”,而开启 BuildKit 后,Docker 能自动并行处理无依赖的构建步骤。在 CI 环境中尤其明显。
启用方式很简单,加个环境变量就行:
DOCKER_BUILDKIT=1 docker build -t myapp .
你会发现某些层几乎是同时开始处理的,整体耗时明显下降。
镜像基础选得好,起点就不慢
别再无脑用 ubuntu:latest 了。这种通用系统自带一堆服务和包,拉取和初始化都费劲。换成 alpine 或者 distroless 这类精简镜像,体积小一半不止。
比如 Go 项目可以直接基于 gcr.io/distroless/static,连 shell 都没有,但足够运行二进制文件。
本地构建也要会“借力”
如果经常在本机构建,可以挂载缓存目录。比如 Node.js 项目把 ~/.npm 映射进去,避免每次都在容器里重新下载。
配合 docker build --mount 使用:
docker build --mount=type=cache,target=/root/.npm ...
虽然不如 CI 中的远程缓存稳定,但在调试阶段很实用。
CI 环境别忘了缓存传递
在 GitLab CI 或 GitHub Actions 里,可以配置缓存策略,把依赖目录(如 node_modules)或 Docker 层保存下来。下次流水线运行时直接复用。
GitHub Actions 示例:
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
**/node_modules
key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }}
这样一来,90% 的构建时间花在真正需要更新的部分,而不是重复劳动。
构建慢不是容器化的锅,更多是使用方式的问题。就像做饭,刀工好、火候准,炒菜自然快。把这些细节理顺,每天节省下来的构建时间,够你多跑十几轮测试了。