Docker & ContainersI · FoundationsOCI ecosystem
The OCI ecosystem
What you'll learn
- Identify the three OCI specifications and what each one defines
- State precisely where each specification stops, and what is deliberately left unspecified
- Explain how Docker, containerd, runc, and CRI-O interoperate via OCI
- Diagnose a push rejected for media types rather than for permissions
- Swap the OCI runtime on a host and verify the swap took effect
Prerequisites
None — start here.
Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12
The phrase “Docker image” is misleading. What you actually ship is an OCI image. What actually runs the container is an OCI runtime. The “Docker registry” protocol is the OCI distribution spec. None of these are owned by Docker, Inc.; they are maintained by the Open Container Initiative, a project under the Linux Foundation.
This separation is the most important reason your Docker skills transfer to Podman, CRI-O, containerd, Kubernetes, and everything else. OCI is the lingua franca.
The three OCI specifications
flowchart LR
Build[Build] -->|image-spec| Registry[(Registry)]
Registry -->|distribution-spec| Host[Container host]
Host -->|runtime-spec| Process[Running process]
1. OCI Image Format Specification
Defines what an “image” is, as content. Four kinds of object, all content-addressed by the SHA-256 of their own bytes and referenced by descriptors carrying a media type, a digest and a size:
- An index (sometimes called a manifest list) — a list of manifests, each tagged with a platform. This is what makes an image multi-platform.
- A manifest — one platform’s image: a descriptor for the config and an ordered list of descriptors for the layers.
- A config — JSON describing the intended runtime parameters:
Entrypoint,Cmd,Env,User,WorkingDir,Labels,ExposedPorts, plus therootfs.diff_idsand thehistoryarray. - Layers — tar archives (usually gzip-compressed) representing
filesystem changesets, including the
.wh.whiteout convention for deletions.
Where it stops. The image spec describes what the config
means as intent. It does not say how any of it is enforced.
ExposedPorts publishes nothing. Volumes mounts nothing.
User is a string, and nothing in the spec says how to resolve a
name to a UID. Those are all decisions for whatever runs the image.
2. OCI Runtime Specification
Note carefully what it does not take as input: an image. The runtime spec has no concept of images, registries, layers or tags.
It defines a bundle: a directory containing a config.json
and a rootfs/ directory. It defines what goes in that
config.json — the namespaces to create or join, the cgroup path
and resource limits, the capability sets, the seccomp profile, the
mounts, the process to execute, its arguments, environment, and
UID/GID. And it defines a lifecycle: create, start, kill,
delete, plus a state query.
A conforming runtime given a bundle is expected to:
- Set up namespaces, cgroups, capabilities, seccomp, AppArmor or SELinux labels.
- Mount the rootfs and the configured mounts, and
pivot_root. execvethe configured process as PID 1 of the new PID namespace.
runc is the reference implementation. crun is an alternative
written in C with lower overhead. youki is another in Rust.
Where it stops. It says nothing about how the rootfs came to
exist, nothing about pulling anything, and — importantly for
anyone debugging container networking — nothing about how a
network namespace gets an interface, an address or a route. The
runtime creates or joins the namespace and stops there. Filling it
in is somebody else’s job: dockerd on a Docker host, CNI plugins
under Kubernetes.
3. OCI Distribution Specification
Defines the HTTP API a registry serves. Every path begins /v2/;
manifests live at /v2/<name>/manifests/<reference> and blobs at
/v2/<name>/blobs/<digest>; there is a chunked upload flow for
pushing blobs, and a referrers endpoint for discovering artifacts
that point at an existing manifest — which is how signatures and
attestations are found.
Docker Registry, Harbor, GitLab Container Registry, Amazon ECR, Google Artifact Registry, and Quay all speak this protocol. You can pull from any of them with any OCI client.
Where it stops. Authentication is not in it. The bearer-token flow every registry implements is a de facto standard inherited from Docker, not part of the distribution spec, which is why credential-helper behaviour varies between registries in ways nothing formally governs. Storage layout, replication, retention and garbage collection are all out of scope too — which is why “the registry deleted my blob” is a per-product question.
The seams, which is where the failures are
You can see the output of that translation directly. The
config.json below is the runtime spec’s artifact, generated from
your image by containerd, and it is the ground truth for what the
container actually got:
CONTAINER=web
ID=$(docker inspect --format '{{.Id}}' "$CONTAINER")
BUNDLE=/run/containerd/io.containerd.runtime.v2.task/moby/"$ID"
jq '{args: .process.args, user: .process.user, caps: .process.capabilities.effective}' "$BUNDLE"/config.json# jq '{args: .process.args, user: .process.user}' /run/containerd/io.containerd.runtime.v2.task/moby/3f9a.../config.json{
"args": [
"/docker-entrypoint.sh",
"nginx",
"-g",
"daemon off;"
],
"user": {
"uid": 0,
"gid": 0,
"additionalGids": [0]
}
}Illustrative output
Media types: the part that decides whether a push works
Every descriptor carries a media type, and there are two families
for the same objects — the Docker ones
(application/vnd.docker.distribution.manifest.v2+json,
application/vnd.docker.distribution.manifest.list.v2+json) and
the OCI ones (application/vnd.oci.image.manifest.v1+json,
application/vnd.oci.image.index.v1+json).
They describe structurally compatible objects. A registry that has not been taught about the OCI ones will still reject them.
The runtime stack on a Docker host
Swapping the runtime, and proving it took
Because the runtime boundary is a real specification, runc is
replaceable. Docker documents registering an alternative in
daemon.json:
{
"runtimes": {
"youki": {
"path": "/usr/local/bin/youki"
}
}
}
Runtimes that are containerd shims rather than runc drop-ins are registered by shim type instead:
{
"runtimes": {
"gvisor": {
"runtimeType": "io.containerd.runsc.v1",
"options": {
"TypeUrl": "io.containerd.runsc.v1.options",
"ConfigPath": "/etc/containerd/runsc.toml"
}
}
}
}
runtimes is on the daemon’s SIGHUP-reloadable list, so
systemctl reload docker picks it up without disturbing anything.
Select it per container with --runtime:
docker info --format '{{.Runtimes}} default={{.DefaultRuntime}}'
docker run --rm --runtime youki hello-world
docker inspect --format '{{.HostConfig.Runtime}}' webThe last line is the verification that can fail. A container created before the daemon knew about the new runtime still reports the old one, because the runtime is fixed at creation — so “we switched to gVisor” is a claim about containers created since the change, not about the host.
What this means for the rest of the course
The labs in this course target Linux + containerd + runc. They will work with Docker (because Docker talks to containerd) and with Kubernetes (because Kubernetes talks to containerd via CRI). They will also work with Podman if Podman is configured to use the OCI runtime.
The commands we teach (docker run, docker exec, docker build)
have equivalents in every OCI runtime. The mental model — namespaces
- cgroups + filesystem isolation — does not change.
Knowledge check
Knowledge check · 6 questions
Q1. Which OCI specification defines how a registry serves images to clients?
Q2. Which of the following are OCI-compliant runtimes? Select all that apply.
Q3. When you run `docker run nginx`, the dockerd daemon itself creates and starts the container.
Q4. What does an OCI runtime take as its input?
Q5. Which of these are deliberately left unspecified by the OCI specifications? Select all that apply.
Q6. A container’s OCI runtime is fixed when the container is created, so changing the default runtime only affects containers created afterwards.
Passing score: 75%. Answers are checked in this browser.