2015 年 6 月,Docker 公司将 libcontainer(runc 的前身)捐赠给 Linux 基金会,成立了 OCI(Open Container Initiative)。这个决定改变了容器生态的走向——从此,容器镜像格式和运行时接口有了开放标准,任何人都可实现兼容的运行时,不再被 Docker 的私有格式锁定。
OCI 定义了三大规范:Image Spec(镜像长什么样)、Runtime Spec(怎么运行容器)、Distribution Spec(怎么分发镜像)。这三个规范串联起来,定义了从镜像构建到容器运行的完整链路。
前置知识
- Ch02 Linux Namespace 深入:OCI Runtime Spec 定义了如何配置 Namespace,理解 Namespace 是理解 Runtime Spec 的前提
- Ch03 Cgroup v2 深入:OCI Runtime Spec 定义了如何配置 Cgroup 资源限制
- Ch04 OverlayFS:容器文件系统:OCI Image Spec 定义了镜像层的格式,OverlayFS 是镜像层复用的底层机制
- OCI 规范是 Ch02-04 三大内核机制的”标准化包装”——理解了三大基石,再看 OCI 规范就会发现它只是把这些内核能力用 JSON 描述清楚
OCI 规范是容器生态的”宪法”——runc、containerd、CRI-O、Kubernetes 都遵循 OCI 规范。理解 OCI 规范后,你会发现所有容器运行时的行为都有据可查。
本章将深入 OCI 规范的细节,理解容器标准化的每一个设计决策。
一、OCI 组织与规范体系
1.1 OCI 的组织结构
1.2 三大规范的关系
| 规范 | 定义内容 | 输入 | 输出 |
|---|---|---|---|
| Image Spec | 镜像格式 | Dockerfile/BuildKit | OCI Image(manifest+config+layers) |
| Runtime Spec | 运行时接口 | OCI Bundle(rootfs+config.json) | 运行中的容器 |
| Distribution Spec | 分发协议 | OCI Image | Registry API(push/pull) |
二、OCI Image Spec
2.1 镜像的组成
OCI 镜像由以下部分组成:
| 组件 | 格式 | 说明 |
|---|---|---|
| Manifest | JSON | 镜像的入口,指向 config 和 layers |
| Config | JSON | 镜像的配置(环境变量、入口命令、层描述) |
| Layer | tar+gzip | 文件系统层(增量变更) |
| Index | JSON | 多架构镜像的索引(可选) |
{ "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "config": { "mediaType": "application/vnd.oci.image.config.v1+json", "size": 7023, "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" }, "layers": [ { "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "size": 32654, "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0" }, { "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "size": 16724, "digest": "sha256:3c3a4f68c8e1e3e2b5c26a0e7e1f6a8c9b2d1e4f5a6b7c8d9e0f1a2b3c4d5e6" } ], "annotations": { "org.opencontainers.image.source": "https://github.com/example/app" }}2.2 Image Config
Image Config 定义了镜像的运行时配置:
{ "architecture": "amd64", "os": "linux", "config": { "User": "nginx", "ExposedPorts": {"80/tcp": {}}, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "NGINX_VERSION=1.25.3" ], "Entrypoint": ["/docker-entrypoint.sh"], "Cmd": ["nginx", "-g", "daemon off;"], "Volumes": {"/var/cache/nginx": {}}, "WorkingDir": "/", "Labels": {"maintainer": "NGINX Docker Maintainers"} }, "rootfs": { "type": "layers", "diff_ids": [ "sha256:a1b2c3d4e5f6...", "sha256:b2c3d4e5f6a7..." ] }, "history": [ {"created": "2024-01-01T00:00:00Z", "comment": "ADD file:..."}, {"created": "2024-01-01T00:01:00Z", "comment": "CMD [\"/bin/sh\"]"} ]}2.3 镜像层(Layer)
每个 Layer 是一个 tar 归档文件,包含相对于上一层的文件系统变更:
- 新增文件:直接包含在 tar 中
- 删除文件:使用 whiteout 标记(
.wh.<filename>) - 删除目录:使用 opaque whiteout(
.wh..wh..opq)
# 查看镜像层的内容docker save nginx -o nginx.tartar -xf nginx.tar
# 查看某一层的文件列表layer_id=$(cat manifest.json | python3 -c "import json, sysm = json.load(sys.stdin)print(m[0]['Layers'][0])")tar -tzf "$layer_id" | head -20
# 查看 whiteout 文件tar -tzf "$layer_id" | grep ".wh."2.4 多架构镜像(Image Index)
OCI Image Index 支持多架构镜像——一个 manifest 列表指向不同平台的镜像:
{ "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests": [ { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:abc123...", "size": 7143, "platform": {"architecture": "amd64", "os": "linux"} }, { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:def456...", "size": 7143, "platform": {"architecture": "arm64", "os": "linux"} } ]}2.5 Image Spec 的媒体类型
| 媒体类型 | 用途 |
|---|---|
application/vnd.oci.image.manifest.v1+json | 镜像 Manifest |
application/vnd.oci.image.config.v1+json | 镜像 Config |
application/vnd.oci.image.layer.v1.tar+gzip | 压缩层 |
application/vnd.oci.image.layer.v1.tar | 未压缩层 |
application/vnd.oci.image.layer.nondistributable.v1.tar+gzip | 不可分发层 |
application/vnd.oci.image.index.v1+json | 多架构索引 |
三、OCI Runtime Spec
3.1 OCI Bundle
OCI Bundle 是 Runtime Spec 的核心概念——一个包含 rootfs 和 config.json 的目录:
my-bundle/├── config.json # 容器运行时配置└── rootfs/ # 容器的根文件系统 ├── bin/ ├── dev/ ├── etc/ ├── lib/ ├── proc/ ├── sys/ └── usr/# 创建 OCI Bundlemkdir -p /tmp/bundle/rootfscd /tmp/bundledocker export $(docker create busybox) | tar -C rootfs -xvf -runc spec # 生成默认 config.json3.2 config.json 详解
config.json 是 OCI Runtime Spec 的核心配置文件,定义了容器的所有运行时参数:
{ "ociVersion": "1.0.2", "process": { "terminal": true, "user": {"uid": 0, "gid": 0}, "args": ["sh"], "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"], "cwd": "/", "capabilities": { "bounding": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"], "effective": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"], "permitted": ["CAP_AUDIT_WRITE", "CAP_KILL", "CAP_NET_BIND_SERVICE"] }, "rlimits": [{"type": "RLIMIT_NOFILE", "hard": 1024, "soft": 1024}], "noNewPrivileges": true }, "root": {"path": "rootfs", "readonly": true}, "hostname": "mycontainer", "mounts": [ {"destination": "/proc", "type": "proc", "source": "proc"}, {"destination": "/dev", "type": "tmpfs", "source": "tmpfs", "options": ["nosuid","noexec","mode=755"]}, {"destination": "/sys", "type": "sysfs", "source": "sysfs", "options": ["nosuid","noexec","ro"]} ], "linux": { "namespaces": [ {"type": "pid"}, {"type": "mount"}, {"type": "ipc"}, {"type": "uts"}, {"type": "network"} ], "resources": { "memory": {"limit": 536870912}, "cpu": {"quota": 200000, "period": 100000} }, "seccomp": { "defaultAction": "SCMP_ACT_ERRNO", "architectures": ["SCMP_ARCH_X86_64"], "syscalls": [{"names": ["accept","access","arch_prctl"], "action": "SCMP_ACT_ALLOW"}] } }}3.3 config.json 的关键配置项
| 配置项 | 说明 | 对应 Docker 参数 |
|---|---|---|
| process.args | 容器启动命令 | CMD / ENTRYPOINT |
| process.env | 环境变量 | -e |
| process.user | 运行用户 | -u |
| process.capabilities | Linux Capabilities | —cap-add / —cap-drop |
| root.path | rootfs 路径 | — |
| root.readonly | 只读根文件系统 | —read-only |
| hostname | 主机名 | -h |
| mounts | 挂载点列表 | -v |
| linux.namespaces | Namespace 配置 | — |
| linux.resources | Cgroup 资源限制 | —memory, —cpus |
| linux.seccomp | seccomp 过滤规则 | —security-opt |
| linux.uidMappings | User Namespace UID 映射 | —userns |
| linux.rootPropagation | Mount 传播类型 | — |
3.4 容器生命周期
OCI Runtime Spec 定义了容器的标准生命周期:
3.5 运行时命令
OCI Runtime Spec 定义了运行时必须实现的命令:
| 命令 | 功能 | 状态变化 |
|---|---|---|
create | 创建容器(不启动进程) | → Created |
start | 启动容器进程 | Created → Running |
kill | 发送信号 | Running → Stopped |
delete | 删除容器和资源 | Stopped → (删除) |
state | 查询容器状态 | — |
features | 查询运行时特性 | — |
# OCI 运行时的标准操作流程runc create mycontainer # 创建容器(状态:Created)runc start mycontainer # 启动容器(状态:Running)runc state mycontainer # 查看状态runc kill mycontainer SIGTERM # 发送信号runc delete mycontainer # 删除容器四、OCI Distribution Spec
4.1 Registry API
Distribution Spec 定义了镜像仓库的标准 API:
| API | 方法 | 功能 |
|---|---|---|
/v2/ | GET | API 版本检查 |
/v2/<name>/manifests/<reference> | GET/PUT/DELETE | Manifest 操作 |
/v2/<name>/blobs/<digest> | GET/PUT/DELETE | Blob(层/配置)操作 |
/v2/<name>/tags/list | GET | 标签列表 |
/v2/<name>/blobs/uploads/ | POST | 开始上传 Blob |
# 使用 OCI Distribution API 拉取镜像# 1. 获取 Manifestcurl -s -H "Accept: application/vnd.oci.image.manifest.v1+json" \ https://registry.example.com/v2/myapp/manifests/latest
# 2. 下载 Configcurl -s \ https://registry.example.com/v2/myapp/blobs/sha256:b5b2b2c507a...
# 3. 下载 Layercurl -s \ https://registry.example.com/v2/myapp/blobs/sha256:9834876dcfb...4.2 推送流程
4.3 镜像拉取流程
# containerd 的镜像拉取流程(简化)ctr image pull docker.io/library/nginx:latest
# 底层操作:# 1. GET /v2/library/nginx/manifests/latest → 获取 Manifest# 2. 解析 Manifest,获取 Config 和 Layers 的 digest# 3. 检查本地是否已有对应 blob(跳过已下载的层)# 4. GET /v2/library/nginx/blobs/sha256:xxx → 下载缺失的 blob# 5. 解压 layers 到 OverlayFS# 6. 验证 digest 一致性五、OCI 规范与容器运行时的关系
5.1 从 Image Spec 到 Runtime Spec
containerd 负责将 Image Spec 转换为 Runtime Spec:
// containerd 将 OCI Image 转换为 OCI Bundle(简化)func imageToBundle(image ocispec.Image, rootfs string) (*specs.Spec, error) { spec := &specs.Spec{ Version: specs.Version, Process: &specs.Process{ Args: image.Config.Cmd, Env: image.Config.Env, Cwd: image.Config.WorkingDir, }, Root: &specs.Root{ Path: rootfs, Readonly: false, }, Mounts: defaultMounts(), Linux: &specs.Linux{ Namespaces: defaultNamespaces(), }, }
// 处理 Entrypoint + Cmd if len(image.Config.Entrypoint) > 0 { spec.Process.Args = append(image.Config.Entrypoint, image.Config.Cmd...) }
// 处理 User if image.Config.User != "" { uid, gid := parseUser(image.Config.User) spec.Process.User = specs.User{UID: uid, GID: gid} }
return spec, nil}5.2 规范兼容性矩阵
| 运行时 | Image Spec | Runtime Spec | Distribution Spec |
|---|---|---|---|
| runc | — | — | |
| containerd | |||
| CRI-O | |||
| gVisor (runsc) | — | — | |
| Kata Containers | — | — | |
| WasmEdge | (扩展) | (扩展) | — |
低层运行时(runc/gVisor/Kata)只实现 Runtime Spec,不关心镜像格式和分发协议。高层运行时(containerd/CRI-O)实现全部三个规范。
六、动手实践
6.1 手动构建 OCI 镜像
#!/bin/bash# 手动构建 OCI 镜像(不使用 Docker)
set -eIMAGE_DIR="/tmp/oci-image"mkdir -p $IMAGE_DIR/blobs/sha256
# 1. 创建 rootfs 层ROOTFS_DIR="/tmp/rootfs"mkdir -p $ROOTFS_DIR/{bin,etc,dev,proc,sys}echo '#!/bin/sh' > $ROOTFS_DIR/bin/helloecho 'echo "Hello OCI!"' >> $ROOTFS_DIR/bin/hellochmod +x $ROOTFS_DIR/bin/helloecho "root:x:0:0::/root:/bin/sh" > $ROOTFS_DIR/etc/passwd
# 2. 打包层cd $ROOTFS_DIRtar -cf layer.tar .LAYER_DIGEST=$(sha256sum layer.tar | cut -d' ' -f1)mv layer.tar $IMAGE_DIR/blobs/sha256/$LAYER_DIGESTLAYER_SIZE=$(stat -c%s $IMAGE_DIR/blobs/sha256/$LAYER_DIGEST)
# 3. 创建 ConfigCONFIG=$(cat <<EOF{ "architecture": "amd64", "os": "linux", "config": {"Cmd": ["/bin/hello"]}, "rootfs": {"type": "layers", "diff_ids": ["sha256:$LAYER_DIGEST"]}}EOF)echo -n "$CONFIG" > config.jsonCONFIG_DIGEST=$(sha256sum config.json | cut -d' ' -f1)CONFIG_SIZE=$(stat -c%s config.json)mv config.json $IMAGE_DIR/blobs/sha256/$CONFIG_DIGEST
# 4. 创建 ManifestMANIFEST=$(cat <<EOF{ "schemaVersion": 2, "mediaType": "application/vnd.oci.image.manifest.v1+json", "config": {"mediaType": "application/vnd.oci.image.config.v1+json", "size": $CONFIG_SIZE, "digest": "sha256:$CONFIG_DIGEST"}, "layers": [{"mediaType": "application/vnd.oci.image.layer.v1.tar", "size": $LAYER_SIZE, "digest": "sha256:$LAYER_DIGEST"}]}EOF)echo -n "$MANIFEST" > manifest.jsonMANIFEST_DIGEST=$(sha256sum manifest.json | cut -d' ' -f1)mv manifest.json $IMAGE_DIR/blobs/sha256/$MANIFEST_DIGEST
# 5. 创建 Image Layoutecho '{"imageLayoutVersion":"1.0.0"}' > $IMAGE_DIR/oci-layoutecho "{\"schemaVersion\":2,\"manifests\":[{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:$MANIFEST_DIGEST\",\"size\":$(stat -c%s $IMAGE_DIR/blobs/sha256/$MANIFEST_DIGEST)}]}" > $IMAGE_DIR/index.json
echo "OCI Image created at $IMAGE_DIR"6.2 用 runc 运行自定义 Bundle
#!/bin/bash# 从 OCI 镜像创建 Bundle 并运行
# 1. 从上面的 OCI 镜像解压 rootfsBUNDLE_DIR="/tmp/my-bundle"mkdir -p $BUNDLE_DIR/rootfscd $BUNDLE_DIR
# 解压层到 rootfstar -xf /tmp/oci-image/blobs/sha256/* -C rootfs/
# 2. 生成 config.jsonrunc spec
# 3. 修改 process.argspython3 -c "import jsonwith open('config.json') as f: spec = json.load(f)spec['process']['args'] = ['/bin/hello']with open('config.json', 'w') as f: json.dump(spec, f, indent=2)"
# 4. 运行sudo runc run my-oci-container# Hello OCI!附、实践:手工构建 OCI Bundle 并运行
本节从零构建一个符合 OCI Runtime Spec 的 Bundle,用 runc 运行容器。所有命令需要 root 权限。
附.1 创建 OCI Bundle 目录
OCI Bundle 的最小结构:一个 rootfs/ 目录和一个 config.json 文件。
mkdir -p /tmp/oci-bundle/rootfscd /tmp/oci-bundle附.2 准备 rootfs
从 Docker 导出 Alpine 的文件系统作为 rootfs:
docker export $(docker create alpine:latest) | tar -xf - -C /tmp/oci-bundle/rootfs/附.3 生成 config.json
runc spec 命令生成符合 OCI Runtime Spec 的默认配置:
runc spec查看生成的配置摘要:
python3 -c "import jsonwith open('/tmp/oci-bundle/config.json') as f: spec = json.load(f)print('ociVersion:', spec.get('ociVersion'))print('process.args:', spec.get('process', {}).get('args'))print('namespaces:', [ns['type'] for ns in spec.get('linux', {}).get('namespaces', [])])"ociVersion: 1.2.0process.args: ['sh']namespaces: ['pid', 'network', 'ipc', 'uts', 'mount', 'cgroup']默认配置会创建 6 种 Namespace(PID/Network/IPC/UTS/Mount/Cgroup),运行 sh 命令。
附.4 修改 config.json
将默认的 sh 改为一条有输出的命令,方便验证:
python3 -c "import jsonwith open('/tmp/oci-bundle/config.json') as f: spec = json.load(f)spec['process']['args'] = ['/bin/sh', '-c', 'echo \"Hello from OCI container!\"; hostname; ps aux']spec['process']['terminal'] = Falsewith open('/tmp/oci-bundle/config.json', 'w') as f: json.dump(spec, f, indent=2)"注意:
process.terminal必须设为false,否则 runc 会尝试打开/dev/tty导致报错。
附.5 用 runc 运行
cd /tmp/oci-bundle && runc run my-oci-demoHello from OCI container!runcPID USER TIME COMMAND 1 root 0:00 ps aux关键观察:
hostname返回runc——UTS Namespace 生效,容器有独立的主机名ps aux只显示 1 个进程——PID Namespace 生效,容器有独立的进程树- 整个过程没有 Docker 参与——runc 直接读取 config.json + rootfs 创建容器
这就是 OCI Runtime Spec 的价值:一个 JSON 文件 + 一个 rootfs 目录 = 一个容器。Docker、containerd、CRI-O 都是在这个基础上添加更高级的管理能力。
注意:实验结束后清理:
runc delete my-oci-demo && rm -rf /tmp/oci-bundle
七、本章小结
上一章探讨了OverlayFS 分层文件系统。
| 规范 | 核心概念 | 关键文件/格式 |
|---|---|---|
| Image Spec | 镜像格式 | manifest.json, config.json, layer.tar |
| Runtime Spec | 运行时接口 | config.json, rootfs/ |
| Distribution Spec | 分发协议 | /v2/ API |
OCI 规范的核心价值在于标准化——任何符合规范的镜像可以在任何符合规范的运行时上运行。这种互操作性是容器生态繁荣的基础。
OCI 规范只定义了接口标准,不保证实现的安全性。runc、gVisor、Kata 都符合 Runtime Spec,但隔离强度差异巨大——runc 共享宿主内核,gVisor 用用户态内核拦截系统调用,Kata 用硬件虚拟化隔离。选型时要根据威胁模型决定,不能只看”OCI 兼容”就认为安全。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






