LearnDevOps
DevOps06. Containers·EXPERT·7 min read

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 runc or 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.
🔒 Locked
Sign in and unlock the full syllabus to keep reading.
Sign in to continue
Knowledge check
5 scenario questions on this topic
Take the quiz →
Related in 06. Containers
17. Container Fundamentals
EXPERT
18. Docker
EXPERT
19. Image Optimization
EXPERT