containerd vs Docker: How the Container Stack Fits Together
Docker Engine is built on containerd, which is built on runc — and Kubernetes now talks to containerd or CRI-O directly. What each layer does, why Kubernetes removed dockershim, namespaces in containerd, nerdctl and ctr, image stores and snapshotters, and which one you actually need.
"Docker vs containerd" sounds like a choice between competitors, but containerd is inside Docker. The container world is a stack of layers, and most confusion comes from mixing them up.
The stack
Developer tools: docker CLI · docker compose · buildx
│
Docker Engine (dockerd): API, networking, volumes, builds, Swarm
│
containerd: images, snapshots, container lifecycle, CRI plugin
│
containerd-shim: one per container, keeps it running independently
│
OCI runtime (runc): creates namespaces/cgroups, execs the process
- runc starts containers from an OCI bundle and exits. (runc and OCI runtimes)
- containerd is a daemon that manages the whole container lifecycle on a host: pulling and storing images, unpacking them into snapshots (filesystems), creating containers via runc, supervising them through shims, and exposing a gRPC API. It's a CNCF graduated project, originally extracted from Docker.
- Docker Engine adds the developer-facing features on top: the familiar API and CLI,
docker build(BuildKit), Compose, user-defined networks with DNS, volumes and port publishing. (Docker vs Docker Compose)
Why Kubernetes dropped Docker
Kubernetes talks to runtimes through the Container Runtime Interface (CRI). Docker Engine never implemented CRI, so Kubernetes maintained an adapter, dockershim, which translated CRI calls to Docker's API — which then called containerd anyway.
Kubernetes removed dockershim in v1.24 (2022). Clusters now talk CRI directly to:
- containerd (with its built-in CRI plugin), or
- CRI-O — a runtime built specifically for Kubernetes by Red Hat and others.
Nothing changed for images: images built with Docker are standard OCI images and run fine. (Do you need Kubernetes?)
Using containerd directly
containerd has its own minimal debug CLI, ctr, and a Docker-compatible one, nerdctl:
sudo nerdctl run -d -p 8080:80 nginx # Docker-like UX on raw containerd
sudo ctr images ls
sudo ctr -n k8s.io containers ls # Kubernetes' containers
containerd uses namespaces (an API concept, not Linux namespaces) to separate clients: Docker uses moby, Kubernetes uses k8s.io. That's why docker ps doesn't show Kubernetes pods on the same node, and vice versa.
crictl is the Kubernetes-oriented CLI that speaks CRI to whichever runtime is configured.
Images and snapshotters
containerd stores image content by digest and unpacks layers through snapshotters: overlayfs (default on Linux), native, btrfs, zfs, and lazy-pulling snapshotters like stargz/SOCI that start containers before the whole image downloads. (Docker image layers and OverlayFS)
Docker historically had its own image store (graph drivers) on top. Newer Docker Engine versions can use containerd's image store directly, which brings multi-platform images and lazy-pulling snapshotters to Docker — and newer releases enable it by default on fresh installs.
Runtimes are pluggable
Because containerd delegates to an OCI runtime via shims, you can swap runc for crun, gVisor (runsc) or Kata per container or per Kubernetes RuntimeClass — the isolation level changes without changing images or tooling. (Kata Containers, Firecracker vs gVisor vs containers)
Podman: the daemonless alternative
Podman (Red Hat) offers a Docker-compatible CLI without a central daemon: each container is a child of the podman process tree (via conmon), rootless by default, using crun or runc. It doesn't use containerd. podman compose and a Docker-compatible socket cover most Docker workflows. (Rootless containers)
Which do you need?
| You are… | Use |
|---|---|
| A developer building and running containers locally | Docker (or Podman) |
| Running a few containers on a server | Docker + Compose, or Podman |
| Operating Kubernetes | containerd or CRI-O (the distribution usually decides) |
| Building a platform that manages containers programmatically | containerd's API (embedding) |
| Wanting rootless, daemonless | Podman |
The images are interchangeable across all of them; the choice is about workflow and operations, not lock-in.
Debugging across layers
When something's wrong, find which layer owns it:
- Networking/port publishing → Docker Engine (or CNI plugins in Kubernetes). (Container networking internals)
- Image pull/unpack errors → containerd (logs:
journalctl -u containerd). - "Failed to create shim task" / OCI errors → runtime and kernel features (cgroups, seccomp, AppArmor).
EasySpawn runs your containers on your own server's VM, with the runtime stack set up and maintained for you — no shim debugging required. See how it works or join the waitlist.
Related: runc and OCI Runtimes · What Is Docker? · Docker Image Layers and OverlayFS · Do You Need Kubernetes?
Keep reading
runc and OCI Runtimes: What Actually Starts Your Container
Below Docker and containerd sits a small program that creates the namespaces, cgroups and mounts and execs your process: the OCI runtime. The OCI image, runtime and distribution specs, a bundle and config.json, running runc by hand, alternatives (crun, youki, gVisor's runsc, Kata), and runc's notable escapes.
PID 1 in Containers: Signals, Zombies, and Why Your Container Won't Stop
Inside a container your app is PID 1, and PID 1 is special: the kernel won't apply default signal handlers to it and it must reap orphaned children. Why docker stop takes 10 seconds, why shell-form CMD swallows SIGTERM, zombies, and the fixes: exec form, tini/--init, signal handling.