How do containerd and CRI-O differ as Kubernetes container runtimes, and what does runc actually do?
Quick Answer
containerd and CRI-O both implement the Kubernetes Container Runtime Interface but differ in scope. containerd is a general-purpose daemon that works with Docker, Kubernetes, and standalone use. CRI-O is built exclusively for Kubernetes with a smaller footprint. Both hand off actual container creation to runc, which sets up Linux namespaces, cgroups, and the root filesystem per the OCI spec.
Detailed Answer
Think of two car engines designed for the same chassis. containerd is a versatile engine that powers sedans, trucks, and race cars. It does more than any single car needs, but it works everywhere. CRI-O is an engine designed for one car model only. It is lighter, has fewer parts, and is tuned for exactly that chassis. Both engines use the same fuel injectors (runc) to actually combust the fuel (run the container).
In Kubernetes, the kubelet talks to the container runtime through the Container Runtime Interface (CRI), a gRPC API that handles image pulling, container lifecycle, and sandbox management. After Kubernetes dropped dockershim in v1.24, clusters moved to either containerd (with its built-in CRI plugin) or CRI-O. containerd is maintained by the CNCF and used by Docker Desktop, AWS EKS, Google GKE, and many self-managed clusters. CRI-O is maintained by the Kubernetes SIG-Node community and used mainly by Red Hat OpenShift and Fedora CoreOS. Both are production-proven and CNCF graduated projects.
Under the hood, when the kubelet sends a CreateContainer CRI request, the high-level runtime (containerd or CRI-O) pulls the container image if needed, unpacks it into a root filesystem using a snapshotter driver (overlayfs is most common), generates an OCI runtime spec (config.json), and calls the low-level OCI runtime. runc, the reference OCI runtime, reads that config.json and creates the container: it sets up Linux namespaces (pid, net, mnt, uts, ipc, user, cgroup), configures cgroups for resource limits, pivots the root filesystem, applies Seccomp and AppArmor profiles, drops capabilities, and finally execs the container's entrypoint process. The OCI runtime spec standardizes this interface so alternatives like crun (written in C for faster startup), gVisor's runsc (kernel-level sandboxing), or Kata Containers' kata-runtime (VM-based isolation) can replace runc without changing the high-level runtime.
At production scale, the choice between containerd and CRI-O depends on your organization. containerd is the safer default for most teams because it has broader ecosystem support, more documentation, and works across managed Kubernetes providers. CRI-O offers a smaller attack surface and tighter Kubernetes alignment since it matches Kubernetes release cycles and skips features irrelevant to Kubernetes. Performance differences are negligible for most workloads, but CRI-O's smaller codebase can be an advantage in security-audited environments. Teams should monitor runtime metrics including container start latency, image pull duration, pod sandbox creation time, and runtime daemon memory use.
The non-obvious gotcha is that switching runtimes on a running cluster requires draining nodes, reconfiguring the kubelet's --container-runtime-endpoint, and potentially reformatting the container storage directory because containerd and CRI-O use different on-disk layouts. Another trap is assuming OCI compatibility means feature parity: gVisor's runsc intercepts system calls for sandboxing but has compatibility gaps with some applications, and Kata Containers add VM startup latency. Architects must test their specific workloads against alternative runtimes instead of assuming drop-in replacement.