skip to content

containerd can be run on its own without Docker Engine. What is containerd responsible for by itself, and what is the Container Runtime Interface (CRI) that lets an orchestrator's node agent drive it directly?

level: seniorimportance: should knowfreq 42%

answer

  1. containerd = content + snapshots + tasks + shims, over gRPC
  2. no networks/volumes/compose — those are Docker Engine
  3. CRI = RuntimeService + ImageService, sandbox-first
  4. dockershim removed in Kubernetes 1.24
  5. ctr -n moby vs crictl / nerdctl -n k8s.io

basics

~20 s

On its own, containerd handles image pull and storage, snapshots/rootfs preparation, container and task lifecycle via shims, and low-level networking hooks — exposed over a gRPC API on a Unix socket. CRI is a standard gRPC API (image and runtime services) that containerd implements as a plugin, so a node agent can drive it without Docker Engine.

solid answer

~50 s

**containerd standalone** provides: a content store and image pull/push over the distribution API; a **snapshotter** that turns layers into a rootfs; **container** and **task** lifecycle through per-container shims and an OCI runtime; namespaces for multi-tenancy (`moby`, `k8s.io`); events, metrics and leases. It has no opinion about Docker networks, volumes, `compose` or restart policies — those are Docker Engine concepts. Its clients are `ctr` (low-level, debugging), `nerdctl` (Docker-compatible CLI) and `crictl`. **CRI** is a gRPC contract with two services — **RuntimeService** (sandboxes/pods, containers, exec, logs) and **ImageService** (pull, list, remove). containerd implements it as a built-in plugin listening on `/run/containerd/containerd.sock`; CRI-O is an alternative implementation. Because Docker Engine never spoke CRI, the kubelet used a translation shim, which was removed in Kubernetes 1.24 — nodes now talk to containerd or CRI-O directly. Same OCI images, same runc underneath; only the layer that was Docker Engine is gone.

code

bash · 7 lines
bash
ctr -n moby containers ls
ctr images pull docker.io/library/alpine:3.20
ctr run --rm -t docker.io/library/alpine:3.20 demo sh

nerdctl -n k8s.io ps
crictl ps -a
crictl images

go deeper

for a junior

Know that containerd is the component underneath Docker that actually manages images and containers, and that it can be used without Docker.

for a middle

List containerd's responsibilities versus Docker Engine's, and explain CRI as the standard API a node agent uses to drive a runtime.

for a senior

Discuss the practical migration surface: crictl/nerdctl instead of docker on nodes, the image-store gotcha, docker.sock mounts breaking, runtime handlers for alternative runtimes.

for a principal

Argue the layering — CRI, OCI image, OCI runtime as three independent seams — and what standardizing on containerd across a fleet buys in upgrade independence and runtime choice.

## containerd as a product in its own right containerd started as the piece Docker split out of its daemon, and became a CNCF-graduated project used directly by many systems. It is a daemon exposing a **gRPC API** over a Unix socket (`/run/containerd/containerd.sock`), and it deliberately implements the *generic* half of container management: - **Content store** — digest-addressed blobs: manifests, configs, layers, pulled over the registry distribution API with credential handling and mirrors. - **Snapshotter** — the pluggable component that turns image layers into a mountable rootfs. `overlayfs` is the common one; others include `native`, `btrfs`, `zfs`, `devmapper`, and lazy-pulling snapshotters like `stargz`. - **Containers and tasks** — a *container* is the metadata plus its OCI spec; a *task* is a running instance of it. Tasks are created via a per-container **shim** which invokes an OCI runtime (`runc` by default). Multiple **runtime handlers** can be registered, so `runsc` or Kata can be selected per container. - **Namespaces** — containerd's own tenancy separation of metadata. Docker's containers live in the `moby` namespace, Kubernetes' in `k8s.io`. This is why `ctr containers ls` looks empty until you pass `-n moby`. - **Leases, events, metrics** — GC protection for content, an event stream, and Prometheus metrics. What it deliberately does **not** own: user-facing networks and iptables plumbing, named volumes, restart policies, `docker compose`, image building, or the friendly CLI model. Those are Docker Engine features layered above. Clients you should know: `ctr` (bundled, low-level, explicitly "for debugging, unstable UX"), `nerdctl` (a Docker-compatible CLI over containerd, including compose support and rootless mode), and `crictl` (a CRI-level debugging CLI). ## What CRI is An orchestrator's node agent needs a stable, runtime-agnostic way to say "run this pod". CRI is that contract: a **gRPC API definition** with two services. - **RuntimeService** — `RunPodSandbox`/`StopPodSandbox`/`RemovePodSandbox`, `CreateContainer`, `StartContainer`, `StopContainer`, `ExecSync`/`Exec`/`Attach`, `PortForward`, `ContainerStatus`, `ListContainers`, `ContainerStats`. - **ImageService** — `PullImage`, `ListImages`, `ImageStatus`, `RemoveImage`, `ImageFsInfo`. Note the **sandbox** concept: CRI is built around a group of containers sharing namespaces, with a sandbox created first and containers added into it. That is the pod model expressed at the runtime boundary; the orchestration semantics above it belong to the orchestrator, not here. containerd implements CRI as a built-in plugin, so no extra process is needed. CRI-O is a purpose-built alternative implementing the same API and also driving `runc`. Both consume standard OCI images and both delegate to an OCI runtime — so the layer being swapped is *management*, not image format or execution semantics. ## The Docker Engine question Docker Engine predates CRI and never spoke it. Kubernetes bridged the gap with an in-kubelet translation component (the "dockershim"), which was deprecated in Kubernetes 1.20 and **removed in 1.24**. The practical consequences are frequently misunderstood, so state them precisely: - Nodes now talk to containerd or CRI-O directly. Docker Engine, if installed at all, is just a developer convenience on that host. - **Images are unaffected.** They are OCI/Docker-format images either way; "Docker images" continue to work everywhere. - **Images you build locally with Docker are not automatically visible** to a containerd-backed node, because Docker's image store and containerd's `k8s.io` namespace are different stores. That is the real day-to-day gotcha. - Node debugging commands change: `docker ps` on a node becomes `crictl ps` or `nerdctl -n k8s.io ps`. - Anything that mounted `/var/run/docker.sock` into a pod (older CI-in-cluster builders, some log agents) breaks and needs a different approach. ## Why this design is right The stack has three clean seams: **CRI** between the node agent and the container manager; the **OCI image-spec/distribution-spec** for artifacts; the **OCI runtime-spec** between the manager and the executor. Each seam lets one component be replaced without touching the others — swap CRI-O for containerd, swap runc for gVisor, swap registries — while the image you built this morning keeps running unchanged. containerd's ability to serve both Docker Engine and a CRI client simultaneously, in different metadata namespaces on the same host, is the clearest demonstration that the seam is real.

  • After a node moved from Docker Engine to containerd, an image built locally with `docker build` on that node no longer starts there. Why?
    Docker Engine and the CRI client use different image stores — or at minimum different containerd metadata namespaces (`moby` versus `k8s.io`). A locally built Docker image lands in Docker's store and is invisible to the CRI namespace, so the runtime tries to pull it from a registry instead. The fixes are to push to a registry, or to build/import directly into the right namespace with `nerdctl -n k8s.io build` or `ctr -n k8s.io images import`.
  • Does removing Docker Engine from a node mean "Docker images" no longer work?
    No, and this is the most common misconception about the dockershim removal. Images are OCI artifacts; the Docker v2 manifest format is understood by containerd and CRI-O alike. What changed is which daemon the node agent talks to, not the image format. The same image you push today runs identically under containerd, CRI-O or Docker Engine.
  • How would you debug a container on a node that has no Docker Engine installed?
    Use `crictl` for CRI-level views — `crictl ps`, `crictl logs`, `crictl exec`, `crictl inspect` — since it speaks the same API the node agent does. For a Docker-like experience use `nerdctl -n k8s.io`, which wraps containerd with familiar commands. `ctr -n k8s.io` is the lowest-level option and is best reserved for inspecting containerd's own state.

saying these in an interview costs you the question

  • Saying Kubernetes "dropped support for Docker images" — only the Docker Engine shim was removed, image formats are unchanged.
  • Thinking CRI is an OCI specification; it is a Kubernetes-defined gRPC API, separate from the OCI specs.
  • Believing containerd includes Docker's networks, volumes and compose features.
  • Expecting `ctr containers ls` to show anything without specifying the namespace.
  • Assuming locally built Docker images are automatically usable by a containerd-backed node.

context