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 靠后的位置,不常用的依赖安装放在前面,可以最大化利用缓存。我通常这样组织:
- 先安装系统包(apt、yum)
- 再安装语言级依赖(pip、npm)
- 最后复制项目代码
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 镜像是只读层的堆叠,每层对应 Dockerfile 的一条操作指令。
- UnionFS(OverlayFS)把多个层“联合”成一个统一的视图。
- 构建缓存靠指令字符串和输入文件哈希判断,顺序很重要。
- 一致性校验依赖 SHA256,每个层和整个镜像都有 digest。