容器

Dockerfile 构建原理 + 镜像分层优化

·15 分钟阅读·5858 字

说明 Dockerfile 构建流程、镜像分层缓存机制以及减少镜像体积的优化方法

📋 目录

Dockerfile 构建原理 + 镜像分层优化

核心定义 Dockerfile 是 Docker 镜像的声明式构建文件。Docker 按顺序执行 Dockerfile 指令,将每一步产生的文件系统变化保存为镜像层,并通过层缓存、联合文件系统和内容寻址机制实现镜像复用与快速构建。

1. Dockerfile 构建的基本流程

Docker 构建镜像时,核心流程如下:

  1. 读取当前构建上下文(build context)。
  2. 解析 Dockerfile 指令。
  3. 按顺序执行每条指令。
  4. 对产生文件系统变化的指令生成新的镜像层。
  5. 复用未变化的缓存层。
  6. 将最终层与镜像元数据组合成一个镜像。
Dockerfile
  ↓
构建上下文 build context
  ↓
逐条执行指令
  ↓
生成或复用镜像层
  ↓
得到最终镜像

构建结果并不是一个单独的大文件,而是一组只读镜像层加上镜像配置元数据。容器运行时会在这些只读层之上增加一个可写容器层。详见 Docker 联合文件系统。

2. 构建上下文与 .dockerignore

2.1. 构建上下文是什么

执行以下命令时,最后的 . 表示构建上下文目录:

docker build -t myapp:latest .

Docker 会将构建上下文发送给构建引擎,Dockerfile 中的 COPY、ADD 只能访问构建上下文内的文件。

构建上下文风险 如果上下文目录过大,构建会变慢;如果包含密钥、日志、依赖缓存等无关文件,还可能被误打入镜像层。

2.2. .dockerignore 的作用

.dockerignore 用于排除不需要进入构建上下文的文件。

.git/
node_modules/
*.log
.env
README.md
target/
dist/

常见作用:

  1. 减少构建上下文大小。
  2. 避免无关文件影响缓存命中。
  3. 防止密钥、日志、临时文件进入镜像。
  4. 减少镜像层中的无效内容。

3. Dockerfile 指令与镜像层

3.1. 常见指令分类

指令是否常产生文件层主要作用
FROM是指定基础镜像,作为第一批只读层
RUN是在构建阶段执行命令,生成新的文件系统层
COPY是从构建上下文复制文件到镜像
ADD是复制文件,额外支持自动解压和远程 URL
ENV通常否设置环境变量,写入镜像配置
WORKDIR通常否设置工作目录,必要时创建目录
CMD否设置容器默认启动命令或参数
ENTRYPOINT否设置容器固定入口
EXPOSE否声明容器监听端口
VOLUME否声明挂载点

是否生成文件层取决于指令是否改变文件系统。RUN、COPY、ADD 通常最容易增加镜像层和镜像体积。

3.2. RUN 指令为什么会形成层

每条 RUN 指令都会在临时容器中执行命令,并将执行后的文件系统差异提交成新层。

FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y nginx
RUN rm -rf /var/lib/apt/lists/*

上面写法的问题是:清理动作发生在后续层,前面层中产生的缓存文件仍可能保留在镜像历史层中。更推荐写成一条 RUN:

FROM ubuntu:22.04
RUN apt-get update \
    && apt-get install -y --no-install-recommends nginx \
    && rm -rf /var/lib/apt/lists/*

这样安装和清理发生在同一层中,可以减少最终镜像体积。

4. 构建缓存原理

4.1. 缓存命中的基本条件

Docker 构建时会从上到下检查每条指令是否可以复用缓存。缓存命中通常依赖以下条件:

  1. 当前指令文本未变化。
  2. 前面的层未失效。
  3. COPY 或 ADD 涉及的文件内容未变化。
  4. 构建参数、基础镜像等输入未发生影响缓存的变化。

只要某一层缓存失效,后续层通常都需要重新构建。

第 1 层命中缓存
第 2 层命中缓存
第 3 层发生变化
第 4 层以及后续层重新构建

4.2. 指令顺序对缓存的影响

Dockerfile 应遵循一个原则:

缓存优化原则 越稳定、越少变化的指令越靠前;越频繁变化的源代码、配置文件越靠后。

低效写法:

FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
CMD ["node", "server.js"]

问题是:只要任意源代码文件变化,COPY . . 就会导致后面的 npm ci 缓存失效。

优化写法:

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]

优化后,只有依赖描述文件变化时才重新安装依赖;普通业务代码变化不会让依赖安装层失效。

5. 镜像分层优化方法

5.1. 合并相关 RUN 指令

多个相关操作应尽量放在同一个 RUN 中完成,尤其是安装与清理。

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

这样可以避免包管理器缓存、临时文件残留在历史层中。

5.2. 减少不必要的 COPY

COPY . . 简单但风险较高,容易把无关文件复制进镜像。更好的做法是:

  1. 使用 .dockerignore 排除无关内容。
  2. 优先复制依赖清单,再复制业务代码。
  3. 只复制运行时需要的文件。
COPY app.py requirements.txt /app/

5.3. 使用更小的基础镜像

基础镜像越大,最终镜像通常越大。常见选择:

基础镜像特点适用场景
ubuntu / debian兼容性好,体积较大依赖复杂、排障便利性优先
alpine体积小,使用 musl libc依赖简单、追求小体积
distroless只包含运行时必要文件生产运行镜像、安全收敛
scratch空镜像静态编译二进制

Alpine 不是万能选择 Alpine 使用 musl libc,某些依赖 glibc 的程序可能存在兼容性问题。选择基础镜像时需要平衡体积、兼容性和排障成本。

5.4. 使用多阶段构建

多阶段构建用于将“编译环境”和“运行环境”分离,避免把编译器、源码、构建缓存带入最终镜像。

FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app

FROM alpine:3.20
WORKDIR /app
COPY --from=builder /out/app /app/app
CMD ["/app/app"]

最终镜像只包含运行二进制和必要运行环境,不包含 Go 编译器、源码和构建缓存。

5.5. 避免在镜像中保存敏感信息

禁止将以下内容写入镜像层:

  • .env 文件
  • SSH 私钥
  • 云厂商 AK/SK
  • 数据库密码
  • Token、证书私钥
  • 临时调试文件

即使后续层删除了敏感文件,历史层中仍可能保留。敏感配置应通过环境变量、密钥管理系统、Kubernetes Secret 或运行时挂载注入。

6. CMD 与 ENTRYPOINT 的区别

6.1. CMD

CMD 定义容器默认启动命令或默认参数,容易被 docker run 后面的命令覆盖。

CMD ["nginx", "-g", "daemon off;"]

如果执行:

docker run image_name ls -l

ls -l 会覆盖原来的 CMD。

6.2. ENTRYPOINT

ENTRYPOINT 定义容器固定入口,docker run 后面的参数通常会作为入口命令的参数。

ENTRYPOINT ["/start.sh"]
CMD ["default"]

执行:

docker run image_name abc

实际效果类似:

/start.sh abc

6.3. 组合使用场景

常见模式是:

  • ENTRYPOINT 固定主程序。
  • CMD 提供默认参数。
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
CMD ["--spring.profiles.active=prod"]

这样既能固定启动入口,又允许运行时覆盖默认参数。

7. ADD 与 COPY 的选择

推荐原则:优先使用 COPY,只有明确需要 ADD 的额外能力时才使用 ADD。

指令能力推荐使用场景
COPY复制本地文件或目录大多数文件复制场景
ADD支持本地压缩包自动解压、远程 URL明确需要自动解压时

不建议使用 ADD 下载远程文件。远程下载更推荐在 RUN 中使用 curl 或 wget,并校验哈希值。

8. 常见低效写法与优化示例

8.1. 包管理器缓存未清理

低效写法:

RUN apt-get update
RUN apt-get install -y curl

优化写法:

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

8.2. 过早复制全部源码

低效写法:

COPY . .
RUN pip install -r requirements.txt

优化写法:

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

8.3. 镜像中包含构建产物和源码

低效写法:

FROM maven:3.9-eclipse-temurin-17
WORKDIR /src
COPY . .
RUN mvn package
CMD ["java", "-jar", "target/app.jar"]

优化方向:使用多阶段构建,只把最终 jar 包复制进运行镜像。

FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /src
COPY pom.xml .
COPY src ./src
RUN mvn -DskipTests package

FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=builder /src/target/app.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]

9. 排查与验证命令

9.1. 查看镜像历史层

docker history image_name:tag

用于观察每一层由哪条指令产生,以及大致体积。

9.2. 查看镜像详细信息

docker image inspect image_name:tag

可以查看镜像的 Config、RootFS、环境变量、入口命令等信息。

9.3. 无缓存构建

docker build --no-cache -t image_name:tag .

用于确认构建过程是否依赖旧缓存,但会牺牲构建速度。

9.4. 指定 Dockerfile 构建

docker build -f Dockerfile.prod -t image_name:prod .

适用于同一项目存在多个 Dockerfile 的场景。

10. 最佳实践清单

  • 使用 .dockerignore 排除无关文件和敏感文件。
  • 稳定指令放前面,频繁变化的 COPY 放后面。
  • 依赖安装和缓存清理放在同一个 RUN 中。
  • 优先使用 COPY,谨慎使用 ADD。
  • 使用多阶段构建分离构建环境和运行环境。
  • 不在镜像层中写入密钥、Token、密码和证书私钥。
  • 使用合适的基础镜像,平衡体积、兼容性和排障成本。
  • 通过 docker history 检查异常大层。
  • 运行时数据使用 volume 或 bind mount,而不是写入容器层。

11. 面试回答模板

标准回答 Dockerfile 构建镜像时,会按照指令顺序逐层执行。FROM 引入基础镜像层,RUN、COPY、ADD 等会产生新的文件系统层,CMD、ENTRYPOINT、ENV 等主要写入镜像配置。Docker 构建缓存按顺序命中,前面某一层失效后,后续层通常都需要重新构建。因此 Dockerfile 优化的核心是:稳定指令靠前、易变文件靠后,使用 .dockerignore 减少构建上下文,合并安装与清理命令,避免敏感信息进入镜像层,并通过多阶段构建只保留运行时需要的文件。这样可以减少镜像体积、提升构建速度,并降低安全风险。


关联文档

  • Docker 联合文件系统:理解镜像层、容器层和写时复制机制。
  • Docker 三种挂载:理解运行时数据为什么不应保存在容器层。
  • 容器技术:容器镜像、运行时和隔离机制的整体入口。
Yanche Blog

记录云原生、Linux、数据库等技术领域的学习心得,以及日常生活的思考与感悟。

© 2026 Yanche Blog. All rights reserved.

Powered by Astro