Back to Containers & Runtime Environments Mastery Series

Part 13: OCI Standards & Specifications

May 14, 2026 Wasil Zafar 22 min read

The container ecosystem could have become a fragmented wasteland of incompatible implementations. Instead, the Open Container Initiative (OCI) established three specifications that ensure any container image built anywhere can run on any compliant runtime. Understanding these standards is essential for anyone working beyond Docker's surface — they are the constitution of the container world.

Table of Contents

  1. Why Standards Matter
  2. The Open Container Initiative
  3. Runtime Specification
  4. Container Lifecycle
  5. Image Specification
  6. Content Addressability
  7. Distribution Specification
  8. OCI Artifacts
  9. Compliance & Conformance
  10. Impact on the Ecosystem
  11. Exercises
  12. Conclusion & Next Steps

Why Standards Matter

Before the Open Container Initiative existed, the container ecosystem was heading toward a dangerous fragmentation. Docker had single-handedly popularised containers, but its proprietary control over the format, runtime, and registry protocol meant the entire industry depended on one company's decisions. Other players — Google, Red Hat, CoreOS — watched nervously as Docker became the de facto standard without formal governance.

The Docker vs rkt War

In December 2014, CoreOS launched rkt (pronounced "rocket"), a direct competitor to Docker's runtime. CoreOS argued that Docker had become a monolithic, opinionated platform rather than a simple container runtime. Their criticisms were pointed:

  • Security concerns — Docker ran as a root daemon, creating a single point of compromise
  • Monolithic design — Image building, container running, registry operations, and orchestration all in one binary
  • Vendor lock-in — No formal specification meant "Docker-compatible" was whatever Docker decided
  • Governance — One company controlled the roadmap for infrastructure that entire industries depended on
Industry Event December 2014
The CoreOS/Docker Split

CoreOS CEO Alex Polvi published a blog post titled "CoreOS is building a container runtime, rkt" that sent shockwaves through the industry. The key quote: "We should stop talking about Docker containers, and start talking about the Docker Platform. It is not becoming the simple composable building block we had envisioned."

CoreOS proposed the App Container (appc) specification — an open standard for container images and runtimes. rkt implemented appc, creating a genuine fork in the ecosystem. The industry faced a choice: Docker's proprietary format or appc's open standard. Neither was ideal — the community needed a neutral body.

Result: The competitive pressure forced Docker to the table. Within six months, Docker donated its container format and runtime code to a new neutral foundation — the Open Container Initiative. The appc specification was eventually deprecated in favour of OCI, and rkt was archived in 2020. But without CoreOS's challenge, OCI might never have existed.

Open Standards Competition Governance
The Lesson: Standards don't emerge from consensus alone — they emerge from competitive pressure. Docker's dominance was only challenged when CoreOS demonstrated a credible alternative. The resulting OCI standards benefit everyone, including Docker, because they guarantee that investments in container tooling won't become stranded assets.

The Open Container Initiative

The Open Container Initiative (OCI) was founded on June 22, 2015, at DockerCon, under the Linux Foundation's governance. Docker donated its container image format and runtime code (which became runc) as the seed technology. The founding members read like a who's who of cloud infrastructure:

Founding Member Primary Contribution Role
DockerContainer format + runc runtimeSeed technology
GoogleKubernetes expertiseRuntime consumer
MicrosoftWindows container supportCross-platform
Red HatEnterprise Linux expertiseEnterprise adoption
CoreOSappc lessons, rkt experienceStandards design
AWSCloud infrastructure scaleCloud provider
IBMEnterprise systemsEnterprise adoption
VMwareVirtualisation expertiseVM-container bridge

Three Specifications

OCI maintains three complementary specifications that together define the complete container lifecycle from build to distribution to execution:

OCI's Role in the Container Ecosystem
flowchart TD
    subgraph BUILD["Build Time"]
        A[Dockerfile / Buildpack] --> B[Container Image]
    end
    subgraph OCI["OCI Specifications"]
        C["Image Spec
(image format)"] D["Distribution Spec
(push/pull protocol)"] E["Runtime Spec
(execution contract)"] end subgraph RUN["Run Time"] F[Container Runtime] --> G[Running Container] end B --> C C --> D D --> |"Registry"| E E --> F style OCI fill:#f0f9f9,stroke:#3B9797 style BUILD fill:#f8f9fa,stroke:#132440 style RUN fill:#f8f9fa,stroke:#132440

Each specification is independent — you can implement one without the others. A registry only needs the Distribution Spec. A container runtime only needs the Runtime Spec. An image builder only needs the Image Spec. This separation of concerns enables the diverse ecosystem we have today.

Runtime Specification (runtime-spec)

The Runtime Specification defines how to run a "filesystem bundle" — a directory containing everything needed to create and start a container. It answers the question: given a rootfs and a configuration, what should a container runtime do?

The Filesystem Bundle

An OCI filesystem bundle consists of exactly two things:

  1. config.json — A JSON document specifying the container's configuration (namespaces, mounts, capabilities, environment, process)
  2. rootfs/ — A directory containing the container's root filesystem

The config.json is the heart of the Runtime Spec. Here's a comprehensive example showing the key fields:

{
    "ociVersion": "1.1.0",
    "process": {
        "terminal": true,
        "user": {
            "uid": 0,
            "gid": 0
        },
        "args": ["/bin/sh"],
        "env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "TERM=xterm"
        ],
        "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": false
    },
    "hostname": "my-container",
    "mounts": [
        { "destination": "/proc", "type": "proc", "source": "proc" },
        { "destination": "/dev", "type": "tmpfs", "source": "tmpfs",
          "options": ["nosuid", "strictatime", "mode=755", "size=65536k"] },
        { "destination": "/sys", "type": "sysfs", "source": "sysfs",
          "options": ["nosuid", "noexec", "nodev", "ro"] }
    ],
    "linux": {
        "namespaces": [
            { "type": "pid" },
            { "type": "network" },
            { "type": "ipc" },
            { "type": "uts" },
            { "type": "mount" },
            { "type": "cgroup" }
        ],
        "resources": {
            "memory": { "limit": 536870912 },
            "cpu": { "shares": 1024, "quota": 100000, "period": 100000 }
        },
        "seccomp": {
            "defaultAction": "SCMP_ACT_ERRNO",
            "architectures": ["SCMP_ARCH_X86_64"],
            "syscalls": [
                { "names": ["read", "write", "exit", "exit_group"], "action": "SCMP_ACT_ALLOW" }
            ]
        }
    }
}

Key sections explained:

Section Purpose Key Fields
processWhat runs inside the containerargs, env, cwd, capabilities, user
rootThe container's root filesystempath (to rootfs), readonly flag
mountsAdditional mount pointsdestination, type, source, options
linux.namespacesKernel isolation boundariespid, network, mount, uts, ipc, cgroup, user
linux.resourcesCgroup resource limitsmemory.limit, cpu.shares/quota/period
linux.seccompSystem call filteringdefaultAction, syscalls whitelist
Platform Support: The Runtime Spec is designed to be cross-platform. The linux section is Linux-specific; Windows containers use a windows section with different fields (HyperV isolation, layer folders, networking). Solaris and FreeBSD sections also exist. The common fields (process, root, mounts) are platform-agnostic.

Container Lifecycle

The Runtime Spec defines a strict state machine that all compliant runtimes must implement. A container moves through well-defined states with explicit transition operations:

OCI Container State Machine
stateDiagram-v2
    [*] --> creating: create
    creating --> created: container ready
    created --> running: start
    running --> stopped: exit / kill
    stopped --> [*]: delete

    note right of creating: Namespaces created\nFilesystem mounted\nHooks: createRuntime, createContainer
    note right of created: Process NOT started\nHook: startContainer
    note right of running: Process executing\nHook: poststart
    note right of stopped: Process exited\nHook: poststop
                            

Lifecycle Hooks

OCI hooks allow custom actions at specific points in the container lifecycle. They are powerful for logging, networking setup, security scanning, and resource cleanup:

Hook When Invoked Use Case
createRuntimeAfter runtime environment created, before pivot_rootSetup network interfaces, mount additional filesystems
createContainerAfter pivot_root, before process startInject files into container rootfs, security scanning
startContainerAfter user process started in container namespaceNotification, health check initialization
poststartAfter start operation returns (process running)Register with service discovery, logging
poststopAfter container process exits, before deleteCleanup network, release resources, audit logging
{
    "hooks": {
        "createRuntime": [
            {
                "path": "/usr/bin/setup-network",
                "args": ["setup-network", "--container-id", "abc123"],
                "env": ["IFACE=eth0"],
                "timeout": 10
            }
        ],
        "poststop": [
            {
                "path": "/usr/bin/cleanup",
                "args": ["cleanup", "--remove-netns"],
                "timeout": 5
            }
        ]
    }
}
Hook Timeouts: Always set timeouts on hooks. A hanging prestart hook will block the container from starting indefinitely. In production, a stuck hook can cascade to orchestrator timeouts, restarts, and service outages. The timeout field specifies seconds before the runtime kills the hook process.

Image Specification (image-spec)

The Image Specification defines the format for container images — the portable, self-contained packages that contain everything needed to run an application. It separates the format of images from the runtime that executes them and the registry that stores them.

Core Components

An OCI image consists of three components linked by content-addressable references:

  1. Image Manifest — Links the configuration and layers together for a single platform
  2. Image Index — Multi-platform pointer (fat manifest) that selects the right manifest per architecture/OS
  3. Configuration Object — Metadata: environment variables, entrypoint, exposed ports, layer history
  4. Filesystem Layers — The actual content as tar+gzip archives, applied in order
{
    "schemaVersion": 2,
    "mediaType": "application/vnd.oci.image.manifest.v1+json",
    "config": {
        "mediaType": "application/vnd.oci.image.config.v1+json",
        "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7",
        "size": 7023
    },
    "layers": [
        {
            "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
            "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0",
            "size": 32654
        },
        {
            "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
            "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b",
            "size": 16724
        },
        {
            "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
            "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736",
            "size": 73109
        }
    ],
    "annotations": {
        "org.opencontainers.image.created": "2026-05-14T10:00:00Z",
        "org.opencontainers.image.authors": "engineering@example.com"
    }
}

Layer Format

Field Purpose Example Value
mediaTypeContent type of the layerapplication/vnd.oci.image.layer.v1.tar+gzip
digestSHA256 hash for verificationsha256:9834876dcfb0...
sizeCompressed size in bytes32654
annotationsOptional metadataCreation time, description

Layers are tar archives compressed with gzip (or zstd for better performance). Each layer represents a set of filesystem changes — added files, modified files, and deleted files (represented by "whiteout" files prefixed with .wh.). Layers are applied bottom-up to reconstruct the final filesystem.

Content Addressability

Every component in the OCI image format is referenced by its content digest — a cryptographic hash (SHA256) of the raw bytes. This design enables three critical properties:

  • Immutability — If the content changes, the digest changes. You can never silently modify a published layer
  • Deduplication — Identical layers across different images share the same digest and storage
  • Verification — After downloading, recalculate the hash to confirm no corruption or tampering
# Pull an image and inspect its digest
docker pull alpine:3.19
# 3.19: Pulling from library/alpine
# Digest: sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b

# Verify the digest locally
docker inspect alpine:3.19 --format='{{index .RepoDigests 0}}'
# alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b

# Pull by digest (immutable reference)
docker pull alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b

# Inspect image layers and their digests
docker inspect alpine:3.19 --format='{{range .RootFS.Layers}}{{.}}{{"\n"}}{{end}}'
# sha256:d4fc045c9e3a848011de66f34b81f052d4f2c15a17a5b3e6e41694ed9b2402d8

# Save image as tar and inspect manifest
docker save alpine:3.19 -o alpine.tar
tar -xf alpine.tar
cat manifest.json | python3 -m json.tool
Security Implication: Content addressability means that docker pull myapp@sha256:abc123... will always return the same bytes, regardless of which registry serves them. Tags (like :latest) are mutable pointers — they can be overwritten. Digests are immutable references. In production, always pin images by digest, not tag.

Distribution Specification (distribution-spec)

The Distribution Specification defines the HTTP API for pushing and pulling container images from registries. It ensures that any OCI-compliant client can interact with any OCI-compliant registry — Docker Hub, GitHub Container Registry, AWS ECR, Google Artifact Registry, Azure Container Registry, or your own private registry.

Core API Endpoints

Method Endpoint Purpose
GET/v2/API version check (returns 200 if registry supports OCI)
GET/v2/{name}/manifests/{reference}Pull a manifest (by tag or digest)
PUT/v2/{name}/manifests/{reference}Push a manifest
GET/v2/{name}/blobs/{digest}Pull a blob (layer or config)
POST/v2/{name}/blobs/uploads/Initiate blob upload
PATCH/v2/{name}/blobs/uploads/{uuid}Upload blob data (chunked)
PUT/v2/{name}/blobs/uploads/{uuid}?digest=sha256:...Complete blob upload
HEAD/v2/{name}/blobs/{digest}Check if blob exists (avoid re-upload)
GET/v2/{name}/tags/listList tags for a repository
GET/v2/{name}/referrers/{digest}List referrers (signatures, SBOMs)
# Interact with a registry directly using curl
# Step 1: Check API support
curl -s https://registry-1.docker.io/v2/ | head -1
# {"errors":[{"code":"UNAUTHORIZED"...}]}
# (401 means API exists, just needs auth)

# Step 2: Get authentication token (Docker Hub specific)
TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/alpine:pull" \
  | jq -r '.token')

# Step 3: Pull the manifest for alpine:3.19
curl -s -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/vnd.oci.image.index.v1+json" \
  https://registry-1.docker.io/v2/library/alpine/manifests/3.19 | jq '.manifests[0]'

# Step 4: List tags
curl -s -H "Authorization: Bearer $TOKEN" \
  https://registry-1.docker.io/v2/library/alpine/tags/list | jq '.tags[:10]'

# Step 5: Check if a blob exists (HEAD request)
curl -sI -H "Authorization: Bearer $TOKEN" \
  https://registry-1.docker.io/v2/library/alpine/blobs/sha256:c5b1261d6d3e...
Cross-Mount Optimization: The Distribution Spec supports cross-repository blob mounting. If a layer already exists in another repository on the same registry, you can mount it instead of re-uploading. This is why pushing a new tag of a large image is fast — only new layers are uploaded, existing ones are mounted by digest.

OCI Artifacts

OCI registries were designed for container images, but their content-addressable storage is useful for any artefact. The OCI community recognised this and formalised OCI Artifacts — the ability to store non-container content using the same registry infrastructure.

What Can Be Stored as OCI Artifacts?

Artifact Type Media Type Example Tool
Helm Chartsapplication/vnd.cncf.helm.chart.content.v1.tar+gziphelm push
Cosign Signaturesapplication/vnd.dev.cosign.simplesigning.v1+jsoncosign sign
SBOM (CycloneDX)application/vnd.cyclonedx+jsonsyft
WASM Modulesapplication/vnd.wasm.content.layer.v1+wasmwasm-to-oci
Flux Sourceapplication/vnd.cncf.flux.content.v1.tar+gzipflux push
Notation Signaturesapplication/vnd.cncf.notary.signaturenotation sign
# Push a Helm chart to an OCI registry
helm package ./my-chart
helm push my-chart-1.0.0.tgz oci://ghcr.io/myorg/charts

# Pull a Helm chart from OCI registry
helm pull oci://ghcr.io/myorg/charts/my-chart --version 1.0.0

# Sign an image with cosign (stores signature as OCI artifact)
cosign sign --key cosign.key ghcr.io/myorg/myapp:v1.0.0

# Attach an SBOM to an image
oras attach ghcr.io/myorg/myapp:v1.0.0 \
  --artifact-type application/spdx+json \
  ./sbom.spdx.json

# List referrers (signatures, SBOMs attached to an image)
oras discover ghcr.io/myorg/myapp:v1.0.0

The Referrers API (/v2/{name}/referrers/{digest}) is the key innovation: it allows discovering all artefacts associated with a given image — signatures, SBOMs, attestations — without knowing their digests in advance. This enables supply chain security workflows where you can verify that an image has been signed, scanned, and attested before deployment.

Compliance & Conformance

How does a runtime prove it correctly implements the OCI specification? The OCI provides conformance test suites that exercise every aspect of the specification. A runtime that passes all tests can claim OCI compliance.

Runtime Conformance

# Clone the OCI runtime-tools repository
git clone https://github.com/opencontainers/runtime-tools.git
cd runtime-tools

# Build the validation tool
make runtimetest

# Run conformance tests against runc
sudo ./validation/default.t --runtime /usr/bin/runc

# Run individual test categories
sudo ./validation/linux_namespaces.t --runtime /usr/bin/runc
sudo ./validation/process.t --runtime /usr/bin/runc
sudo ./validation/mounts.t --runtime /usr/bin/runc

OCI-Compliant Runtimes

Runtime Language Key Feature Use Case
runcGoReference implementationDefault for Docker & containerd
crunC2× faster startup, lower memoryPodman default, performance-critical
youkiRustMemory safety, modern codebaseSecurity-focused environments
kata-containersGo/RustVM-based isolationMulti-tenant, untrusted workloads
gVisor (runsc)GoUser-space kernelDefence in depth, sandboxing
Windows HCSC++Windows container supportWindows Server containers
Why Compliance Matters: If you build an image on your laptop with Docker (which uses runc), push it to a registry, and Kubernetes pulls it onto a node running containerd with kata-containers — OCI compliance guarantees it will work. The image format is standard. The runtime contract is standard. The registry protocol is standard. No vendor lock-in.

Impact on the Ecosystem

OCI's greatest achievement isn't the specifications themselves — it's the ecosystem they enabled. Before OCI, "container" meant "Docker". After OCI, an entire ecosystem of interchangeable tools emerged:

Function Docker (Pre-OCI) Post-OCI Alternatives
Image Buildingdocker buildBuildKit, Buildah, kaniko, ko, Bazel
Image StorageDocker HubGHCR, ECR, GCR, ACR, Harbor, Quay
Container RuntimeDocker Enginecontainerd, CRI-O, Podman
Low-level Runtimerunc (Docker's)runc (standalone), crun, youki, kata
CLI Interfacedocker CLInerdctl, podman, crictl
Image ScanningDocker ScoutTrivy, Grype, Snyk, Clair
Image Signing(none built-in)Cosign, Notation, DCT
Post-OCI Container Ecosystem
flowchart LR
    subgraph Build["Image Builders"]
        B1[BuildKit]
        B2[Buildah]
        B3[kaniko]
    end
    subgraph Registry["OCI Registries"]
        R1[Docker Hub]
        R2[GHCR]
        R3[ECR/GCR/ACR]
    end
    subgraph HighRT["High-Level Runtimes"]
        H1[containerd]
        H2[CRI-O]
    end
    subgraph LowRT["Low-Level Runtimes"]
        L1[runc]
        L2[crun]
        L3[kata]
        L4[gVisor]
    end
    Build -->|"OCI Image Spec"| Registry
    Registry -->|"Distribution Spec"| HighRT
    HighRT -->|"Runtime Spec"| LowRT
                            

This interchangeability is OCI's legacy. Kubernetes doesn't care which runtime you use — it communicates via CRI, which abstracts over any OCI-compliant runtime. You can switch from Docker to containerd to CRI-O without rebuilding a single image.

Exercises

Hands-On 30 minutes
Exercise 1: Inspect OCI Image Structure

Pull an image, save it as a tar, and examine the OCI manifest, config, and layers manually:

# Save an image to a tar archive
docker save nginx:alpine -o nginx-alpine.tar

# Extract and examine the contents
mkdir nginx-inspect && tar -xf nginx-alpine.tar -C nginx-inspect
ls nginx-inspect/

# Read the manifest
cat nginx-inspect/manifest.json | python3 -m json.tool

# Examine a layer
tar -tzf nginx-inspect//layer.tar | head -20
Image Spec Layers
Hands-On 20 minutes
Exercise 2: Query a Registry with curl

Use the Distribution Spec API directly to authenticate, pull manifests, and list tags:

# Get a token for the alpine repository
TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/nginx:pull" | jq -r '.token')

# List available tags
curl -s -H "Authorization: Bearer $TOKEN" \
  https://registry-1.docker.io/v2/library/nginx/tags/list | jq '.tags[:15]'

# Fetch the image index (fat manifest)
curl -s -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json" \
  https://registry-1.docker.io/v2/library/nginx/manifests/alpine | jq '.manifests[] | {platform: .platform, digest: .digest}'
Distribution Spec Registry API
Hands-On 25 minutes
Exercise 3: Generate and Examine a Runtime Spec

Use runc to generate a default config.json and understand the Runtime Spec fields:

# Create a directory for your OCI bundle
mkdir -p mycontainer/rootfs

# Export an image's rootfs
docker export $(docker create alpine:3.19) | tar -xC mycontainer/rootfs

# Generate a default OCI config.json
cd mycontainer
runc spec

# Examine the generated config.json
cat config.json | python3 -m json.tool | head -60

# Modify and run the container
runc run my-test-container
Runtime Spec config.json runc

Conclusion & Next Steps

The Open Container Initiative transformed containers from a single vendor's product into an industry-standard technology. The three specifications — Runtime, Image, and Distribution — form a complete contract: how to package applications, how to distribute them, and how to run them. Any tool that implements these specs interoperates with every other compliant tool.

Key takeaways from this article:

  • Runtime Spec defines the contract between a bundle (rootfs + config.json) and a container runtime — the lifecycle, hooks, and kernel features to use
  • Image Spec defines how to package filesystem layers with metadata into a content-addressable, portable format
  • Distribution Spec defines the HTTP API for pushing, pulling, and discovering images in registries
  • Content addressability (SHA256 digests) ensures immutability, deduplication, and verification at every layer
  • OCI Artifacts extend registries beyond containers to store Helm charts, signatures, SBOMs, and any content

Next in the Series

In Part 14: containerd & runc Deep Dive, we'll open the hood of the actual runtime implementations. You'll learn how containerd manages images, snapshots, and tasks via its gRPC API, how runc manipulates kernel namespaces and cgroups to create containers, and how the Container Runtime Interface (CRI) connects Kubernetes to any OCI-compliant runtime.