Also at Deasil Works · txn2 · Plexara
Profiles GitHub · X · LinkedIn
Theme Light · Auto · Dark
Professional notes by Craig Johnston
long-form, short-form, working drafts · since 2008
VOL. XIX · MMXXVI
129 NOTES IN PRINT
FOLIO CXXIX 2026-07-23 · 7 MIN · SHORT-FORM

containerd Without Docker: the Runtime Under Your Cluster

The container runtime the kubelet actually uses, and how to configure and debug it now that the Docker shim is gone

Diagram · folio cxxix
flowchart TB
  KUBELET["kubelet"] -->|CRI| CD["containerd<br>CRI plugin"]
  CD --> SHIM["shim per pod"]
  SHIM --> RUNC["runc"]
  RUNC --> CTR["containers"]
  CD --> IMG[("image store<br>OCI layers")]

When you built the cluster, the container runtime under the kubelet was containerd, with Docker nowhere in sight. That trips people up, because for a decade “containers” and “Docker” were the same word. They are not, and on a Kubernetes node in 2026 Docker has no role at all. This post is the deeper look at the runtime your whole platform actually runs on: what containerd is, how to configure it correctly, and how to debug a node now that docker ps is gone.

This series rebuilds my 2020 Apress book, Advanced Platform Development with Kubernetes, for 2026. The approach behind it comes from building and running data platforms in production for more than twenty years.

§Docker Was Never the Runtime

The 2020 book installed Docker on every node, and that was how everyone did it, so it is worth being clear about what changed and why it is not a loss. The kubelet never spoke to Docker directly. It speaks the Container Runtime Interface, a small gRPC contract, and for years a piece of glue called the dockershim translated CRI calls into Docker API calls. Docker itself, underneath its UI and its build features, was already using containerd to actually run containers. So the path was kubelet, to dockershim, to Docker, to containerd, to your container, with two of those hops adding overhead and a maintenance burden.

Kubernetes removed the dockershim in version 1.24, in 2022, and the path became what it should have been all along: kubelet, to containerd, to your container. Nothing you care about was lost. Your images are OCI images, the same standard Docker produces, and they run identically. What went away was a translation layer and a daemon that a production node never needed. Docker remains a fine tool on a developer’s laptop for building and running containers locally; it is simply not what runs them in your cluster.

§What containerd Actually Is

containerd is a CNCF graduated project and an industrial-strength container runtime, and understanding its parts is what lets you reason about a node when something is wrong. It pulls and stores images, manages their layers, and hands the actual running of a container to a lower-level runtime. The CRI plugin, built into containerd, is the piece the kubelet talks to. When a pod starts, containerd launches a small shim process for it and uses runc, the reference OCI runtime, to set up the namespaces, cgroups, and mounts that make a container a container. The shim stays alive alongside the container so containerd can be restarted without killing your workloads, an advantage over the old daemon model.

Everything here is built on open specifications. The image is an OCI image, the runtime behavior is the OCI runtime spec, and the kubelet contract is CRI. That standardization is why the runtime is swappable, containerd and CRI-O both implement CRI, and why nothing about your workloads is tied to a particular vendor’s container engine. You are running on commodity, interchangeable parts at the runtime layer, the same principle as everywhere else in this platform.

§Configure It Right

The cluster build set up containerd with a default config and two critical edits. They are worth understanding rather than copying, because they are the difference between a node that works and one that fails in confusing ways.

containerd config default > /etc/containerd/config.toml

The first edit is the cgroup driver. The kubelet and containerd both manage resources through the kernel’s cgroup hierarchy, and on a systemd host they must both use the systemd driver, or they fight over accounting and pods restart unpredictably.

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
  SystemdCgroup = true

The second is the sandbox image, the tiny “pause” container that holds a pod’s network namespace open. containerd ships with a default pin, and kubeadm has its own expectation; when they disagree, pods stall. Set it to exactly what your Kubernetes version wants.

[plugins."io.containerd.grpc.v1.cri"]
  sandbox_image = "registry.k8s.io/pause:3.10"

Those two settings cause most first-run failures, and both fail in ways that do not name themselves, which is why they are worth knowing in advance.

§Private Registries and Mirrors

A real platform pulls images from places besides the public registries: the Forgejo registry you stood up, a private mirror, a pull-through cache to survive a public-registry outage or rate limit. containerd configures these through a hosts directory rather than one giant config block, which keeps each registry’s settings in its own file.

# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry]
  config_path = "/etc/containerd/certs.d"

Then a hosts.toml per registry sets a mirror or authentication. A pull-through cache for Docker Hub, for instance, so your nodes are not at the mercy of its rate limits, lives in /etc/containerd/certs.d/docker.io/hosts.toml.

server = "https://registry-1.docker.io"

[host."https://mirror.apk8s.dev"]
  capabilities = ["pull", "resolve"]

This is the kind of node-level control a managed runtime hides from you. Owning the runtime means you can point your cluster at your own mirror, cache aggressively, and stop depending on a public registry’s availability for your deploys to work.

§Debugging Without Docker

The real worry people have about losing Docker is “how do I poke at a node now.” The answer is two tools, and they are better suited to the job than docker was. crictl talks to the CRI directly and speaks in Kubernetes terms, pods and containers as the kubelet sees them. nerdctl is a Docker-compatible CLI for containerd, so the commands you already know work, just pointed at the runtime the cluster actually uses.

# what the kubelet sees, in Kubernetes terms
crictl pods
crictl ps
crictl logs <container-id>

# Docker-style commands against containerd
nerdctl --namespace k8s.io ps
nerdctl --namespace k8s.io images

The k8s.io namespace matters: containerd partitions its containers and images into namespaces, and the kubelet’s live in k8s.io, so that is where you look when debugging the cluster’s workloads. For the lowest level there is also ctr, containerd’s own client, though crictl and nerdctl cover nearly everything you will actually do. The commands you already know mostly carry over; only the binary changed.

§CRI-O, the Alternative

containerd is not the only CRI runtime. CRI-O is the other mature choice, built by Red Hat specifically and only to run Kubernetes containers, with no ambitions beyond that. It is the default on OpenShift and a perfectly good pick; it is lighter in scope because it does one job. I use containerd because it is the broadest default and the most widely deployed, but the point worth taking is that the runtime is a replaceable part. You could swap one for the other and your workloads would not notice, because both implement the same CRI and run the same OCI images. That interchangeability keeps you from being locked to a single runtime.

§Why Owning the Runtime Matters

It would be easy to treat the container runtime as plumbing you never think about, and on a managed control plane you have no choice, it is fenced off, and a runtime problem becomes a support ticket. On a cluster you own, the runtime is in reach. You can read its config, point it at your own registry mirror, inspect exactly what is running on a node, and adjust how it talks to the kernel. This is the same argument that runs through the whole series: the more of the machine you can see, the more an agent operating the platform can actually fix, rather than escalate. On a cluster you own, the runtime is part of that.

§What You Have

A clear picture of the runtime your platform runs on: containerd under the kubelet, runc under that, OCI images and the CRI contract holding it together, configured with the cgroup driver and sandbox image that keep a node healthy, pointed at your own registry mirror, and debuggable with crictl and nerdctl now that Docker is off the node. None of it is exotic, and all of it is yours to control.

Next I go up a layer to the network that carries traffic between all these containers, Cilium, and the eBPF data plane that replaced a stack of older pieces with one.

← back to all notes