20. Container Runtimes
Container Runtimes
TL;DR
- containerd and CRI-O are the two runtimes that actually run on production Kubernetes nodes; the full Docker Engine is no longer the default node-level runtime.
- Kubernetes talks to any runtime through the CRI (Container Runtime Interface), a standard API — containerd and CRI-O implement it directly and minimally; Docker Engine bundles far more than a kubelet needs.
- All of these runtimes execute the OCI image spec and the OCI runtime spec (via
runcor an equivalent), so a Docker-built image runs unmodified under containerd or CRI-O — no conversion step exists or is needed. - Podman targets a different problem: daemonless, rootless container execution for workstations and standalone use, not cluster-node scheduling.
- Biggest gotcha: assuming the Docker-to-containerd shift on Kubernetes nodes was a compatibility break. It wasn’t — OCI compliance made it transparent.
Core Concepts
CRI: how Kubernetes decouples from any specific runtime
The kubelet on every node doesn’t talk to “Docker” or “containerd” directly — it talks to the Container Runtime Interface (CRI), a gRPC API that any compliant runtime implements. This decoupling is what let Kubernetes drop its built-in dockershim (removed in v1.24) without breaking workloads: any CRI-compliant runtime is a drop-in replacement at the node level. containerd and CRI-O both implement CRI natively; the full Docker Engine historically required the dockershim translation layer because Docker Engine itself predates CRI and was never a CRI implementation.
containerd
containerd is what most production Kubernetes nodes run today. It handles the parts a kubelet actually needs: pulling and storing images, managing container lifecycle (create/start/stop/delete), and executing containers via a lower-level OCI runtime (typically runc). It deliberately excludes what Docker Engine bundles on top — a user-facing CLI, image build tooling (docker build), and its own networking stack — because none of that is relevant to node-level container execution. containerd is also used independently of Kubernetes, e.g. as the runtime underneath Docker Engine itself (Docker Engine has used containerd internally since Docker 1.11).
CRI-O
CRI-O was purpose-built from day one as a minimal CRI implementation with no scope beyond what Kubernetes needs from a node runtime. It has no CLI oriented at humans, no build tooling, nothing outside image pull/execute/lifecycle management via CRI. The practical difference from containerd is more about governance and footprint philosophy than a hard capability gap — both satisfy standard Kubernetes workloads. CRI-O is released and versioned in lockstep with Kubernetes minor versions, which appeals to teams wanting a runtime whose support window maps cleanly to their cluster’s Kubernetes version.
OCI: the interoperability layer underneath all of this
The Open Container Initiative (OCI) defines two specs that make runtime substitution possible: the image spec (the on-disk/registry format of a container image, independent of what tool built it) and the runtime spec (how a compliant runtime like runc unpacks and executes that image as an isolated process using Linux namespaces and cgroups). Because Docker, Podman, Buildah, and CI build tools all produce OCI-compliant images, and because containerd/CRI-O both execute OCI images via an OCI runtime, an image’s origin is irrelevant to which runtime can run it. This is the mechanical reason a Docker-built image runs identically under containerd or CRI-O with zero conversion.
Podman
Podman solves a workstation/standalone problem, not a cluster-scheduling problem. Architecturally it is daemonless: there is no persistent background service that every podman command talks to (unlike the traditional Docker Engine, where the docker CLI is a client to a long-running dockerd daemon). Each podman run invocation starts a container as a direct child process of the command that launched it. Podman also supports rootless operation by default — containers run under the invoking user’s UID via user namespaces, rather than requiring a root-owned daemon.
The security implication is concrete: a persistent, typically root-privileged daemon is a single high-value process — compromise it and you have broad control over every container it manages, and potentially the host. Removing that daemon removes the concentrated, elevated-privilege target. Podman’s CLI is intentionally Docker-compatible (alias docker=podman works for most common commands), which lowers the switching cost.
containerd vs CRI-O vs Docker Engine (and runc)
| Aspect | containerd | CRI-O | Docker Engine |
|---|---|---|---|
| CRI-native | Yes, built in | Yes, built in (purpose-built for it) | No — required dockershim (removed from Kubernetes 1.24+) |
| Scope | Image pull, lifecycle, execution — minimal but general-purpose | Same, but scoped exclusively to Kubernetes needs | Full platform: CLI, build tooling, networking stack, Compose/Swarm support |
| Typical use | Default runtime on most managed Kubernetes node images | Kubernetes nodes wanting minimal, K8s-only footprint | Local development, CI, standalone container workflows |
| Versioning | Independent release cycle | Released in lockstep with Kubernetes minor versions | Independent release cycle |
| Underlying OCI runtime | runc (default) or alternatives (gVisor, Kata) |
runc (default) or alternatives |
runc (via containerd internally) |
| Runs Docker-built images | Yes, natively (OCI-compliant) | Yes, natively (OCI-compliant) | Yes (native format) |
runc itself is the reference OCI runtime implementation — the low-level process that actually sets up namespaces, cgroups, and calls into the kernel to start the container process. containerd and CRI-O both shell out to runc (or a compatible alternative) as their default low-level executor; they are the higher-level daemons that manage images and container lifecycle around it.
Command / Configuration Reference
# Check which runtime a Kubernetes node is using
kubectl get nodes -o wide # look at the CONTAINER-RUNTIME column
# containerd CLI (crictl is the CRI-generic debugging tool, works with containerd and CRI-O)
crictl ps # list running containers via CRI
crictl images # list images known to the runtime
crictl inspect <container-id># inspect container details
# containerd's native (non-CRI) CLI
ctr images pull docker.io/library/nginx:latest # pull an image directly via containerd
ctr run docker.io/library/nginx:latest demo # run a container directly via containerd
# Podman — daemonless, rootless by default
podman run -d --name web nginx:latest # runs as a child process, no daemon involved
podman ps # list running containers
podman info --format '{{.Host.Security.Rootless}}' # confirm rootless mode
Common Pitfalls
- Pitfall: Assuming a Docker Engine → containerd migration on Kubernetes nodes requires re-building or converting images. Why: Confusing the runtime (execution engine) with the image format (OCI, which both runtimes consume identically). Fix: Verify the image is OCI-compliant (virtually all Docker-built images are) and treat the runtime swap as a node-level operational change with no image impact.
- Pitfall: Running local development containers under a root-owned daemon without evaluating rootless alternatives. Why: Docker Engine’s traditional daemon model defaults to root privilege, and teams inherit that default without reassessing it. Fix: Use Podman’s rootless mode, or configure Docker’s rootless mode, where a compromised container process shouldn’t translate into host-root compromise.
- Pitfall: Choosing a Kubernetes node runtime based on name recognition rather than footprint/scope needs. Why: containerd and CRI-O are frequently treated as interchangeable without examining project governance, versioning cadence, or footprint differences. Fix: Evaluate based on operational fit — CRI-O for K8s-only minimal footprint tied to K8s release cadence, containerd for broader general-purpose maturity and ecosystem tooling.
- Pitfall: Treating daemon vs. daemonless as a cosmetic implementation detail. Why: It understates the real difference in blast radius when a container-runtime component is compromised. Fix: Factor daemon privilege level and attack surface into runtime selection for security-sensitive environments.
Interview Questions
- Explain why containerd can run a Docker-built image without any conversion step. — Tests whether OCI’s role as the interoperability layer between build tool and runtime is understood, not just assumed to “just work.”
- Why did Kubernetes remove dockershim, and what had to be true architecturally for that removal to not break workloads? — Tests understanding of CRI as the decoupling layer and OCI as the format-compatibility guarantee.
- Why might a security-conscious team prefer Podman over Docker Engine for local development? — Tests whether the daemon/rootless security distinction is understood precisely, not cited as a buzzword.
- When would you choose CRI-O over containerd for a new Kubernetes cluster’s node runtime? — Tests whether the footprint/scope/versioning trade-off is treated as a real decision factor.
- What is the relationship between containerd, CRI-O, and runc? — Tests whether the candidate distinguishes the CRI-level daemon from the low-level OCI runtime executor.