深入理解 Docker 的"层"(Layer)
如果说镜像和容器是 Docker 的血肉,那么层就是构成这一切的骨骼。理解了层,你就能透彻地明白 Docker 为什么构建快、为什么镜像能共享、为什么容器删了数据会丢。
一、一句话定义
层是 Docker 镜像中一个只读的文件系统增量快照,由 Dockerfile 中的指令在构建时生成,通过联合文件系统叠加成完整的镜像。
简单说:镜像不是一整块,而是一层层透明玻璃纸叠出来的。
二、层从哪来?Dockerfile 指令与层的对应关系
每当你执行 docker build,Docker 会逐条解析 Dockerfile 里的指令,大部分指令都会产生一个新层。
| 指令 | 是否产生新层 | 层里存了什么 |
|---|---|---|
FROM |
是 | 基础镜像的所有层(本身就是多层叠加) |
RUN |
是 | 执行命令后文件系统的变化(新增、修改、删除的文件) |
COPY |
是 | 从宿主机拷入的文件 |
ADD |
是 | 同 COPY,也可能包含解压后的文件 |
WORKDIR |
否 | 仅修改元数据,不产生层 |
ENV |
否 | 仅修改环境变量,不产生层 |
CMD / ENTRYPOINT |
否 | 仅记录启动命令,不产生层 |
所以,一个包含 FROM + 3 个 RUN + 1 个 COPY 的 Dockerfile,最终会产生大约 5 个只读层。
三、层的物理形态:增量快照,不是完整拷贝
这是最关键的认知:层不是完整文件系统,它只记录"相对于下一层的变化"。
举个例子,三层叠加:
- 第 1 层(ubuntu 基础) :包含
/bin/bash、/bin/ls、/etc/os-release等所有基础文件。 - 第 2 层(apt-get install vim) :只记录"新增了
/usr/bin/vim和相关的依赖文件",它不重复存第 1 层已有的/bin/bash。 - 第 3 层(COPY my-app) :只记录"新增了
/app/my-app"。
这些层通过联合文件系统(UnionFS,Docker 默认用 overlay2) 从下往上叠加,最终呈现给你的是一个完整、统一的文件系统视图。就像把几张透明玻璃纸叠在一起,你从上面往下看,看到的是所有纸上的内容拼成的完整画面。
如果你在第 3 层删除了 /bin/bash,它并不是真的把第 1 层的文件物理删除,而是在第 3 层放一个"白障文件"标记该文件已删除。第 1 层的 /bin/bash 实际还在,只是被遮挡看不到了。所以镜像是只读的,你无法真正修改底层,只能在上面覆盖。
四、镜像 vs 容器的层结构
这是初学者最容易混淆的点,我用两张图帮你彻底分清:
镜像的结构(全是只读层)
+------------------+
| 第4层: COPY | 只读(你的源码)
+------------------+
| 第3层: RUN | 只读(装第三方库)
+------------------+
| 第2层: RUN | 只读(装系统依赖)
+------------------+
| 第1层: FROM | 只读(基础 ubuntu)
+------------------+
这些层从 docker build 完成那一刻就存在了,存储在宿主机磁盘上,可以被不同镜像共享。
容器的结构(镜像层 + 一个可写层)
+------------------+
| 容器层(可写) | <-- 这是容器独有的,可读写
+------------------+
| 第4层: COPY | 只读
+------------------+
| 第3层: RUN | 只读
+------------------+
| 第2层: RUN | 只读
+------------------+
| 第1层: FROM | 只读
+------------------+
当你 docker run 启动容器时,Docker 在所有只读层之上临时贴一张可写层(容器层) 。你在容器里 touch 一个文件、apt-get install 一个包,所有写操作都发生在这张可写层里,底下的镜像层纹丝不动。
当你删除容器时,撕掉的只是这张可写层,镜像层毫发无伤。这也是为什么容器删除后数据会丢失 的根本原因------你的改动都在那张被扔掉的纸上。要想数据持久化,必须用 -v 挂载卷,它绕过容器层直接写宿主机磁盘。
五、层的两大超级能力
1. 共享基础层,节省磁盘
假设你有三个镜像:
my-ros:1.0(基于ubuntu:22.04)my-node:1.0(也基于ubuntu:22.04)my-python:1.0(也基于ubuntu:22.04)
这三个镜像的 ubuntu:22.04 底层,在宿主机上只存一份 。三个镜像都通过指针引用它,磁盘占用不是 基础层大小 × 3,而是 基础层大小 × 1 + 各自独有层大小。这就是容器能在一台机器上跑成百上千个的原因之一。
2. 缓存复用,极速构建
这是 docker build 飞快的秘密。
每次构建,Docker 会逐条检查指令。对于每条指令,它会看:
- 指令本身(如
RUN apt-get install vim)有没有变? - 它下面那层有没有变?
如果都没变,Docker 就不重新执行,直接复用上次构建生成的层快照,控制台输出 ---> Using cache。一旦某一层发现变化(比如 COPY ./src 的源码改了),从这层开始往下,所有层都必须重建。
这就是为什么要把代码 COPY 放在 Dockerfile 最后面:
- 系统依赖(
RUN apt-get)几乎不变,永远用缓存,瞬间跳过。 - 你改了代码,只需要重建最后那一两层,整次构建从十几分钟变成几十秒。
这个优化在 CI/CD 流水线里格外重要------每次代码提交都会触发自动构建,如果每次都从头装依赖,流水线根本跑不动。
六、用一种形象的方式全部串起来
用书本做比喻:
- 每个层就像一页透明胶片,上面写着文件的变化。
- 镜像就是装订好的一本只读胶片册。
- 容器是在这本册子最上面夹了一张空白可写的透明纸。你在上面涂写,撕掉它就什么也不剩。
- 构建缓存就是:如果这一页内容没变,直接从复印机里拿上次印好的那页,不再重新排版。
用蛋糕做比喻:
- 蛋糕胚 =
FROM ubuntu层 - 奶油 =
RUN apt-get install层 - 水果 =
COPY ./src层 - 整个蛋糕 = 镜像(多层堆叠)
- 装盘 = 容器(蛋糕上盖一层保鲜膜,在上面挤酱,不破坏蛋糕本身)
- 再做一次蛋糕时,只要配方前几步一样,就直接从冰箱取半成品,只重新摆水果 = 缓存构建
七、一张表总结
| 问题 | 答案 |
|---|---|
| 层是什么? | 只读的文件系统增量快照,记录相对于下一层的文件变化 |
| 层怎么产生? | docker build 时由 FROM、RUN、COPY 等指令生成 |
| 镜像有层吗? | 有,镜像本身就是多层只读层的堆叠 |
| 容器有层吗? | 有,在镜像层之上额外加一个可写层(容器层) |
| 容器层能修改吗? | 可以,但容器删除后改动消失 |
| 为什么能共享基础层? | 多个镜像指向同一个底层,磁盘上只存一份 |
| 为什么构建快? | 没变的层直接复用缓存,只重建变动的层 |
掌握了层的概念,你就不再只是会用 Docker,而是真正理解了 Docker。这会让你在优化 Dockerfile、排查存储问题、设计 CI/CD 流水线时,都游刃有余。