容器技术发展至今,镜像格式已经从 Docker 一家独大演变为 OCI 开放标准。很多初学者在使用 podman、buildah 或 containerd 时,会发现这些工具构建出的镜像与 Docker 构建的镜像在结构上略有不同,甚至 registry 中的 manifest 类型也分为 Docker V2 和 OCI 两种。要理解这些差异,需要回到镜像格式的历史演进过程中去看,才能明白 OCI 规范解决了什么问题,以及两者在文件结构、媒体类型、分发协议上的具体区别。

一、Docker 镜像格式的演进历史
Docker 早期的镜像格式称为 Schema 1,这是 2013 年 Docker 刚问世时使用的格式。Schema 1 存在很多设计缺陷,比如没有内容寻址机制、镜像签名逻辑混乱、层与层之间的父子关系依赖 manifest 中的引用链来维护。这些缺陷导致镜像在被篡改或损坏时难以校验,也使得多架构镜像几乎无法实现。
2016 年 Docker 推出了 Schema 2,也就是现在常说的 Docker V2 格式。它引入了内容寻址(Content Addressable)机制:镜像的每一层都通过其内容的 SHA256 摘要来标识,manifest 本身也有 digest。这种设计让镜像校验变得简单可靠,同时引入了 manifest list 的概念,为多架构镜像铺平了道路。一个典型的 Docker V2 镜像由三部分组成:manifest 清单文件、config 配置文件和若干 layer 层文件。
Docker V2 的 manifest 使用 application/vnd.docker.distribution.manifest.v2+json 这样的 media type 标识,config 使用 application/vnd.docker.container.image.v1+json,每一层则使用 application/vnd.docker.image.rootfs.diff.tar.gzip。这一命名体系完全是 Docker 私有的,也就埋下了后来标准化的伏笔——如果整个容器生态都依赖 Docker 的私有格式,其他运行时就很难摆脱对 Docker 的依赖。
二、OCI 规范的诞生与核心结构
2015 年,Docker 联合 CoreOS、Google、Red Hat 等公司成立了 OCI(Open Container Initiative)组织,目标是制定开放的容器标准。最初 OCI 只定义了运行时规范 runtime-spec,镜像格式规范 image-spec 直到 2017 年才发布 1.0 版本。OCI 镜像规范在很大程度上借鉴了 Docker V2 的设计,可以说是在其基础上做了一次“去 Docker 化”的标准化整理。
OCI 镜像同样由 manifest、config 和 layers 组成,文件结构几乎一致,最大的区别在于 media type 的命名。OCI 规范将所有媒体类型统一收纳到 application/vnd.oci 命名空间下,例如 manifest 使用 application/vnd.oci.image.manifest.v1+json,config 使用 application/vnd.oci.image.config.v1+json,层文件使用 application/vnd.oci.image.layer.v1.tar+gzip。
一个 OCI manifest 的典型结构如下,可以看到与 Docker V2 的差异主要在 media type 字段上:
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:8f1b3e2c...",
"size": 1234
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:a1c4e5d6...",
"size": 34567890
}
]
}
此外,OCI 规范引入了 artifact 类型支持,允许在 manifest 中通过 artifactType 字段声明镜像承载的是任意制品而不仅是容器镜像,比如 Helm Chart、WASM 模块、签名数据等。这是 OCI 1.1 版本的重要扩展,也是如今很多项目把 OCI Registry 当作通用制品仓库的技术基础。
三、两种格式的实际差异与兼容性处理
从文件层面看,Docker V2 与 OCI 镜像的层内容是完全兼容的,都是压缩的 tar 归档,digest 计算方式也一致。也就是说同一个 layer 文件在两种格式下可以复用,区别只在于外层的 manifest 和 config 描述。真正影响使用的是分发环节:当容器引擎向 Registry 拉取镜像时,会根据 Accept 头声明自己支持的 media type,Registry 则返回对应格式的 manifest。
Docker 从 23.0 版本开始默认推送 OCI 格式的镜像,而 containerd、podman 等工具更早就以 OCI 为主要格式。这就带来一个实际问题:如果一个老旧的私有 Registry 只支持 Docker V2,接收 OCI 镜像时可能直接报错,或者把 OCI manifest 当作未知类型处理。反过来,主流公共仓库如 Docker Hub、ghcr.io、quay.io 都已同时支持两种格式,日常使用感知不到差别。
在实际运维中,可以使用 skopeo 或 crane 检查和转换镜像格式。skopeo 的 inspect 命令能够查看远端镜像的真实 media type:
# 查看远端镜像的 manifest 类型 skopeo inspect --raw docker://docker.io/library/nginx:latest | head -5 # 将 Docker V2 镜像转换为 OCI 格式存储到本地目录 skopeo copy docker://docker.io/library/nginx:latest oci:/tmp/nginx-oci:latest # 将 OCI 布局目录推送到其他 registry 并保持格式 skopeo copy oci:/tmp/nginx-oci:latest docker://registry.ipipp.com/ops/nginx:latest
对于构建工具,BuildKit 和 buildah 都提供了格式控制参数。例如 buildah 可以在构建时明确指定 manifest 类型,避免目标仓库不兼容的尴尬。而 Docker 的 daemon 配置中也可以通过 "features": {"containerd-snapshotter": true} 等方式调整存储行为。
四、如何选择:面向未来还是兼容存量
如果是新搭建的基础设施,推荐全面采用 OCI 格式。OCI 是社区共同维护的开放标准,不存在厂商锁定问题,artifact 扩展也让同一套 Registry 能承载更多类型的制品。Kubernetes 生态中的镜像安全扫描、签名验证工具(如 cosign、trivy)也都以 OCI 规范为优先支持对象。
如果环境中存在大量存量系统,比如旧版本的 Harbor 或自研 Registry 代理,就需要先确认它们对 OCI manifest 的支持情况,必要时用 skopeo 做格式归一化处理。一个常见的实践是在 CI 流水线中统一产出 OCI 格式镜像,并在推送前校验目标 Registry 的能力,出现不兼容时自动降级为 Docker V2 格式重新推送。
总的来说,Docker V2 与 OCI 镜像在核心数据结构上高度相似,层内容完全互通,差异集中在 media type 命名和制品扩展能力上。理解了这一点,遇到格式相关的报错时就能快速定位:多数问题都出在 Registry 对某种 manifest 类型不支持,而不是镜像内容本身损坏。掌握 skopeo、crane 这类工具的检查与转换方法,基本可以应对所有格式兼容场景。