docker镜像由多层只读增量快照构成,每层对应dockerfile一条指令,记录文件差异;unionfs(如overlay2)通过叠加只读层与可写层实现统一视图,支持写时复制。

理解Docker镜像分层存储,关键在于抓住两个核心:镜像不是“一个整体文件”,而是一叠可复用的只读快照;UnionFS(如 overlay2)是让这叠快照“看起来像一个完整文件系统”的底层机制。
镜像层 = 增量变更快照
每一层对应 Dockerfile 中一条指令(比如 RUN apt install nginx 或 COPY app.py /app/),它不保存整个文件系统,只记录与上一层相比新增、修改或删除了哪些文件——也就是一个差异包(diff)。基础镜像(如 ubuntu:22.04)本身也由多层构成,最底下是精简的 bootfs + rootfs。所有层默认只读,不可更改。
- 相同基础镜像的不同应用镜像(如 python:3.11-slim 和 golang:1.22)会共享底层共用层,节省磁盘空间
- 执行 docker history 镜像名 可直观看到每层的构建命令、大小和创建时间
- 层是缓存单位:某一层变动,其后所有层在下次构建时都会重建
UnionFS 是怎么把多层“变”成一个文件系统的?
以当前主流的 overlay2 驱动为例:启动容器时,Docker 把镜像的所有只读层 + 一个空的可写层,按顺序叠加挂载到同一个路径下。你访问 /bin/bash,系统从顶向下查找——先看可写层有没有,没有就穿透到第一层只读层,再没有就继续往下,直到找到或确认不存在。
- 写新文件 → 直接写入可写层
- 改已有文件 → 先从某只读层复制该文件到可写层,再编辑(Copy-on-Write)
- 删文件 → 在可写层生成 .wh.filename 标记,屏蔽下层同名文件
为什么分层设计既高效又容易“踩坑”?
分层带来复用和快速启动,但也让构建逻辑和镜像体积变得敏感。比如 RUN apt update && apt install curl && apt clean 如果拆成三条指令,apt clean 就无法清理前两层中已下载的包缓存,导致镜像虚胖。
- 把关联操作写进同一 RUN 指令,避免中间产物残留
- 用多阶段构建(multi-stage):编译环境用大镜像,最终运行只 COPY 二进制和必要配置,丢弃全部构建依赖层
- 定期执行 docker image prune -a 清理未被任何镜像引用的悬空层(dangling layers)
实际验证:看看镜像层到底长啥样
在 Linux 主机上,overlay2 的真实结构藏在 /var/lib/docker/overlay2/ 目录里。每层有独立目录,内容存于 diff/ 子目录;元数据(如各层 SHA256 哈希、依赖关系)则记录在 /var/lib/docker/image/overlay2/imagedb/content/sha256/ 下。这些路径印证了:镜像不是黑盒,而是可追溯、可验证的分层对象。










