镜像到底是什么文件?这篇不看文档的描述,直接把一个镜像 docker save 成 tar,打开 index.json、manifest、config 和每一层,逐个重新计算 sha256,看 OCI 镜像规范里的各个字段在真实镜像里长什么样。
顶层条目:['blobs', 'index.json', 'manifest.json', 'oci-layout']
blobs/sha256/ 下共 5 个文件,文件名就是内容的 sha256
oci-layout 的内容:{'imageLayoutVersion': '1.0.0'}
逐个重新计算 sha256 与文件名对不上的 blob 数:0用 docker save 导出的是 OCI 镜像布局:blobs/sha256/ 放所有内容,index.json 是入口,oci-layout 标版本,另外还有一个 Docker 自己加的 manifest.json。5 个 blob 的文件名都等于自己内容的 sha256(我逐个重算过)——这就是“内容寻址”。
index.json:schemaVersion=2,1 个条目;第 1 个 mediaType=application/vnd.oci.image.manifest.v1+json size=857 manifest 的键:['schemaVersion', 'mediaType', 'config', 'layers'];mediaType=application/vnd.oci.image.manifest.v1+json config:mediaType=application/vnd.oci.image.config.v1+json size=1864 digest=84689109decd… layers[0]:mediaType=application/vnd.oci.image.layer.v1.tar+gzip size=4092319 digest=3f26bc2dec0b… layers[1]:mediaType=application/vnd.oci.image.layer.v1.tar+gzip size=108 digest=891034b893e2… layers[2]:mediaType=application/vnd.oci.image.layer.v1.tar+gzip size=112 digest=b1de4487bdf8…
从 index.json 指向的 manifest(OCI image manifest,857 字节)里,只有 config 和 layers 两样:config 是一个 JSON,layers 是 3 个 tar+gzip,每一项都带 mediaType、size、digest。这个镜像是用 --provenance=false --sbom=false 构建的,所以没有附加证明(见 c11、c13)。
config 的顶层键:['architecture', 'config', 'created', 'history', 'os', 'rootfs']
config.Env=['PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'APP_MODE=demo']
config.Entrypoint=['/bin/sh', '-c'] Cmd=['cat /hello.txt'] ExposedPorts={'8080/tcp': {}} Labels={'bwlab': '1', 'purpose': 'c14'}
architecture=arm64 os=linux created=2026-10-10T08:25:59
rootfs.type=layers;diff_ids 共 3 个config 记录运行时的信息:环境变量、Entrypoint、Cmd、暴露的端口、标签,以及 architecture / os 和 rootfs 的 diff_ids。docker run 的默认行为就来自这里。
history 共 9 条,其中 empty_layer=true 的有 6 条;layers 有 3 层
(有层) ADD alpine-minirootfs-3.20.10-aarch64.tar.gz / # buildkit
(无层) CMD ["/bin/sh"]
(无层) ENV APP_MODE=demo
(无层) LABEL purpose=c14
(有层) RUN /bin/sh -c echo hello > /hello.txt # buildkit
(有层) COPY data.txt /data.txt # buildkit
(无层) EXPOSE [8080/tcp]
(无层) ENTRYPOINT ["/bin/sh" "-c"]
(无层) CMD ["cat /hello.txt"]9 条 history 里只有 3 条产生了层(ADD rootfs、RUN、COPY),其余 6 条 empty_layer 只改 config。基础镜像自带的 CMD ["/bin/sh"] 也在其中。想估算镜像大小,看的是层,不是指令条数。
layers[0]:压缩后 4092319 字节,解压后 9107968 字节;压缩数据的 sha256 与 manifest 里的 digest 一致:True;解压数据的 sha256 与 diff_ids[0] 一致:True;digest 和 diff_id 相同吗?False layers[1]:压缩后 108 字节,解压后 2048 字节;压缩数据的 sha256 与 manifest 里的 digest 一致:True;解压数据的 sha256 与 diff_ids[1] 一致:True;digest 和 diff_id 相同吗?False layers[2]:压缩后 112 字节,解压后 2560 字节;压缩数据的 sha256 与 manifest 里的 digest 一致:True;解压数据的 sha256 与 diff_ids[2] 一致:True;digest 和 diff_id 相同吗?False
manifest 里的 layer digest 是压缩后数据的 sha256,config 里的 diff_id 是解压后 tar 的 sha256。三层都验证了两者各自成立、彼此不同。我的理解是前者用来校验传输/存储,后者用来描述文件系统内容,与压缩方式无关;这是从规范的字段含义和上面的验证推断的,没有做换压缩算法的实验。
最后一层(COPY data.txt)的文件:data.txt(1001 字节) 倒数第二层(RUN echo hello)的文件:hello.txt(6 字节)
最后一层只含 data.txt,倒数第二层只含 hello.txt:每层只带这一步新增或改动的文件(删除用 whiteout 标记,见 c3)。