在容器技术发展的早期,Docker 镜像是事实上的标准。随着容器生态的爆发,为了确保不同容器运行时(如 Docker, containerd, CRI-O, Podman 等)和构建工具之间的互操作性,Open Container Initiative (OCI) 成立并推出了 OCI Image Specification。
本文将基于最新的 OCI Image Specification 详细解读 OCI 镜像的构成及其背后的技术细节。
OCI 镜像规范定义了 OCI 镜像的结构。简单来说,一个 OCI 镜像包含以下几个核心部分:
amd64, arm64)。这些组件共同工作,使得镜像可以被构建、传输、校验并最终运行。
在深入各个组件之前,必须先理解 Content Descriptor。它是 OCI 规范中用来引用内容的通用方式。
一个描述符(Descriptor)就是一个 JSON 对象,它告诉我们“去哪里找内容”、“内容长什么样”以及“内容的指纹是什么”。核心字段包括:
mediaType: 内容的类型(例如:application/vnd.oci.image.manifest.v1+json)。digest: 内容的哈希摘要(通常是 sha256),用于唯一标识和校验内容。size: 内容的大小(字节)。{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 7682,
"digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270"
}OCI 广泛使用了 Merkle Directed Acyclic Graph (DAG) 结构,描述符就是图中的边,连接了不同的组件。
OCI 镜像的结构关系如下图所示:
Manifest 是镜像的“物料清单”。对于一个特定的架构和操作系统,Manifest 定义了镜像由哪些层组成,以及配置文件的位置。
一个典型的 Manifest JSON 结构如下:
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:b5b2b2...",
"size": 7023
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:983487...",
"size": 32654
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:3c3a46...",
"size": 16724
}
],
"annotations": {
"com.example.key1": "value1"
}
}config: 引用镜像配置 Blob。layers: 一个有序数组,引用了组成文件系统的各个层 Blob。数组索引 0 是底层 (Base Layer),后续层依次叠加。Configuration 是一个 JSON 文档,包含了将镜像转换为运行时 Bundle 所需的信息。它不包含文件系统的实际内容,而是包含元数据。
主要包含:
architecture / os: 适用的 CPU 架构和操作系统。config: 运行时配置,如 Env (环境变量), Cmd (默认命令), Entrypoint, User, WorkingDir 等。rootfs: 引用了由于层叠加而产生的 Diff IDs,用于校验文件系统完整性。history: 描述了每一层是如何构建出来的(例如 Dockerfile 中的指令)。Layers 包含了文件系统的实际变化。每一个层通常是一个 .tar 或 .tar.gz 归档文件。
OCI 镜像采用分层存储和写时复制 (CoW) 策略。每一层只记录相对于上一层的 变化 (Changeset) 。变化主要有三种类型:
如果我们在上层删除了下层的一个文件 /etc/my-app-config,我们不能直接从 Tar 包里把这个文件拿掉(因为下层是只读的)。相反,我们在新层中创建一个特殊的文件,名字叫 .wh.my-app-config。
运行时在联合挂载(Union Mount)时,看到 .wh. 前缀的文件,就会知道要“遮挡”住下层的同名文件,从而实现删除效果。
还有一个特殊的 Opaque Whiteout (.wh..wh..opq),用于隐藏父目录下的所有子项。
# 示例:层内容
/etc/my-app.d/
/etc/my-app.d/default.cfg
/bin/my-app-tools
/etc/.wh.my-app-config <-- 表示删除了 /etc/my-app-configImage Index 是一个更高层级的 Manifest。它本身不包含层或配置,而是包含了一个指向其他 Manifest 的列表。
这主要用于多平台支持。当你 docker pull my-image:latest 时,客户端会先获取 Index,然后根据当前的机器架构(如 linux/amd64),找到列表中对应的 Manifest 并下载。
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:e69241...",
"size": 7143,
"platform": {
"architecture": "ppc64le",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:5b0bca...",
"size": 7682,
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
]
}了解 OCI 规范后,值得深入了解两个在云原生生态中被广泛使用的工具和库,它们完美体现了 OCI 标准带来的互操作性。
Skopeo 是一个功能强大的命令行工具,用于对容器镜像和镜像库执行各种操作。与其他工具不同,Skopeo 的最大优势在于它无需运行容器守护进程(如 Docker Daemon) 即可工作。
常用场景示例:
远程检视 (skopeo inspect)
在不下载(Pull)镜像层的情况下,直接读取远程仓库中镜像的 Manifest 和配置信息。这在 CI/CD 流水线中非常实用,可以快速检查镜像标签、架构或创建时间。
命令:
skopeo inspect docker://hub.example.io/library/alpine:latest输出示例 (JSON):
skopeo inspect docker://hub.example.io/library/alpine:latest --tls-verify=false
{
"Name": "hub.example.io/library/alpine",
"Digest": "sha256:9d04ae17046f42ec0cd37d0429fff0edd799d7159242938cc5a964dcd38c1b64",
"RepoTags": [
"3.10",
"3.22",
"latest"
],
"Created": "2025-10-08T11:04:56Z",
"DockerVersion": "",
"Labels": null,
"Architecture": "amd64",
"Os": "linux",
"Layers": [
"sha256:2d35ebdb57d9971fea0cac1582aa78935adf8058b2cc32db163c98822e5dfa1b"
],
"LayersData": [
{
"MIMEType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"Digest": "sha256:2d35ebdb57d9971fea0cac1582aa78935adf8058b2cc32db163c98822e5dfa1b",
"Size": 3802452,
"Annotations": null
}
],
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
]
}镜像复制与格式转换 (skopeo copy)
支持在不同的存储机制之间直接复制镜像。例如,将 Docker Hub 上的镜像直接下载并转换为符合 OCI Layout 标准的本地文件目录,而无需中间环节。
命令:
# 将 Docker 在线镜像复制为本地 OCI 目录结构
skopeo copy docker://docker.io/library/alpine:latest oci:/tmp/alpine-layout:latest输出:
Getting image source signatures
Copying blob 05455a5d5d3e done
Copying config 124c7d2707 done
Writing manifest to image destination
Storing signaturescontainers/image(也被称为 image library)是上述 Skopeo 以及 Podman、Buildah 等下一代容器工具背后的核心库。
这是一个 Go 语言库,提供了一套通用的 API 来处理容器镜像的传输、签名、及其元数据管理。它抽象了底层的存储细节,支持多种 传输协议 (Transports) :
docker://:访问标准的远程 Docker/OCI 注册表。docker-daemon://:直接操作 Docker 守护进程的本地存储。oci://:操作符合 OCI 目录布局(OCI Layout)的本地镜像。dir://、tarball:// 等:支持本地目录和归档文件。正是因为有了 containers/image 库的底层支持,OCI 生态中的工具才能实现跨平台、跨存储后端的无缝互操作,真正践行了 OCI “一次构建,到处运行” 的理念。
代码示例 (Go) : 以下代码展示了如何使用该库编程将一个远程 Docker 镜像复制到本地 OCI 目录中。
package main
import (
"context"
"os"
"github.com/containers/image/v5/copy"
"github.com/containers/image/v5/signature"
"github.com/containers/image/v5/transports/alltransports"
)
func main() {
src := "docker-daemon://alpine:latest"
dest := "oci:/tmp/my-alpine-layout:latest"
// 1. 解析源和目标引用
srcRef, err := alltransports.ParseImageName(src)
if err != nil {
panic(err)
}
destRef, err := alltransports.ParseImageName(dest)
if err != nil {
panic(err)
}
// 2. 配置策略上下文 (示例中使用宽松策略)
policy := &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}
policyContext, _ := signature.NewPolicyContext(policy)
// 3. 执行 Image Copy 操作
// 这相当于运行 `skopeo copy`
_, err = copy.Image(context.Background(), policyContext, destRef, srcRef, ©.Options{
ReportWriter: os.Stdout, // 将进度输出到控制台
})
if err != nil {
panic(err)
}
}执行结果:
Getting image source signatures
Copying blob 989e799e6349 done |
Copying config b956011c2e done |
Writing manifest to image destinationOCI 镜像规范不仅仅是一个文件格式标准,它构成了现代云原生基础设施的基石。通过解耦 Manifest (清单)、Config (配置) 和 Layers (数据) ,它实现了高度的灵活性和互操作性。
本文的核心要点回顾:
containers/image 等工具,我们可以轻松在不同环境(Docker, OCI Layout, Remote Registry)之间迁移和操作镜像。理解这些底层细节,有助于我们更好地进行容器镜像的构建优化、安全扫描以及故障排查,从而构建更高效、更安全的云原生应用。