容器技术的早期阶段,Docker 几乎就是容器的代名词。但随着生态走向标准化,Docker 已经不再是一个封闭的引擎,而是围绕开放容器计划 OCI 构建的模块化工具。要理解这一点,需要回到 Docker 的架构演进,以及 OCI 两个核心规范:runtime-spec 和 image-spec。本文将先梳理二者关系,再深入规范细节,最后给出验证兼容性的具体方法。

一、从 Docker 引擎到 OCI 标准:一次关键的架构拆分
早期 Docker 把镜像构建、容器运行、镜像分发都塞在 docker daemon 中。这种单体架构虽然上手快,但一旦需要调度大规模容器,就会暴露出与 Kubernetes、Mesos 等编排系统的耦合问题。2015 年,Docker 联合 CoreOS、Google、Red Hat 等公司发起开放容器计划 Open Container Initiative,简称 OCI,目标是以厂商中立的方式定义容器运行时和镜像格式。Docker 把底层运行时 libcontainer 捐给 OCI,成为 runc 的前身,同时把镜像格式草案作为 OCI image-spec 的基础。这个拆分让 Docker 本身可以继续专注用户体验,而更底层的容器创建、启动、停止则由独立组件完成。
这一变化并不是简单的代码搬家。它意味着容器的接口被标准化了,任何实现 OCI runtime-spec 的运行时都可以接管同一个容器 bundle,任何实现 OCI image-spec 的镜像都可以在不同工具之间流转。对 Docker 来说,它从事实标准转变为遵循标准的角色,反而推动了整个云原生生态的健康发展。现在我们在服务器上看到的 Docker,实际上由 Docker CLI、dockerd、containerd、runc 多层组成,runc 是真正读写 cgroups 和 namespace 的 OCI 运行时。
二、OCI 两大规范拆解:runtime-spec 与 image-spec
OCI 规范由两个独立文件组成。runtime-spec 定义容器的配置文件格式、运行生命周期、状态机,以及 hooks 的调用时机。一个符合规范的容器 bundle 必须包含 config.json 和 rootfs 目录。config.json 中声明了进程入口、环境变量、挂载点、Linux 资源限制、namespaces 设置等。runc 就是根据这份 config.json 来创建进程,并在结束后返回状态。可以手工编辑 config.json 再交给 runc 执行,这就解释了为什么不同厂商的容器运行时能够互相替换。
image-spec 则负责镜像的存储和分发。一个 OCI 镜像在磁盘上通常以目录形式存在,包含 index.json、oci-layout 文件和 blobs 目录。blobs 中保存了 manifest、config 和 layer 的压缩包,全部通过 sha256 摘要寻址。这种内容寻址设计让镜像层可以被安全地复用。Docker 自 1.10 起开始采用内容寻址的镜像 ID,正是向 OCI 规范靠拢。用户用 docker save 导出的 tar 包,在较新版本中已经与 OCI 镜像布局兼容。第三方工具如 skopeo、crane 可以跨 registry 复制镜像,其底层原理就是操作这些 OCI 结构。
经常有人把 OCI 与 CRI 混淆。CRI 是 Kubernetes 定义的容器运行时接口,面向的是高层运行时与 kubelet 之间的 gRPC 协议;而 OCI 是更底层的标准,关注单个容器的进程和文件系统。containerd 同时实现了 CRI 和 OCI 调用,这也是它能替代 Docker 直接服务于 Kubernetes 的原因。
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:abc...",
"size": 1234
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:def...",
"size": 5678
}
]
}
三、runC 与 containerd:Docker 背后的 OCI 运行时链
runC 最初叫 libcontainer,被捐赠给 OCI 后成为官方参考实现。它体积小、职责单一,只做一件事情:加载 bundle,解析 config.json,创建 namespace、cgroups、挂载 rootfs,启动进程。runC 不处理镜像拉取、网络设置、日志这些上层能力。你可以直接在命令行中安装 runC,创建一个 rootfs,然后用 runc spec 生成模板配置,再执行 runc run 启动一个最小容器。这种直接操作的方式非常有利于理解容器底层原理。
containerd 则是一个常驻守护进程,管理容器的完整生命周期。Docker 通过 gRPC 调用 containerd,containerd 再调用 runC 或其它 OCI 运行时。在 Kubernetes 节点上,kubelet 可以直接通过 CRI 插件与 containerd 通信,不再需要 dockerd。由此可见,Docker 中的容器创建能力其实与 Kubernetes 使用的 containerd 是同一条链路。用户切换运行时后,原有的 OCI 镜像不需要任何改动,因为镜像和运行时规范已经解耦。
验证这条链路最直接的方法是查看 docker info 中的 runtime 信息。你会看到默认 runtime 是 runc,并且注册了 io.containerd.runc.v2。还可以用 docker buildx 构建一个 OCI 格式的镜像,再用 nerdctl 或 ctr 导入运行。如果这些操作都能成功,就说明镜像和运行时都严格遵循了 OCI 规范。
# 查看 Docker 使用的默认 OCI 运行时 docker info | grep -i runtime # 直接用 runc 启动一个最小容器 mkdir -p /tmp/mycontainer/rootfs docker export $(docker create busybox) | tar -C /tmp/mycontainer/rootfs -xvf - cd /tmp/mycontainer runc spec runc run mycontainer
四、OCI 兼容性带来的实际问题与验证
当团队决定从 Docker 迁移到 containerd 或 Podman 时,最关心的是现有镜像能否继续使用。答案通常是可以,因为只要镜像满足 OCI image-spec,支持 OCI 的工具就能拉取并运行。例如 Podman 默认以 OCI 格式存储镜像,Docker pull 下来的镜像在 quay 等 registry 上同样可以被 Podman 使用。部分老旧的 Docker 镜像可能因为使用不受支持的 schema 或非法配置而失败,但这种情况已经很少见。遇到兼容性问题时,可以用 skopeo inspect 查看 manifest 的 mediaType,判断是否包含 vnd.oci 或 vnd.docker 前缀。Docker 和 OCI 的 manifest 在 schemaVersion 2 下高度相似,OCI 规范兼容 Docker 格式,反之则不一定所有 OCI 扩展都能被旧版 Docker 识别。
另一个常见误区是认为 OCI 定义了网络和存储标准。实际上 OCI 不包含 CNI 或 CSI 这类接口。容器网络由 CNI 插件负责,存储由 CSI 驱动负责。OCI 只解决两个问题:一个容器进程怎么启动,以及一个镜像怎么打包和分发。所以即使两个工具都宣称 OCI 兼容,它们的网络实现可能完全不同。用户在选择时需要区分 oci runtime 兼容与 k8s 运行时类别的差别。
验证一个运行时是否符合 OCI 规范,可以使用 OCI 官方提供的 runtime-tools 工具。它包含 runtime validation 测试,通过执行一系列命令检查 runtime 是否正确实现了规范要求。也可以自己制作一个简单的 rootfs,用 runc spec 生成配置后,尝试用目标运行时启动。例如把 config.json 和 rootfs 复制到另一个目录,再调用 crun 或 youki 运行。只要能正常启动并看到相同输出,就说明这些运行时在 OCI 层面是可互换的。