containerd, the underlying container runtime for Docker, is actually a more streamlined and modular system that focuses purely on container execution, leaving higher-level orchestration and management to other components.

Let’s see it in action. Imagine you want to pull an image and run a simple container.

# Pull an image using containerd's CLI tool, nerdctl
sudo nerdctl pull alpine:latest

# Run a container
sudo nerdctl run -it --rm alpine sh

Inside the container, you can run commands:

/ # ls
bin etc lib root tmp usr
/ # hostname
a1b2c3d4e5f6
/ # exit

Now, let’s break down how this works under the hood, and why this separation matters.

The core problem containerd solves is efficiently and securely managing the lifecycle of containers. Before containerd, Docker handled everything: image building, pushing/pulling, network management, storage, and container execution. This monolithic approach meant that any bug or performance issue in one area could impact the entire system. containerd, on the other hand, is designed as a daemon that manages container execution, image transfer, and storage. It exposes a gRPC API for clients (like Docker itself, or nerdctl) to interact with.

Here’s the breakdown of the main components and their roles:

  • containerd daemon: This is the heart of the operation. It runs as a background service, listening on a Unix socket (typically /run/containerd/containerd.sock). Its primary job is to manage the container lifecycle: starting, stopping, pausing, and resuming containers. It also handles image management (pulling, pushing, storing images) and provides an interface for storage drivers.

  • runc: This is the low-level container runtime. containerd doesn’t directly interact with the kernel’s cgroups and namespaces. Instead, it uses runc (a CNCF graduated project) to create and run containers according to the Open Container Initiative (OCI) runtime specification. runc takes a container configuration (generated by containerd) and the container’s root filesystem, then uses Linux kernel features to isolate the container.

  • Image Service: containerd has its own image service responsible for downloading images from registries, verifying their integrity, and storing them locally. It uses a content-addressable storage mechanism, meaning images are stored based on their content hash, which aids in deduplication and efficient sharing of layers.

  • Task Service: This service is responsible for managing the execution of containers. When you start a container, containerd’s task service instructs runc to create and start the container process. It also handles signals, I/O redirection, and other aspects of the container’s runtime.

  • CRI (Container Runtime Interface) Plugin: For Kubernetes integration, containerd implements the Kubernetes CRI. This plugin translates Kubernetes’ requests (like "create pod") into containerd’s native API calls, allowing Kubernetes to manage containers running on containerd.

The real magic happens in how containerd orchestrates runc. When a client requests a container to be run, containerd:

  1. Retrieves the container’s root filesystem and image layers from its image store.
  2. Generates a config.json file for runc, specifying the container’s namespaces, cgroups, mounts, and the entrypoint command, all conforming to the OCI specification.
  3. Invokes runc create to set up the container’s namespaces and cgroups.
  4. Invokes runc start to execute the container’s main process.

The most surprising part is how containerd itself doesn’t directly manage networking or user-level orchestration. It relies on external components. For instance, when Docker uses containerd, Docker’s daemon is the client that talks to containerd’s API. Docker’s networking plugins and its own orchestration logic are separate from containerd. Similarly, when using nerdctl, you’re interacting directly with containerd, but nerdctl provides some of the higher-level commands that Docker used to offer. This modularity means you can swap out parts of the stack. For example, you could run containerd and use a different network plugin or even a custom orchestrator that talks to its gRPC API.

This separation of concerns is why you’ll often see containerd used in environments where a leaner, more integrated runtime is desired, especially within Kubernetes clusters where the Kubernetes control plane handles the higher-level orchestration.

The next step you’ll likely encounter is understanding how these container images are built and managed, and the role of tools like BuildKit in that process.

Want structured learning?

Take the full Containerd course →