一、什么是 Docker 层(Layer)?

Docker 镜像由一系列只读层(Read-only Layers)构成,每层都代表一次文件系统的变更。这种分层架构的设计来源于 Linux 的联合文件系统(UnionFS),它允许多个只读层叠加,并在最上层添加一个可写层(Writable Layer)供容器运行时使用。

每个镜像层:

  • 是前一层的“快照 + 差异”

  • 是不可修改、不可变的

  • 可以被多个镜像复用(共享层)

容器运行时,Docker 在镜像之上加一层可写层,称为容器层(Container Layer)。


二、Docker 构建流程中的层生成过程

每一条 Dockerfile 指令都会生成一个层。以下是构建流程的图示与说明:

FROM ubuntu:20.04          # 层1:基础镜像
RUN apt update             # 层2:更新缓存
RUN apt install -y curl    # 层3:安装 curl
COPY . /app                # 层4:复制项目文件

构建时过程

+-------------------------+       
|    应用文件 /app         |        <- Layer 4(COPY)
+-------------------------+
|    安装 curl 的效果      |       <- Layer 3(RUN)
+-------------------------+
|   apt update 的修改     |        <- Layer 2(RUN)
+-------------------------+
|    ubuntu:20.04 镜像    |       <- Layer 1(FROM)
+-------------------------+

运行容器时

+-------------------------+
|    容器可写层           |   ← 只有容器运行时才有
+-------------------------+
|    只读层4              |
|    只读层3              |
|    只读层2              |
|    只读层1              |
+-------------------------+

三、层的存储位置与结构

Docker 将每一层保存在宿主机的存储驱动中(如 overlay2、aufs、btrfs 等)。

在 overlay2 驱动下,镜像层通常位于:

/var/lib/docker/overlay2/

每层实际存储为一个目录,包含文件内容(diff)、元数据(lowerdir/upperdir)等结构。Docker 通过联合挂载将多个层合并视图提供给容器。


四、Docker 缓存与层的重用机制

Docker 会根据每一条 Dockerfile 指令的输入内容(命令 + 上一层 hash + 文件变更)判断是否命中缓存。

缓存命中条件:

  1. 指令未变(如 RUN apt install

  2. 所引用的文件未变化(如 COPY package.json

  3. 上一层 hash 一致

缓存失效场景示例:

COPY . /app
RUN npm install

哪怕你只改了一个 .md 文件,也会让 COPY . 缓存失效,从而导致 npm install 也要重新执行。

正确的顺序:

COPY package*.json ./
RUN npm install
COPY . .

这样小改动不会影响前面的层,提升构建效率。


五、“删除不等于释放”:隐藏的层数据

如果你在一层中删除文件:

RUN rm -rf /tmp/huge_file

这个文件依然存在于构建镜像中,因为它是在前一层创建的,Docker 的层是不可变的。新的层只能打一个“删除标记”,叫做 whiteout 文件

💡 所以删除不等于减小体积,只有重新构建(不生成那个文件的层)才行。


六、多阶段构建(Multi-stage Build)

通常,我们构建应用时需要很多依赖或构建工具,比如:

  • Go 的构建工具链

  • Node.js 的打包工具

  • Java 的 Maven/Gradle

这些依赖对构建是必须的,但对最终运行时并没有用,如果直接把这些工具一并打包进镜像,就会导致:

  • 镜像变大

  • 存在安全隐患(多余工具)

  • 启动效率下降

多阶段构建它的目标就是:在一个阶段里负责“构建”,另一个阶段里只保留运行时所需内容。

原始写法(臃肿镜像)

FROM golang:1.20
WORKDIR /app
COPY . .
RUN go build -o main
CMD ["./main"]

问题:最终镜像包含整个 Go 编译器、源码、临时文件,体积可达 700MB+

使用多阶段构建

# 第一阶段:构建阶段
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN go build -o main
​
# 第二阶段:运行阶段(精简)
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/main .
CMD ["./main"]

图示如下:

+---------------------------+       <- 第1阶段:builder
| Go 工具链 + 源码 + 构建产物 |
+---------------------------+
​
        ↓ 只复制编译产物 main
​
+---------------------------+       <- 第2阶段:最终镜像
|       main 可执行文件     |
+---------------------------+

✨ 最终镜像只有几 MB,大大减少体积!

原理总结

  • 每个 FROM 都代表一个新的构建阶段。

  • COPY --from=阶段名 语法可以把前面构建阶段的文件复制过来。

  • 只将“需要部署运行”的内容复制进最终镜像。

语言示例补充

Node.js 多阶段构建

FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
​
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

Python 多阶段构建(适用于打包成二进制的情况)

FROM python:3.11 AS builder
WORKDIR /app
COPY . .
RUN pip install pyinstaller && pyinstaller app.py
​
FROM alpine
COPY --from=builder /app/dist/app /app/app
CMD ["/app/app"]

七、常见问题与误区

COPY 全目录导致缓存失效

错误写法:直接 COPY . /app 会导致什么?

COPY . /app
RUN npm install

为什么不好

  • COPY . /app 会把所有文件(包括代码、配置、README、日志等)复制进去。

  • 只要你修改了任意一个文件,哪怕是注释或 README,Docker 就认为 . 有变,就会使得 这条 COPY 命令之后的所有缓存都失效

  • 尤其是 RUN npm install 又慢又重,每次都重新执行,浪费时间。

正确做法:分阶段 COPY,精细控制缓存

# 只先复制依赖文件
COPY package*.json /app/
RUN npm install
​
# 再复制代码(可能会频繁变)
COPY src/ /app/src/

好处

  • 如果你只改了代码,不改依赖package.json 没动),RUN npm install 会使用缓存 ✅

  • 如果你改了依赖:才会重新 npm install,这是合理的 ⏱️

图解构建缓存失效

┌────────────┐        ┌──────────────────────┐
│ package.json│─────▶│ COPY + npm install    │──▶ 缓存有效 ✔️
└────────────┘        └──────────────────────┘
                           │
┌────────────┐        ┌──────────────────────┐
│  src/ 文件  │─────▶│ COPY src/ + 构建阶段   │──▶ 缓存可控
└────────────┘        └──────────────────────┘

而错误做法是:

┌────────────┐
│  全部文件   │────▶ COPY .(一改全部失效) ──▶ npm install 每次都执行 ❌
└────────────┘

没用 .dockerignore

很多初学者没有配置 .dockerignore,导致 .gitnode_modules 被打包进去。

推荐配置

.git
node_modules
*.log
Dockerfile
*.md

没清理缓存

RUN apt install -y curl  # 会留下缓存

推荐写法:

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

八、查看镜像层结构与大小

命令:docker history

docker history your-image-name

输出示例:

IMAGE       CREATED       SIZE       CREATED BY
<id>        2 hours ago   145MB      COPY . /app
<id>        2 hours ago   100MB      RUN apt install curl
...

可以快速识别哪些层比较大。


九、最佳实践清单

操作

推荐原因

合并 RUN

减少层数

使用多阶段构建

减少产物体积

使用 .dockerignore

避免无关文件

精确 COPY 指定文件

减少缓存失效

清理构建缓存与中间产物

降低层体积

关注镜像层顺序

控制缓存失效与加速构建

使用 docker history 分析

了解镜像构成,定位异常大层


十、总结:掌控层,才能掌控镜像

Docker 的分层机制是容器构建高效、可复用的关键,但如果不了解其底层逻辑,可能会带来:

  • 镜像臃肿

  • 构建缓慢

  • 缓存频繁失效

掌握层的本质,结合 .dockerignore、合理顺序、缓存机制与多阶段构建等手段,才能真正构建出高效、轻量、可维护的镜像。