Docker 镜像构建原理:Manifest 与 SHA256 计算详解
· 1670 字 · 约 8 分钟
很多人使用 Docker 很久,却不太清楚镜像到底是怎么构建出来的。manifest、config、layer 这些概念听起来很抽象,SHA256 计算更是像黑盒。
本文通过一个实际的 Docker 镜像构建案例,带你一步步拆解镜像内部结构,验证 SHA256 的计算方式,让你彻底理解 Docker 镜像的构建原理。
准备一个演示镜像
先创建一个包含多个指令的 Dockerfile,这样能看到多个 layer 的生成过程:
|
|
构建镜像:
|
|
构建过程输出:
|
|
注意到每个 step 都生成了一个镜像 ID(如 de2b9975f8fd、6888b70c8d5f 等),这些就是每一层的结果。
查看镜像构建历史
使用 docker history 查看每一层的详细信息:
|
|
输出:
|
|
关键信息:
- 每一列的 IMAGE 列就是该层的 SHA256(完整版本)
- SIZE 显示这一层的大小(0B 表示只修改元数据,不产生文件层)
#(nop)表示这是一个元数据操作,不执行 shell 命令empty_layer: true的层(如 ENV、CMD)不会产生新的文件层,只会更新 config
导出镜像并查看内部结构
Docker 镜像本质上是一组文件,可以用 docker save 导出:
|
|
解压查看:
|
|
输出:
|
|
镜像的结构:
|
|
理解 Manifest
Manifest 是镜像的"目录",记录了镜像包含哪些层和配置文件。
Docker 格式的 manifest.json
|
|
输出:
|
|
关键字段:
Config: 指向配置文件的路径RepoTags: 镜像的标签Layers: 所有文件层的路径(按顺序从底到顶)
OCI 格式的 index.json
|
|
输出:
|
|
OCI 格式使用 index.json 指向实际的 manifest 文件(fe19fc8e...)。
OCI Manifest 文件
|
|
输出:
|
|
Manifest 的核心作用:
- 引用 config 文件(通过 digest)
- 引用所有 layer(通过 digest)
- 记录每个文件的大小和媒体类型
理解 Config
Config 文件记录了镜像的所有配置信息和构建历史。
|
|
输出(关键部分):
|
|
Config 的关键内容:
- config 字段:运行时配置(Env, Cmd, WorkingDir 等)
- history 字段:构建历史,记录每一步的命令和时间
empty_layer: true表示该步骤不产生文件层(如 ENV、CMD)
- rootfs.diff_ids:所有文件层的未压缩 SHA256
- 注意:这是未压缩的 tar 文件的 SHA256
- 与 manifest 中的 digest(压缩后的 SHA256)不同
理解 Layer
Layer 是实际的文件系统层,每个 layer 是一个 tar 包,包含文件系统的变更。
查看 Layer 的类型和大小
|
|
输出:
|
|
查看 Layer 的文件类型
|
|
输出:
|
|
可以看到 layer 是未压缩的 POSIX tar 包。
注意:这是 docker save 导出的本地镜像格式。实际推送到 Registry(如 Docker Hub)时,layer 会被压缩成 gzip 格式。这是理解 diff_ids 和 digest 关系的关键。
查看 Layer 的内容
|
|
输出:
|
|
这个 layer 包含:
app/start.sh- 我们的启动脚本etc/ssl/certs/orbstack-root.crt- OrbStack 添加的根证书
提取 Layer 中的文件
可以直接从 tar 包中提取单个文件:
|
|
输出:
|
|
这就是我们在 Dockerfile 中创建的启动脚本。
SHA256 计算验证
现在来验证 SHA256 是如何计算的。关键原则:文件名就是文件内容的 SHA256。
验证 Config 文件的 SHA256
|
|
输出:
|
|
确实,文件名就是内容的 SHA256。
验证 OCI Manifest 的 SHA256
|
|
输出:
|
|
同样正确。
验证 Layer 的 SHA256
|
|
输出:
|
|
完美匹配!
diff_ids vs digest
这是容易混淆的概念:
- diff_ids:在 Config 文件中,是未压缩的 tar 文件的 SHA256
- digest:在 Manifest 中,通常是压缩后的 tar.gz 文件的 SHA256
但在我们的案例中,所有 layer 都是未压缩的 tar 包,所以 diff_ids 和 digest 完全相同:
Config 中的 diff_ids:
|
|
Manifest 中的 digest:
|
|
如果 layer 被压缩(gzip),那么:
- digest = SHA256(layer.tar.gz)
- diff_ids = SHA256(layer.tar)
- digest ≠ diff_ids
本地存储 vs Registry 存储
理解了 diff_ids 和 digest 的区别后,一个关键问题是:什么时候会压缩?
本地存储(Docker daemon)
本地 Docker 存储中,layer 通常不压缩:
|
|
原因:
- 直接挂载使用,启动容器快
- OverlayFS 支持直接挂载目录
这就是为什么我的演示中看到的是未压缩的 tar 包,且 diff_ids 和 digest 相同。
Registry 存储(Docker Hub 等)
推送到 Registry 时,layer 会压缩成 gzip:
|
|
此时 Manifest 中的 digest 会变化:
|
|
而 Config 中的 diff_ids 保持不变(因为它是未压缩 tar 的 SHA256):
|
|
实际验证方法
可以通过 docker manifest inspect 查看远程镜像的压缩情况:
|
|
注意 mediaType 是 tar.gzip,说明 Registry 存储时是压缩的。
为什么要这样设计?
-
双重校验:
- Registry 用 digest 验证压缩文件的完整性
- Docker 用 diff_ids 验证解压后文件的完整性
-
性能优化:
- 本地存储不压缩 → 快速挂载启动
- Registry 存储压缩 → 节省存储和网络带宽
-
去重能力:
- 相同内容的 tar 包 → 相同的 diff_ids
- 即使压缩方式不同,也能识别内容相同的层
完整的 SHA256 计算流程
总结 Docker 镜像 SHA256 的计算流程:
|
|
快速查看镜像信息的命令
日常工作中,可以使用这些命令快速查看镜像信息:
|
|
总结
通过这个实际案例,我们验证了:
- 文件名即 SHA256:每个文件(config、manifest、layer)的文件名都是其内容的 SHA256
- Layer 是 tar 包:每个 layer 是一个 POSIX tar archive,包含文件系统的变更
- Manifest 是目录:Manifest 记录了镜像包含的所有文件及其 digest
- Config 是配置中心:Config 包含运行时配置、构建历史和 diff_ids
- diff_ids vs digest:未压缩时两者相同,压缩时 digest 是压缩后的 SHA256
理解这些原理后,Docker 镜像不再是黑盒,你可以:
- 手动验证镜像的完整性
- 排查镜像构建问题
- 优化镜像大小(减少层数、合并层)
- 理解镜像分发和缓存机制
下次看到镜像的 SHA256 时,你就知道它是怎么计算出来的了。
参考资料
- OCI Image Spec: https://github.com/opencontainers/image-spec
- Docker Image Spec: https://github.com/moby/docker-image-spec
- Docker Documentation: https://docs.docker.com/build/building/understand-images/