# OCI spec - стандарт контейнеров _Контейнеры (бонус) · LinuxLab Knowledge Base_ **TL;DR:** OCI - три спеки: Image (слои + manifest), Runtime (config.json + rootfs для runc), Distribution (registry API). Стандарт после Docker'а; runc, podman, containerd, CRI-O - всё OCI-compatible. ## Зачем 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/ ├── ← config (JSON) ├── ← layer (tar или tar.gz) ├── └── ← manifest (связывает config + layers) ``` ### index.json ```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 ```json { "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, WORKDIR - `layers`, упорядоченный список tarball'ов ### Config ```json { "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.` 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 ```bash # 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, что внутри ```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](/kb/namespaces.md), какие [cgroups](/kb/cgroups.md) лимиты, какие [capabilities](/kb/capabilities.md), какой [seccomp](/kb/seccomp.md)-profile. ## OCI Distribution, registry API HTTP API registry'ев. Главные endpoint'ы: ``` GET /v2/ ← ping GET /v2//tags/list ← список тегов GET /v2//manifests/ ← manifest по tag/digest GET /v2//blobs/ ← скачать слой/config POST /v2//blobs/uploads/ ← начать upload PUT /v2//manifests/ ← залить manifest ``` - ``, image name (`library/ubuntu`, `myorg/myapp`) - ``, 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. ```bash # Сырой запрос к registry curl -H "Accept: application/vnd.oci.image.manifest.v1+json" \ https://registry-1.docker.io/v2/library/alpine/manifests/latest ``` ## skopeo, низкоуровневая работа с OCI ```bash # Скопировать 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). ```bash # Получить 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) ## Команды ```bash skopeo inspect docker://nginx:latest ``` Manifest и config image'а без pull - быстрый view ```bash skopeo copy docker://src/app:v1 oci:/tmp/app-oci:v1 ``` Сохранить image в OCI-layout на диск - переносной формат ```bash docker manifest inspect --verbose nginx:latest ``` Multi-arch index и каждый platform-manifest ```bash docker inspect --format='{{index .RepoDigests 0}}' nginx:1.25 ``` Digest текущего tag - для immutable-pinning в prod ```bash umoci unpack --image image:tag bundle/ ``` Распаковать OCI-image в OCI-runtime-bundle (config.json + rootfs) ```bash buildah bud -t myapp:v1 . ``` Build OCI-image без Docker daemon - rootless-friendly ```bash podman pull --platform=linux/arm64 nginx:latest ``` Принудительная архитектура при pull - для cross-arch dev ## См. также - [runc, runsc, kata - container runtimes](/kb/runc-and-runsc.md) - [Docker storage drivers - overlay2, btrfs, zfs](/kb/docker-storage-drivers.md) - [tmpfs и overlayfs - RAM-disk и слои](/kb/tmpfs-overlayfs.md) - [Linux namespaces](/kb/namespaces.md) - [Kubernetes pod lifecycle - от Pending до Terminated](/kb/kubernetes-pod-lifecycle.md) - [Cosign и подпись container images (sigstore)](/kb/image-signing-cosign.md)