Container Runtime Internals: containerd, CRI-O, and the OCI Spec
How the OCI runtime and image specs standardized what a 'container' actually is, and how containerd/CRI-O/runc fit together beneath Docker and Kubernetes.
“Docker,” “containerd,” “CRI-O,” and “runc” get used almost interchangeably in casual conversation, but they occupy genuinely distinct layers in the actual container execution stack — and the reason they can be swapped in and out of Kubernetes without breaking anything is a deliberate standardization effort: the OCI (Open Container Initiative) specs.
The layers, from top to bottom
A useful mental model, from what you interact with down to what actually creates namespaces and cgroups:
kubectl / docker CLI — user-facing tools
│
CRI (Container Runtime Interface) — Kubernetes' abstraction boundary
│
containerd / CRI-O — high-level runtime: image pulling, storage, lifecycle
│
runc (or another OCI runtime) — low-level runtime: actually creates the container
│
Linux kernel (namespaces, cgroups)
Docker itself, under the hood, has used containerd as its own high-level runtime for years — Docker Engine is a layer of UX and API on top of largely the same underlying components Kubernetes uses directly.
The OCI Runtime Spec: what “run a container” actually means
The OCI Runtime Specification defines a precise, tool-agnostic contract: given a root filesystem and a JSON configuration file (config.json) describing namespaces, mounts, capabilities, and the process to execute, a compliant runtime must produce an equivalent running container. runc is the reference implementation, but crun, kata-runtime (running containers inside lightweight VMs for stronger isolation), and others all implement the same spec.
{
"ociVersion": "1.0.2",
"process": {
"args": ["/bin/sh"],
"cwd": "/"
},
"linux": {
"namespaces": [
{"type": "pid"},
{"type": "network"},
{"type": "mount"}
]
}
}
runc run mycontainer
This standardization is exactly why Kata Containers (running each “container” inside its own lightweight VM for hardware-level isolation, addressing the shared-kernel risk namespaces alone don’t fully solve) can be swapped in as a drop-in alternative to runc — both satisfy the same OCI Runtime Spec contract.
The OCI Image Spec: what an image actually is
Separately, the OCI Image Specification defines the format of a container image itself — a manifest listing content-addressed layers (each a tar archive, typically gzip-compressed) plus a config blob describing environment variables, the entrypoint, and exposed ports:
docker manifest inspect nginx:1.25
{
"schemaVersion": 2,
"config": {"digest": "sha256:abc123...", "size": 7023},
"layers": [
{"digest": "sha256:def456...", "size": 31411704}
]
}
Because every image-building and image-running tool targets this same format, an image built by docker build, buildah, or podman build is fully interoperable with any OCI-compliant runtime, regardless of which tool produced or eventually runs it.
CRI: Kubernetes’ plug-in point for runtimes
The Container Runtime Interface (CRI) is a gRPC API Kubernetes’ kubelet speaks to whatever container runtime is configured on a node — this is the abstraction that let Kubernetes drop direct Docker-specific integration (dockershim) in favor of talking to any CRI-compliant runtime uniformly:
crictl ps
crictl images
crictl inspect <container-id>
containerd and CRI-O both implement CRI directly; crictl is the CRI-native equivalent of docker ps/docker inspect, useful for debugging directly on a Kubernetes node without going through kubectl at all.
Why containerd and CRI-O both exist
containerd began as an extraction of Docker’s own internal runtime component into a standalone, CNCF-governed project — broadly general-purpose, used by Docker itself and directly by Kubernetes. CRI-O was built specifically and only to implement CRI for Kubernetes, with a narrower scope and no independent CLI-driven use case of its own outside that role. Both wrap runc (or another OCI runtime) underneath and both are CNCF graduated projects — the choice between them in a given Kubernetes distribution is largely a matter of ecosystem fit rather than one being definitively superior.
systemctl status containerd
systemctl status crio
Why the layering, in practice
The value of this OCI-standardized stack shows up whenever you need to swap a layer without disrupting everything above it: switching from runc to crun (a faster, lower-memory-footprint OCI runtime) or to kata-runtime (for VM-level isolation) requires no changes to containerd, no changes to Kubernetes, and no changes to how images are built — because every layer only depends on the OCI-specified contract of the layer below it, not on that layer’s specific implementation. That’s the entire point of standardizing the spec rather than just standardizing on one tool.