Container Runtimes in Kubernetes: containerd vs CRI-O (Illustrated)


🧩 Introduction

Containers are executed on nodes by container runtimes. This guide explains how containerd and CRI-O implement the Container Runtime Interface (CRI) and how each runtime interacts with kubelet, the OCI runtime (runc), Linux kernel resources (namespaces, cgroups), storage drivers, and CNI plugins. Diagrams below are from official project sources (Kubernetes, containerd, CRI-O / GitHub) to ensure accuracy.


⚙️ The Container Runtime Interface (CRI)

Kubernetes Container Runtime Interface diagram
CRI - Container runtime interface diagram.

Flow summary: kubelet ⇄ CRI (gRPC socket) ⇄ runtime (containerd / CRI-O) ⇄ OCI runtime (runc) ⇄ kernel namespaces & cgroups.


🐳 containerd

How containerd integrates with kubelet and the node:

  1. kubelet issues CRI calls (RunPodSandbox, CreateContainer, StartContainer) over the containerd socket (commonly /run/containerd/containerd.sock).
  2. containerd (CRI plugin) handles image pulls, snapshotting, namespace setup, and delegates container execution to an OCI runtime (runc or alternative).
  3. containerd-shim isolates container lifecycles so containerd can exit without leaving orphaned processes; it also proxies stdio and exit codes back to containerd.
  4. runc performs the final clone()/execve() syscalls to start the container in new namespaces and cgroups.
  5. Networking is handled by CNI plugins; containerd requests the CNI to set up pod network interfaces during sandbox creation.

Key files & sockets:

  • containerd daemon: /usr/bin/containerd (systemd service containerd)
  • primary socket: /run/containerd/containerd.sock
  • CLI tools: ctr, crictl, nerdctl

Common verification commands:

# Check containerd systemd service
sudo systemctl status containerd

# Check containerd socket & version
sudo ss -l | grep containerd
sudo ctr version
sudo crictl info

🦠 CRI-O — Official Architecture (CRI-O project’s documentation / GitHub)

CRI-O architecture - official Official CRI-O architecture diagram from the CRI-O repository showing conmon, crio daemon, runc, and interactions with kubelet.

How CRI-O integrates with kubelet and the node:

  1. kubelet calls CRI methods to create sandboxes and containers using CRI-O’s socket (/var/run/crio/crio.sock).
  2. CRI-O uses containers/image to pull images and containers/storage to manage layers and snapshots.
  3. During container start, CRI-O launches conmon — a small monitor process that reaps processes, captures logs, and proxies stdio between the container and CRI-O.
  4. CRI-O then delegates execution to runc (or other OCI runtime). conmon helps decouple the container lifecycle from the parent process.
  5. Networking is configured via CNI during sandbox creation; CRI-O will invoke CNI plugins similarly to containerd’s flow.

Key files & sockets:

  • CRI-O daemon binary: /usr/bin/crio (systemd service crio)
  • primary socket: /var/run/crio/crio.sock
  • CLI tools: crictl, podman (for local debugging), runc

Common verification commands:

# Check CRI-O systemd service
sudo systemctl status crio

# Verify socket & crictl info
sudo ss -l | grep crio
sudo crictl info

🔀 Detailed Data Flow (Both Runtimes)

  1. API / Controller flow: User applies YAML → kubectl → API server → Controller records desired state in etcd.
  2. Scheduling: Scheduler assigns Pod to node.
  3. Node actions: kubelet gets Pod spec → kubelet calls CRI RunPodSandbox on runtime socket (containerd or crio).
  4. Runtime actions: Runtime pulls images, sets up root filesystem using snapshotter, creates network namespace via CNI, spawns container via runc.
  5. Runtime ↔ kubelet: Runtime updates container status via CRI; kubelet reports Pod readiness to API server.

Diagrammatic summary: The CRI socket is the single integration point. The runtime then orchestrates image/content, snapshot, shim, OCI runtime, and kernel primitives (namespaces, cgroups).


🧰 Installation & Configuration (Canonical steps)

containerd (Ubuntu example)

# Install containerd (apt repos or package)
sudo apt update
sudo apt install -y containerd

# Configure systemd cgroups for Kubernetes
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

# Start & enable
sudo systemctl enable --now containerd

# Verify
sudo systemctl status containerd
sudo ctr version
sudo crictl info

CRI-O (Ubuntu example)

# Fetch CRI-O repos (adjust OS and VERSION to match k8s)
OS=xUbuntu_22.04
VERSION=1.30
curl -fsSL https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/Release.key | sudo gpg --dearmor -o /usr/share/keyrings/libcontainers-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/libcontainers-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /" | sudo tee /etc/apt/sources.list.d/libcontainers.list
# CRI-O repo
curl -fsSL https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$VERSION/$OS/Release.key | sudo gpg --dearmor -o /usr/share/keyrings/libcontainers-crio-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/libcontainers-crio-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$VERSION/$OS/ /" | sudo tee /etc/apt/sources.list.d/crio.list

sudo apt update
sudo apt install -y cri-o cri-o-runc
sudo systemctl enable --now crio
sudo systemctl status crio
sudo crictl info

🛠️ Troubleshooting & Verification Commands

# Which runtime does kubelet see?
kubectl get nodes -o jsonpath='{.items[*].status.nodeInfo.containerRuntimeVersion}'

# Ensure kubelet points to correct runtime socket (kubeadm env)
cat /var/lib/kubelet/kubeadm-flags.env | grep container-runtime-endpoint || true

# Restart services if socket mismatch found
sudo systemctl restart containerd
sudo systemctl restart crio
sudo systemctl restart kubelet

# Use crictl to inspect runtime
sudo crictl ps -a
sudo crictl images
sudo crictl inspect <container-id>

⚖️ Decision Guide & Recommendations

  • Use containerd for general clusters, cloud-managed clusters, or Docker-compatible workflows. It has broad ecosystem support and tooling.
  • Use CRI-O for security-focused, Red Hat/OpenShift ecosystems, or when minimal runtime surface area is desired.
  • If any official image link breaks, I can host local copies of the SVG/PNG inside your repo (recommended for long-term stability).

🧩 References