Docker镜像构建与分层原理

你有没有想过,Docker 镜像那么大,为什么下载起来却很快?我刚开始接触容器时也挺困惑的。直到我理解了分层原理,才明白其中的门道。

1. 镜像的“千层饼”结构

说白了,每个 Docker 镜像都是一层一层堆起来的。每一层代表一个文件系统的变更。这些层是只读的,而且可以被多个镜像共享。

  • 基础层 (base layer):最底下的那一层,通常是一个最小化的操作系统,比如 Ubuntu 或 Alpine。
  • 中间层 (intermediate layer):每运行一条 Dockerfile 指令,就会生成一个新层。比如 RUN apt-get update 会创建一个层。
  • 顶层 (top layer):容器运行时,最上面会加一个可写层。你对容器做的一切修改都写在这里。

为什么会分层?其实是为了复用。你想想看,如果 100 个容器都基于同一个 Ubuntu 镜像,它们就共享同一份基础层,不用重复下载。我在项目中遇到过,一个团队把基础镜像做到 20MB 左右,上线时拉取速度飞快。

2. UnionFS——联合文件系统

Docker 默认使用 OverlayFS(一种 UnionFS 实现)。它把多个目录“联合”挂载成一个统一的文件系统。

# 查看当前容器的挂载信息(简化示例)
mount | grep overlay
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/.../diff,upperdir=...,workdir=...)

这段命令你可能会在排查问题时用到。我曾经帮同事定位过一次诡异问题:容器里明明改了配置文件,重启后又变回原样。后来发现他忘了那个可写层是临时的,一重启就丢了。嗯,这里要注意,持久化数据得用 volume。

⚠️ 重要提醒
overlay 的 upperdir(可写层)在容器删除后会一起被清理。不要把重要数据直接写在容器的可写层里,除非你用 volume 或 bind mount 挂载出来。

3. Dockerfile 指令与层的对应关系

每条 RUN、COPY、ADD 指令都会产生一个新层。而 LABEL、ENV、EXPOSE 等元数据指令不会产生新层,它们只影响镜像的配置。

指令是否产生新层说明
FROM否(引用已有层)指定基础镜像,用其所有层
RUN是在 shell 中执行命令,结果保存为新层
COPY / ADD是把文件从上下文复制到镜像中
CMD / ENTRYPOINT否仅设置默认启动命令,不产生文件变更
ENV / LABEL / EXPOSE否元数据,写入镜像配置

我习惯把多个 RUN 合并成一个,比如这样:

RUN apt-get update && \
    apt-get install -y python3 nodejs && \
    rm -rf /var/lib/apt/lists/*

为什么要合并?因为每个层都会占用空间。如果你写三条 RUN,镜像就多出三层,每层都包含中间缓存文件。合并后只有一层,而且删掉了 apt 缓存,镜像体积能小不少。

4. 构建缓存机制

Docker 构建时,会检查每一条指令的缓存。如果缓存命中,就复用之前的层,跳过执行。怎么判断命中?看指令字符串和上下文文件内容是否一致。

举个例子:

# 第一次构建
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nginx
COPY ./app /app

# 第二次构建,修改了 ./app 里的文件
# Docker 会发现 COPY 指令的上下文变了,后面的所有层都会失效并重新构建
💡 小技巧
把经常变化的 COPY 放在 Dockerfile 靠后的位置,不常用的依赖安装放在前面,可以最大化利用缓存。我通常这样组织:
  1. 先安装系统包(apt、yum)
  2. 再安装语言级依赖(pip、npm)
  3. 最后复制项目代码
这样你改代码时,前两层都能命中缓存,构建飞快。

5. 分层校验的原理

一致性校验的关键,就是确保每一层的内容没有被篡改过。每个层都有一个唯一的哈希值(通常是 SHA256)。Docker 在 pull 和 push 时都会校验这些哈希。

你看过 Docker 的 pull 日志吗?类似这样:

myimage:latest: digest: sha256:abcdef... size: 12345
Status: Downloaded newer image for myimage:latest

digest 就是整个镜像清单的哈希。而每一层也有自己的 digest。如果有人在传输过程中篡改了某个层的数据,哈希对不上,Docker 就会拒绝加载。

我在生产环境遇到过一回:某个私有的 registry 用了老旧的存储后端,同步磁盘时发生了 bit rot,导致一个层的哈希校验失败。镜像拉不下来,服务部署卡住了。最后我们重建了那个镜像,问题才解决。自那以后,我坚持给 registry 做定期一致性检查。

6. 本章核心知识图

下面这张图把镜像分层、UnionFS、缓存和校验串起来了,方便你整体把握。

Docker镜像分层与校验核心流程 镜像分层(OverlayFS) 顶层(可写层) 中间层(RUN apt-get) 基础层(Ubuntu 22.04) 构建缓存与指令 COPY ./app /app RUN apt-get install FROM ubuntu:22.04 校验层 SHA256 哈希校验 Manifest digest 联合挂载点:/ (只读层 + 可写层 overlay 挂载) 构建时每层缓存键 = 指令内容 + 上下文文件哈希 Docker Daemon Registry 容器运行时 pull push run
📌 核心要点回忆
  • Docker 镜像是只读层的堆叠,每层对应 Dockerfile 的一条操作指令。
  • UnionFS(OverlayFS)把多个层“联合”成一个统一的视图。
  • 构建缓存靠指令字符串和输入文件哈希判断,顺序很重要。
  • 一致性校验依赖 SHA256,每个层和整个镜像都有 digest。