Docker 镜像分层与构建缓存的底层逻辑
镜像是分层的只读压缩包
一个 Docker 镜像本质上是一串**只读层(layer)**叠加,加上一份描述这些层关系的 manifest。
docker image inspect nginx:alpine --format '{{json .RootFS.Layers}}' | jq .
每执行一条会改变文件系统的指令(RUN、COPY、ADD),Docker 就产生一个新的层。层是内容寻址的:内容相同 → layer digest 相同 → 直接复用。
这解释了镜像仓库的两个现象:
- 拉取时显示
Already exists —— 本地已有该层,跳过下载。
- 不同镜像共享基础层 ——
python:3.12-slim 的层在多个项目间复用,只存在一份。
容器可写层
容器启动时,Docker 在镜像之上挂一个可写层。所有写操作都用 Copy-on-Write:修改一个文件时,先把它从下层复制上来再改。
docker run -d --name demo nginx
docker exec demo sh -c 'echo hello > /usr/share/nginx/html/index.html'
docker diff demo
# C /usr/share/nginx/html/index.html
docker diff 展示的就是可写层相对镜像的变更。容器一删,可写层随之消失——这是容器无状态的物理基础。
构建缓存:命中的条件
docker build 会逐条判断指令能否复用缓存。判断依据有三项:
- 指令文本是否完全一致;
- 依赖的父层 digest 是否一致;
- 对
COPY / ADD 而言,源文件的内容校验和是否一致。
一旦某层缓存失效,它之后的所有层都会失效。这就是 Dockerfile 顺序至关重要的原因。
反面教材
FROM python:3.12-slim
WORKDIR /app
COPY . . # 代码一变,这层就失效
RUN pip install -r requirements.txt # 于是每次都要重装依赖
CMD ["python", "app.py"]
正确写法
FROM python:3.12-slim
WORKDIR /app
# 先只拷贝依赖清单,利用缓存
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 再拷贝会频繁变化的代码
COPY . .
CMD ["python", "app.py"]
只要 requirements.txt 不变,依赖安装层的缓存就一直有效。日常改代码时构建时间能从分钟级降到秒级。
多阶段构建:砍掉构建期污染
编译型语言的最佳实践。
# 构建阶段
FROM golang:1.24 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/server .
# 运行阶段:只带二进制
FROM gcr.io/distroless/static-debian12
COPY --from=builder /out/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
最终镜像只有十几 MB,且不含编译器、shell、包管理器——攻击面大幅缩小。
几个容易忽略的点
| 做法 |
效果 |
--no-cache-dir(pip)/ --no-install-recommends(apt) |
减小层体积 |
合并 RUN 并用 && 清理临时文件 |
减少层数 |
.dockerignore 排除 node_modules、.git |
避免缓存失效 + 加速上下文传输 |
| 固定基础镜像 digest |
构建可复现 |
小结
- 层是内容寻址的只读包,缓存失效会「连坐」后续所有层。
- Dockerfile 的书写顺序本质上是在按变更频率排序。
- 多阶段构建是体积与安全性的双赢解。