Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

17.3 Container Runtimes and the CRI

⏱️ ~8 min read

TL;DR: Kubernetes doesn’t run containers itself — it delegates to a Container Runtime via the CRI (Container Runtime Interface). Docker, containerd, and CRI-O are all valid runtimes; K8s doesn’t care which one you use.


The Abstraction Layer

Early Kubernetes had Docker hardcoded into the kubelet. This was messy — every Docker release could break K8s. The solution was the Container Runtime Interface (CRI): a gRPC API that any runtime can implement.

graph TD
    K[Kubelet] -->|CRI gRPC| A[containerd]
    K -->|CRI gRPC| B[CRI-O]
    K -->|CRI gRPC| C[Other CRI Runtime]
    A -->|OCI| D[runc]
    B -->|OCI| D
    
    style K fill:#3b82f6,color:#fff
    style D fill:#10b981,color:#fff
LayerWhat it isExamples
CRIThe API standardgRPC interface (ImageService + RuntimeService)
High-level runtimeImplements CRIcontainerd, CRI-O
Low-level runtimeActually creates containersrunc, crun, kata-containers
OCI specContainer format standardWhat runc implements

What Happened to Docker?

Kubernetes removed the Dockershim in v1.24. Docker is no longer a supported runtime for K8s worker nodes.

This confused many people — let’s be clear about what changed and what didn’t:

Before v1.24After v1.24
K8s worker runtimeDocker (via dockershim)containerd or CRI-O
Your Docker imagesStill workStill work
docker buildWorksWorks
docker pushWorksWorks
Docker as the kubelet runtimeSupported❌ Removed

💡 Tip: Your Dockerfile and docker build workflows are completely unaffected. The change only affects what runs inside the Kubernetes node. Images built with Docker follow the OCI spec and run fine under containerd.


What containerd Does

containerd is the runtime used by Minikube, most managed K8s services (EKS, GKE, AKS), and the majority of self-managed clusters.

The kubelet calls containerd via CRI to:

1. Pull image         → ImageService.PullImage()
2. Create sandbox     → RuntimeService.RunPodSandbox()
3. Create container   → RuntimeService.CreateContainer()
4. Start container    → RuntimeService.StartContainer()
5. Execute in container → RuntimeService.ExecSync()
6. Stop container     → RuntimeService.StopContainer()
7. Remove container   → RuntimeService.RemoveContainer()

Try It — Inspect containerd Directly

# SSH into the minikube node
minikube ssh

# List all containerd containers (ctr is the containerd CLI)
sudo ctr -n k8s.io containers list | head -20

Expected output:

CONTAINER                                                           IMAGE                                          RUNTIME
0a3b4f5c6d...   registry.k8s.io/pause:3.9                         io.containerd.runc.v2
1b2c3d4e5f...   docker.io/library/nginx:latest                     io.containerd.runc.v2
...
# List running tasks (actual processes)
sudo ctr -n k8s.io tasks list | head -10

The OCI Standard

The Open Container Initiative (OCI) defines two specs:

  • Image Spec — how container images are structured (layers, manifests)
  • Runtime Spec — how to run a container from an image (config.json)

runc is the reference implementation of the OCI Runtime Spec. It’s what actually calls clone(), chroot(), and cgroup syscalls to create an isolated process.

graph LR
    A[Pod Spec] -->|kubelet| B[containerd]
    B -->|OCI image| C[Image Layers]
    B -->|OCI runtime spec| D[runc]
    D -->|Linux syscalls| E[Container Process]
    
    style E fill:#10b981,color:#fff

What runc Actually Does

When runc starts a container, it:

  1. Creates a new network namespace (or joins the pod’s existing one)
  2. Creates a new PID namespace
  3. Creates a new mount namespace (the container filesystem)
  4. Sets up cgroups for resource limits (CPU, memory)
  5. Applies seccomp and AppArmor profiles
  6. Runs the container’s entrypoint process as PID 1 in that namespace

Alternative Runtimes

RuntimeDescriptionUse Case
containerdIndustry standard, used everywhereDefault for most clusters
CRI-OLightweight, designed for K8s onlyRed Hat / OpenShift clusters
kata-containersVMs as containers (stronger isolation)Multi-tenant, untrusted workloads
gVisorUserspace kernel (Google’s runsc)Extra sandboxing for security

🏭 In Production: Almost everyone uses containerd. CRI-O is popular in OpenShift environments. kata-containers and gVisor are used by cloud providers for their “serverless container” products (AWS Fargate, Google Cloud Run).


Inspecting the Runtime from Kubernetes

You don’t need to SSH into nodes. crictl is a CRI-compatible CLI available on the node:

minikube ssh

# crictl talks directly to the CRI socket
sudo crictl ps | head -10

Expected output:

CONTAINER    IMAGE                     CREATED       STATE    NAME
a3b4c5d6e   nginx@sha256:abc...       2 hours ago   Running  nginx
f1e2d3c4b   pause@sha256:123...       2 hours ago   Running  POD
...
# Inspect a container
sudo crictl inspect <container-id>

# Get container logs
sudo crictl logs <container-id>

# List images
sudo crictl images

Key Takeaways

#ConceptOne-liner
1CRI decouples kubelet from runtimeK8s defines the API; runtimes implement it
2Docker images still workThe image format (OCI) is unchanged; only the node runtime changed
3containerd is the defaultEKS, GKE, AKS, Minikube all use it
4runc does the real workcontainerd calls runc which calls Linux kernel APIs
5crictl is your node-level kubectlUse it to debug container issues the kubelet can’t explain

✅ Quick Check

Q1: Your team just upgraded from Kubernetes v1.23 to v1.25. The upgrade guide says you must migrate from Docker to containerd. Does this break your existing container images?

Answer No. Container images follow the OCI Image Spec regardless of which tool built them. Docker builds OCI-compliant images. containerd runs OCI-compliant images. Your images will run identically under containerd. Only the node-level runtime changes; your image workflow and Dockerfiles remain the same.

Q2: A pod is in ContainerCreating state for 10 minutes. The kubelet events show “failed to pull image.” Which component is actually failing — the CRI or runc?

Answer The CRI (containerd/CRI-O) — specifically its ImageService. Image pulling happens before runc is involved. runc only gets called once the image is present locally. Check for network issues, authentication problems with the registry, or an incorrect image name/tag.

Q3: You need to run untrusted, multi-tenant workloads on a shared K8s cluster. Standard containers won’t cut it from a security standpoint. What runtime would you investigate?

Answer kata-containers or gVisor (runsc). Both provide stronger isolation — kata-containers runs each pod in a lightweight VM, while gVisor intercepts syscalls via a user-space kernel. Both implement the OCI Runtime Spec so they're compatible with containerd as a shim.