Docker 镜像为什么越建越慢?一文讲透分层缓存与 5 个优化实践
一、从一个常见现象说起
很多同学刚开始用 Docker 时,会发现一个奇怪的现象:第一次 docker build 只要两分钟,改了一行代码之后重新构建,却要等上十几分钟。明明只改了一个文件,为什么整个镜像都要重来?要回答这个问题,得先理解 Docker 镜像的分层结构。
二、镜像不是"一个文件",而是一摞只读层
Docker 镜像由一层层的只读文件系统组成,每一层都记录着相对上一层的增量变化。docker history 可以把这些层列出来:
docker history myapp:latest当我们执行 FROM ubuntu:22.04、RUN apt-get install、COPY . /app 时,Docker 会为每条指令生成一层。层与层之间通过内容寻址的方式关联,相同内容的层可以被多个镜像共享,这也是为什么拉取第二个基于同一基础镜像的应用时,速度会快很多。
容器启动时,Docker 在所有只读层之上再加一个可写层。所有写操作都发生在可写层,删除容器时这一层也随之消失——这就是"容器无状态"的底层原因。
三、缓存的命中规则:命中即跳过,未命中则全部重来
构建时,Docker 会按 Dockerfile 的顺序逐条检查指令。如果某条指令与上一次构建完全一致,就直接复用缓存层;一旦某条指令发生变化,它以及它后面的所有指令都会重新执行。
关键在于"完全一致"这四个字:COPY 和 ADD 会计算文件内容的校验和,文件内容、权限、时间戳的细微变化都会导致缓存失效。所以下面这种写法几乎必然踩坑:
COPY . /app
RUN npm install只要改了一行业务代码,COPY . /app 的校验和就变了,后面的 npm install 自然全部重跑。
四、五个立竿见影的优化
- 先拷贝依赖清单,再安装依赖。把
package.json和锁文件单独 COPY 进去,安装完依赖后再 COPY 源码。这样改业务代码不会影响依赖层。
- 合并 RUN 指令并及时清理。
apt-get update和apt-get install放在同一条 RUN 里,安装完立刻rm -rf /var/lib/apt/lists/*,避免缓存文件留在层里。
- 使用多阶段构建。编译工具链只出现在 builder 阶段,最终镜像只保留运行产物,体积往往能缩小一个数量级。
- 合理使用 .dockerignore。把 node_modules、.git、日志、测试数据排除在构建上下文之外,既加快上下文传输,也避免无谓的缓存失效。
- 固定基础镜像版本。用
ubuntu:22.04而不是ubuntu:latest,避免基础镜像悄悄更新导致整条缓存链断裂。
五、一个可以直接套用的模板
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80这个模板把"变化快的源码"放在"变化慢的依赖"之后,配合多阶段构建,既快又小。
六、写在最后
理解分层和缓存,不是为了背几条口诀,而是为了在写 Dockerfile 时能预判"这一行会不会让缓存失效"。把变化频率不同的内容分层放置,是 Docker 构建优化的第一性原理。下次构建变慢时,不妨先跑一遍 docker history,看看究竟是哪一层在反复重建。
- 点赞
- 收藏
- 关注作者
评论(0)