Зачем OCI
В 2015 Docker согласился вынести спеки контейнеров из своего проекта, чтобы экосистема не зависела от одного вендора. Появилась Open Container Initiative (под Linux Foundation), которая поддерживает три отдельные спецификации:
| Спека | Что описывает |
|---|---|
| OCI Image | формат image на диске: layers + manifest + config |
| OCI Runtime | как runtime запускает контейнер из rootfs + config.json |
| OCI Distribution | HTTP API registry для push/pull |
Сегодня "Docker container", почти синоним "OCI container": Dockerfile → image → registry → runtime, каждый шаг по OCI. Альтернативные runtime'ы ([[runc-and-runsc|runc/runsc]]), registry'и (Harbor, GHCR, Quay), build-tool'ы (buildah, kaniko) все работают с одним форматом.
OCI Image, что это на диске
Image, это набор файлов на диске, не tarball. Структура:
myimage/
├── oci-layout ← {"imageLayoutVersion": "1.0.0"}├── index.json ← root, ссылается на manifest'ы
└── blobs/
└── sha256/
├── <hash-config> ← config (JSON)
├── <hash-layer1> ← layer (tar или tar.gz)
├── <hash-layer2>
└── <hash-manifest> ← manifest (связывает config + layers)
index.json
{"schemaVersion": 2,
"manifests": [
{"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:abc...",
"size": 1234,
"platform": { "architecture": "amd64", "os": "linux" }},
{"digest": "sha256:def...",
"platform": { "architecture": "arm64", "os": "linux" }}
]
}
→ multi-arch index. Один manifest на платформу.
Manifest
{"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:abc...",
"size": 7000
},
"layers": [
{ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip","digest": "sha256:111...", "size": 5000000 },
{ "digest": "sha256:222...", "size": 1000000 }, { "digest": "sha256:333...", "size": 50000 }]
}
Manifest содержит:
config, environment, entrypoint, ENV, USER, WORKDIRlayers, упорядоченный список tarball'ов
Config
{"architecture": "amd64",
"os": "linux",
"config": {"User": "1000:1000",
"Env": ["PATH=/usr/bin:/bin"],
"Entrypoint": ["/app/server"],
"Cmd": ["--port=8080"],
"WorkingDir": "/app",
"ExposedPorts": { "8080/tcp": {} }},
"rootfs": {"type": "layers",
"diff_ids": [
"sha256:layer1-uncompressed-hash",
"sha256:layer2-uncompressed-hash"
]
},
"history": [ ... ]
}
Layers, основа image-deduplication
Каждый слой, diff относительно предыдущего: добавленные/изменённые
файлы как tar-archive. Удалённые файлы, .wh.<filename> whiteout-маркеры.
При deploy registry загружает только отсутствующие слои. Если 100
image'ей основаны на одном ubuntu:22.04, слой Ubuntu хранится один
раз. На клиенте/registry storage экономия колоссальная.
Layers применяются через [[tmpfs-overlayfs|overlayfs]]: lower-stack из read-only слоёв + upper для container-writes.
Build vs Pull
# Build из Dockerfile
docker build -t myimage:v1 .
# Push в registry
docker push registry.example.com/myimage:v1
# Pull
docker pull registry.example.com/myimage:v1
# Без Docker, buildah / podman
buildah bud -t myimage:v1 .
buildah push myimage:v1 docker://registry.example.com/myimage:v1
buildah/podman не требуют daemon'а, работают как обычные CLI и пишут OCI-совместимые images.
OCI Runtime, config.json + rootfs
Runtime берёт bundle: директорию с
bundle/
├── config.json ← всё про контейнер (mounts, namespaces, args)
└── rootfs/ ← extracted layers, готовая FS-tree
├── bin/
├── etc/
└── usr/
и запускает контейнер. Это не image, это разобранный image плюс runtime-config. Image нужно "распаковать" в bundle прежде чем runtime может с ним работать (это делает runtime supervisor containerd, CRI-O).
config.json, что внутри
{"ociVersion": "1.2.0",
"process": {"args": ["/app/server", "--port=8080"],
"cwd": "/app",
"env": ["PATH=/usr/bin:/bin"],
"user": { "uid": 1000, "gid": 1000 }, "capabilities": {"bounding": ["CAP_NET_BIND_SERVICE"],
"effective": ["CAP_NET_BIND_SERVICE"],
"permitted": ["CAP_NET_BIND_SERVICE"]
},
"noNewPrivileges": true,
"rlimits": [ { "type": "RLIMIT_NOFILE", "hard": 65535, "soft": 65535 } ]},
"root": { "path": "rootfs", "readonly": false },"mounts": [
{ "destination": "/proc", "type": "proc", "source": "proc" }, { "destination": "/dev", "type": "tmpfs", "source": "tmpfs", "options": ["mode=755", "size=65536k"] }, { "destination": "/data", "type": "bind", "source": "/var/lib/myapp/data", "options": ["bind", "ro"] }],
"linux": {"namespaces": [
{ "type": "pid" }, { "type": "network" }, { "type": "mount" }, { "type": "uts" }, { "type": "ipc" }, { "type": "user" }],
"cgroupsPath": "system.slice:myapp:abc123",
"resources": { "memory": { "limit": 268435456 }, "cpu": { "shares": 1024, "quota": 50000, "period": 100000 }},
"seccomp": { "defaultAction": "SCMP_ACT_ALLOW", ... }}
}
Это полное описание контейнера: что запустить, какие namespaces, какие cgroups лимиты, какие capabilities, какой seccomp-profile.
OCI Distribution, registry API
HTTP API registry'ев. Главные endpoint'ы:
GET /v2/ ← ping
GET /v2/<name>/tags/list ← список тегов
GET /v2/<name>/manifests/<reference> ← manifest по tag/digest
GET /v2/<name>/blobs/<digest> ← скачать слой/config
POST /v2/<name>/blobs/uploads/ ← начать upload
PUT /v2/<name>/manifests/<reference> ← залить manifest
<name>, image name (library/ubuntu,myorg/myapp)<reference>, tag (v1.0) или digest (sha256:...)
Любой OCI-registry (Docker Hub, GHCR, Harbor, Quay, ECR, GCR, ACR, GitLab Registry) реализует это API. Pull/push кросс-совместим.
Аутентификация, Bearer-token, обычно через OAuth2 token-server.
# Сырой запрос к registry
curl -H "Accept: application/vnd.oci.image.manifest.v1+json" \
https://registry-1.docker.io/v2/library/alpine/manifests/latest
skopeo, низкоуровневая работа с OCI
# Скопировать image между registry'ями без локальной разборки
skopeo copy docker://registry.example.com/app:v1 \
docker://other-registry.com/app:v1
# Inspect manifest без pull
skopeo inspect docker://nginx:latest
# Сохранить как OCI-layout
skopeo copy docker://nginx:latest oci:/tmp/nginx-oci:latest
ls /tmp/nginx-oci/ # классическая OCI-структура
Tags vs digest, immutability
- Tag (
nginx:1.25), мутабельный pointer;latestособенно - Digest (
nginx@sha256:abc...), immutable, хеш manifest'а
В production-deploy всегда по digest, не по tag. Tag можно переписать в registry; digest, нет (изменишь content → изменится hash).
# Получить digest текущего tag'а
docker inspect --format='{{index .RepoDigests 0}}' nginx:1.25▸nginx@sha256:abcdef...
# Зафиксировать в Dockerfile / k8s manifest
FROM nginx@sha256:abcdef...
Когда что-то пошло не так
manifest unknownпри pull, tag не существует или удалён из registry.skopeo list-tags docker://registry/repo.- Multi-arch image не подтянулся, container runtime не нашёл
подходящего platform-manifest.
docker pull --platform=linux/arm64. unauthorized, нет токена или expire.docker login, проверь credentials в~/.docker/config.json/~/.config/containers/auth.json.- Image build долгий каждый раз, нет layer caching. Build-cache
invalidates при изменении любой строки выше; ставь
RUN apt-get installпослеCOPY package*.jsonчтобы переиспользовать слой. - OCI vs Docker manifest schema, старые registry отдают v1 manifest, modern, v2 OCI. Большинство клиентов умеют оба, но некоторые server-side validators могут падать.
- Digest mismatch при air-gapped transfer, после
gzip-перепаковки layer'а его SHA256 меняется → manifest invalid. Используйskopeoили сохраняй в OCI-layout.
Альтернативные форматы (на любителя)
- AppImage / Snap / Flatpak, для desktop, не контейнеры в OCI-смысле
- Singularity / Apptainer (.sif), научные кластеры, single-file image
- WASM components, ещё не контейнеры в OCI, но движется в эту сторону (некоторые runtime'ы запускают WASM через OCI-config)