Docker 镜像分层原理与构建优化
Docker 镜像分层原理与构建优化
一、镜像为什么是分层的
Docker 镜像不是一个单一文件,而是由多层只读文件系统叠加而成。Dockerfile 里每一条指令(FROM、RUN、COPY、ADD)通常都会生成一层。下层只做增量修改,构建时相同层可被多个镜像共享,这就是镜像复用、拉取提速的基础。
二、分层与联合文件系统
底层用联合挂载(UnionFS)把多层目录合并成一个统一视图,容器启动时在最顶层加一层可写层,所有运行时改动都写在这层。镜像层只读,容器层可写,删除容器时可写层随之消失。
FROM php:8.2-cli
RUN apt-get update && apt-get install -y libzip-dev
COPY ./app /var/www/html
CMD ["php", "-S", "0.0.0.0:8000", "/var/www/html"]
每一行对应一层,基础镜像的层被复用,只新增自己的改动层。
三、构建缓存如何工作
Docker 按 Dockerfile 顺序逐层构建,某一层的指令和上游层未变化时,直接复用已有缓存层,不必重新执行。这就是改了一行代码却不必重装依赖的原因。
四、优化一:把变化少的指令放前面
依赖安装基本不变,业务代码频繁变动。顺序反过来会导致每次改代码都重装依赖:
# 推荐:先装依赖(缓存稳定),再拷代码
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "start"]
COPY . . 每次代码变更都会让它之后的层失效,必须放在 npm ci 之后。
五、优化二:合并 RUN、清理缓存
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
多个 RUN 会产生多层且缓存残留无法删除;合并成一层并在同层清理安装缓存,镜像更小。
六、优化三:多阶段构建
编译产物和构建工具分开,最终镜像只留运行所需:
# 构建阶段
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o app
# 运行阶段,只拷贝二进制
FROM alpine:latest
COPY --from=builder /src/app /app
CMD ["/app"]
最终镜像不带 Go 工具链,体积大幅下降。
七、其他实用技巧
- 用
.dockerignore排除 node_modules、.git、日志,避免无谓拷入; - 选小基础镜像(alpine、distroless),但注意 glibc 兼容;
COPY优于ADD,ADD 会自动解压缩带来不确定性;- 避免
FROM latest这种浮动标签,保证可复现。
八、常见误区
- 把
COPY . .放在 RUN 安装依赖之前,缓存全失效; - 每条 RUN 单独写,镜像层层叠加又删不掉缓存;
- 直接用超大基础镜像塞进编译工具;
- 忽略 .dockerignore,把源码和依赖目录一起打进镜像;
- 以为分层越多越好,其实多余层只增体积不增价值。
九、总结
镜像分层让相同层共享、构建可缓存;优化关键是稳定指令前置、合并 RUN 并清理、用多阶段构建只留运行产物、配合 .dockerignore 和小基础镜像。理解层与缓存机制,才能把镜像做小、构建做快。