Skip to content
daniel@cosenza:~/blog
SRE & DevOpsDeep Dive March 21, 2026 4 min read

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.