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
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.
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 |
|---|---|---|
| Docker | Container format + runc runtime | Seed technology |
| Kubernetes expertise | Runtime consumer | |
| Microsoft | Windows container support | Cross-platform |
| Red Hat | Enterprise Linux expertise | Enterprise adoption |
| CoreOS | appc lessons, rkt experience | Standards design |
| AWS | Cloud infrastructure scale | Cloud provider |
| IBM | Enterprise systems | Enterprise adoption |
| VMware | Virtualisation expertise | VM-container bridge |
Three Specifications
OCI maintains three complementary specifications that together define the complete container lifecycle from build to distribution to execution:
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:
config.json— A JSON document specifying the container's configuration (namespaces, mounts, capabilities, environment, process)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 |
|---|---|---|
process | What runs inside the container | args, env, cwd, capabilities, user |
root | The container's root filesystem | path (to rootfs), readonly flag |
mounts | Additional mount points | destination, type, source, options |
linux.namespaces | Kernel isolation boundaries | pid, network, mount, uts, ipc, cgroup, user |
linux.resources | Cgroup resource limits | memory.limit, cpu.shares/quota/period |
linux.seccomp | System call filtering | defaultAction, syscalls whitelist |
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:
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 |
|---|---|---|
createRuntime | After runtime environment created, before pivot_root | Setup network interfaces, mount additional filesystems |
createContainer | After pivot_root, before process start | Inject files into container rootfs, security scanning |
startContainer | After user process started in container namespace | Notification, health check initialization |
poststart | After start operation returns (process running) | Register with service discovery, logging |
poststop | After container process exits, before delete | Cleanup 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
}
]
}
}
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:
- Image Manifest — Links the configuration and layers together for a single platform
- Image Index — Multi-platform pointer (fat manifest) that selects the right manifest per architecture/OS
- Configuration Object — Metadata: environment variables, entrypoint, exposed ports, layer history
- 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 |
|---|---|---|
mediaType | Content type of the layer | application/vnd.oci.image.layer.v1.tar+gzip |
digest | SHA256 hash for verification | sha256:9834876dcfb0... |
size | Compressed size in bytes | 32654 |
annotations | Optional metadata | Creation 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
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/list | List 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...
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 Charts | application/vnd.cncf.helm.chart.content.v1.tar+gzip | helm push |
| Cosign Signatures | application/vnd.dev.cosign.simplesigning.v1+json | cosign sign |
| SBOM (CycloneDX) | application/vnd.cyclonedx+json | syft |
| WASM Modules | application/vnd.wasm.content.layer.v1+wasm | wasm-to-oci |
| Flux Source | application/vnd.cncf.flux.content.v1.tar+gzip | flux push |
| Notation Signatures | application/vnd.cncf.notary.signature | notation 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 |
|---|---|---|---|
| runc | Go | Reference implementation | Default for Docker & containerd |
| crun | C | 2× faster startup, lower memory | Podman default, performance-critical |
| youki | Rust | Memory safety, modern codebase | Security-focused environments |
| kata-containers | Go/Rust | VM-based isolation | Multi-tenant, untrusted workloads |
| gVisor (runsc) | Go | User-space kernel | Defence in depth, sandboxing |
| Windows HCS | C++ | Windows container support | Windows Server containers |
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 Building | docker build | BuildKit, Buildah, kaniko, ko, Bazel |
| Image Storage | Docker Hub | GHCR, ECR, GCR, ACR, Harbor, Quay |
| Container Runtime | Docker Engine | containerd, CRI-O, Podman |
| Low-level Runtime | runc (Docker's) | runc (standalone), crun, youki, kata |
| CLI Interface | docker CLI | nerdctl, podman, crictl |
| Image Scanning | Docker Scout | Trivy, Grype, Snyk, Clair |
| Image Signing | (none built-in) | Cosign, Notation, DCT |
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
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
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}'
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
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.