Container Runtime (CRI)
Container Runtime Interface: containerd, CRI-O & the Runtime Landscape
TL;DR
- CRI: gRPC API kubelet uses to talk to container runtimes (containerd, CRI-O, etc.) — decouples K8s from any single runtime.
- Dockershim removal (K8s 1.24): kubelet talks directly to containerd via CRI instead of Docker Engine. Docker images still work (OCI format).
- Common runtimes: containerd (most common, what Docker uses internally), CRI-O (minimal K8s-specific), gVisor/Kata (sandboxed).
- RuntimeClass: selects alternative runtimes per-pod (e.g., gVisor for untrusted workloads).
- crictl: low-level debugging tool to interact directly with CRI runtime on nodes.
Core Concepts
CRI is gRPC interface kubelet uses to any conformant runtime; implementations vary (containerd vs CRI-O), but interface is identical.
1. Why CRI Exists
Before CRI (pre-1.5), the kubelet had Docker-specific code hardwired in — adding support for any other runtime meant patching kubelet itself. CRI abstracted this into a generic gRPC interface (RuntimeService + ImageService), so kubelet talks to any CRI-compliant runtime identically:
kubelet ──gRPC (CRI)──▶ containerd / CRI-O / any CRI-compliant runtime
│
▼
OCI Runtime (runc, gVisor, Kata)
│
▼
Actual container process
2. What “Docker Support Was Removed” Actually Meant (v1.24+)
Docker Engine itself was never CRI-compliant — Kubernetes used a shim called dockershim (built into kubelet) to translate CRI calls into Docker Engine API calls. Maintaining this shim inside kubelet was a growing burden, and since Docker Engine internally already uses containerd anyway, Kubernetes removed dockershim in 1.24 and clusters moved to talk to containerd directly via native CRI — skipping the Docker Engine layer entirely.
Critical clarification for interviews: images built by docker build are still perfectly compatible and runnable — they’re standard OCI images, and OCI compliance was never Docker-specific. What changed is purely the runtime path kubelet uses to run them, not the image format itself. cri-dockerd exists as a community-maintained standalone shim for anyone who genuinely needs to keep using Docker Engine as the runtime.
3. Runtime Options Compared
| Runtime | Notes |
|---|---|
| containerd | Most widely used today; graduated CNCF project; what Docker Engine itself uses internally |
| CRI-O | Purpose-built minimal runtime specifically for Kubernetes, no extra Docker-compatible API surface |
| cri-dockerd | Community shim for teams that specifically require Docker Engine underneath |
| gVisor / Kata Containers | Sandboxed/VM-isolated runtimes (via RuntimeClass) for stronger workload isolation than standard runc |
# Selecting an alternative runtime per-pod via RuntimeClass
apiVersion: v1
kind: Pod
metadata:
name: sandboxed-workload
spec:
runtimeClassName: gvisor
containers:
- name: untrusted-app
image: myorg/app:1.0
4. Hands-on Command Cheat Sheet
Check which container runtime a node is using:
kubectl get node worker-1 -o jsonpath='{.status.nodeInfo.containerRuntimeVersion}'
Interact with the runtime directly via crictl (works identically regardless of underlying CRI implementation):
crictl ps -a
crictl images
crictl logs <container-id>
crictl inspect <container-id>
Check kubelet’s configured CRI socket:
cat /var/lib/kubelet/kubeadm-flags.env | grep container-runtime-endpoint
List available RuntimeClasses:
kubectl get runtimeclass
5. Architectural Interview Q&A
Q: A junior engineer says “our images won’t run anymore since we removed Docker support.” What’s actually true here?
A: This conflates two separate things: the container image format (OCI-compliant, produced identically by docker build, buildah, kaniko, or any other builder) versus the runtime that executes containers (which changed from talking to Docker Engine via dockershim to talking to containerd directly via native CRI). Any image built with docker build remains a completely standard OCI image and runs identically under containerd — nothing about image compatibility changed at all; only kubelet’s internal communication path to actually run that image changed.
Q: Why would a team deliberately choose CRI-O over containerd, given containerd is more widely adopted? A: CRI-O was purpose-built from day one exclusively for Kubernetes, with a minimal surface area and release cadence tightly aligned to Kubernetes versions themselves (a new CRI-O version ships alongside each Kubernetes minor release) — teams wanting the smallest possible attack surface and the tightest guaranteed compatibility with a specific Kubernetes version sometimes prefer this over containerd’s broader, more general-purpose scope (which also serves non-Kubernetes use cases like standalone Docker Engine and other container platforms).
6. Common Pitfalls
[!WARNING] Assuming crictl and docker Commands Are Interchangeable
crictlintentionally has a different command surface than thedockerCLI (nocrictl run, nocrictl build— it’s a low-level debugging tool for the CRI runtime, not a full container management CLI) and its output/behavior can differ subtly across different CRI implementations (containerd vs CRI-O). Engineers who reflexively reach for Docker CLI muscle memory (docker exec,docker cp) often get confused whencrictldoesn’t offer equivalent conveniences — for interactive debugging inside a running container,kubectl execis almost always the right tool, withcrictl/ctrreserved for genuine runtime-level troubleshooting on the node itself.