深入理解 Docker 的“层”(Layer)

深入理解 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 时由 FROMRUNCOPY 等指令生成
镜像有层吗? 有,镜像本身就是多层只读层的堆叠
容器有层吗? 有,在镜像层之上额外加一个可写层(容器层)
容器层能修改吗? 可以,但容器删除后改动消失
为什么能共享基础层? 多个镜像指向同一个底层,磁盘上只存一份
为什么构建快? 没变的层直接复用缓存,只重建变动的层

掌握了层的概念,你就不再只是会用 Docker,而是真正理解了 Docker。这会让你在优化 Dockerfile、排查存储问题、设计 CI/CD 流水线时,都游刃有余。

相关推荐
梦梦代码精4 小时前
PHP 还是 Java?LikeShop 多版本对比,附6条落地避坑建议
低代码·docker·开源·代码规范
运维大师5 小时前
【K8S 运维实战】15-链路追踪Jaeger与OTel
运维·容器·kubernetes
霁月的小屋6 小时前
Docker 工程化实践(二):理解镜像分层与容器文件系统
运维·docker·容器
码上上班1 天前
docker课程
java·docker·容器
取谖慕12.1 天前
Docker容器从入门到精通
docker·容器·eureka
取谖慕12.1 天前
Harbor仓库从讲解到部署实战
docker·harbor
逍遥德1 天前
运维技术栈Linux+docker+Kubernetes+Jenkins/GitLab CI 知识点详细列表
linux·运维·docker
名字还没想好☜1 天前
Kubernetes CrashLoopBackOff 排查全流程:从 describe 到日志到 exit code
运维·docker·云原生·容器·kubernetes
冰封之寂1 天前
Docker 部署与基础命令详解:从安装到容器管理全流程指南
linux·运维·docker·容器