containerd isn’t just a container runtime; it’s the engine that actually makes containers do their thing on your nodes, and Kubernetes orchestrates that engine.

Here’s a quick look at a pod being scheduled and running:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80

When you kubectl apply -f deployment.yaml:

  1. API Server: Receives the request, validates it, and stores the Deployment object in etcd.
  2. Controller Manager: Sees the new Deployment object and creates a ReplicaSet.
  3. Scheduler: Sees the ReplicaSet and creates a Pod object. It then selects a node for the pod based on resource requests, affinity rules, etc., and updates the Pod object with the chosen node name.
  4. Kubelet (on the chosen node): Polls the API server for pods assigned to its node. It sees the new nginx-deployment pod.
  5. Kubelet to containerd: The Kubelet doesn’t pull images or start containers itself. It communicates with containerd, typically via its gRPC API, telling it to pull the nginx:latest image and then create and start a container from that image, with the specified configuration.
  6. containerd: Pulls the nginx:latest image from the registry (e.g., Docker Hub). It then uses low-level containerization technologies (like runc, a low-level OCI runtime) to create and start the container. containerd manages the container’s lifecycle (start, stop, pause, delete) and reports its status back to the Kubelet.
  7. Kubelet to API Server: Kubelet continuously updates the Pod object’s status in the API server, reflecting that the container is running.

The core problem containerd solves is abstracting away the complexities of the Open Container Initiative (OCI) runtime specification. Kubernetes needs a standardized way to talk to any OCI-compliant runtime, and containerd provides that stable, well-defined API. It handles image management (pulling, storing, cleaning up) and container lifecycle management independently of Kubernetes. This separation of concerns is crucial: Kubernetes focuses on orchestration (what pods should run where), while containerd focuses on execution (making those containers run on the node).

Think of containerd as the daemon that runs on each node, listening for instructions. It doesn’t care why a container needs to run, only that it’s been told to run it and how. It manages the low-level details of interacting with the OS kernel (namespaces, cgroups) and the actual container runtime (like runc) to create isolated environments.

The relationship is built on the Container Runtime Interface (CRI). Kubelet implements the CRI, and containerd implements the CRI server. They communicate over a gRPC connection. Kubelet sends CRI requests like RunPodSandbox and CreateContainer, and containerd executes them. containerd also exposes its own lower-level API, which Kubelet uses.

You can see containerd in action by looking at its logs or by inspecting the containers it manages directly.

On a node with containerd running, you can check its status:

sudo systemctl status containerd

To see the images containerd has pulled:

sudo ctr --namespace k8s.io images list

And to see the containers it’s currently running (these correspond to your pods):

sudo ctr --namespace k8s.io containers list

The k8s.io namespace is where Kubernetes typically tells containerd to operate. Each container listed here will have an ID that you can cross-reference with Kubelet’s internal state or pod status.

The nginx:latest image will be pulled and stored on the node. containerd will then create a container using runc (or another OCI runtime) based on that image, setting up the necessary namespaces and cgroups. Kubelet receives confirmation that the container is running and updates the pod’s status.

When you scale down the deployment, Kubelet tells containerd to stop and remove the container, and containerd cleans up the associated resources. Image garbage collection is also handled by containerd, often triggered by Kubelet requests or based on its own policies to free up disk space.

What most people don’t realize is that containerd is also responsible for managing the pod sandbox. A pod sandbox is essentially a network namespace and a set of mounts that all containers within a pod share. When Kubelet asks containerd to run a pod, it first asks it to create a sandbox, and then it asks it to create containers within that sandbox. This ensures that containers within the same pod share network isolation and other resources correctly, as defined by Kubernetes.

The next concept you’ll likely encounter is how containerd manages storage for container images and volumes, and the intricacies of different storage drivers like overlayfs or zfs.

Want structured learning?

Take the full Containerd course →