What Actually Runs Your Containers
Summary
- A container runtime is the software that takes an image plus a config and turns it into a running, isolated Linux process.
- Insight: "Docker" is a developer toolkit, not the thing that runs your pods. On a modern Kubernetes node, containerd talks to runc, and Docker is nowhere in the path.
- Key Concepts: high-level runtime (containerd, CRI-O) manages images and container lifecycle; low-level runtime (runc, crun) creates the actual isolated process; CRI is the gRPC contract between the kubelet and the high-level runtime; OCI standardizes image and runtime formats.
- When to Use: Understand this stack whenever you debug node-level issues, evaluate sandboxing, or explain why
docker psshows nothing on a worker. - Limitations/Trade-offs: More layers means more moving parts to learn, but each layer is independently swappable and standardized.
- Related Topics: containerd and crictl, CRI-O, runc and low-level OCI.
Foundations
A container is not a special object in the kernel. It is a normal process wrapped in namespaces, cgroups, and a filesystem view.
The job of "running a container" is really: unpack an image, set up isolation, and start a process inside it.
Historically Docker did all of this in one monolithic daemon. Over time the community split that work into standardized, replaceable layers.
Two standards anchor the ecosystem. The OCI (Open Container Initiative) defines the image format and the runtime spec. The CRI (Container Runtime Interface) defines how Kubernetes asks a runtime to do work.
Think of it as a stack. At the top is Kubernetes. At the bottom is the Linux kernel. Everything in between is pluggable.
Mechanics & Interactions
Walk the stack top to bottom for a single pod on a node.
The kubelet decides a pod should run. It does not create containers itself.
Instead the kubelet calls the CRI - a gRPC API - against whatever high-level runtime the node was configured with. On most clusters that is containerd.
# On a node: the kubelet's runtime endpoint (containerd)
crictl --runtime-endpoint unix:///run/containerd/containerd.sock psThe high-level runtime pulls and unpacks the image, sets up the container's root filesystem, and prepares the OCI runtime config.
It then invokes a low-level OCI runtime - usually runc - to actually create the process.
runc reads the OCI runtime spec (config.json), sets up namespaces and cgroups, applies seccomp and capabilities, pivots into the new root, and execs your entrypoint.
After the container starts, runc exits. A small shim process (containerd-shim-runc-v2) stays behind to keep the container's stdio and report its exit status.
kubelet --CRI--> containerd --OCI--> runc --> [namespaces + cgroups] --> your processThe pod sandbox matters too. Before your app container starts, the runtime creates a pause container that holds the pod's network and IPC namespaces so all containers in the pod share them.
Nowhere in this path is the Docker daemon involved. That is deliberate.
Advanced Considerations & Applications
Kubernetes used to talk to Docker through a compatibility layer called dockershim. It was removed in Kubernetes 1.24 and is long gone in 1.36.
Removing dockershim changed nothing for most users, because containerd was already doing the real work underneath Docker anyway.
The split lets you swap layers. You can replace runc with crun (a faster C implementation), gVisor (runsc, a userspace kernel), or Kata Containers (lightweight VMs) without changing Kubernetes.
This is configured through RuntimeClass, which points a pod at a named handler on the node.
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runscapiVersion: v1
kind: Pod
metadata:
name: sandboxed
spec:
runtimeClassName: gvisor
containers:
- name: app
image: registry.example.com/app@sha256:abc123...For images, the same OCI standard means an image built by BuildKit runs unchanged under containerd or CRI-O. Build tooling and runtime are decoupled.
That is why "build with Docker, run with containerd" is not a contradiction. Docker builds an OCI image; containerd runs it.
Common Misconceptions
"Kubernetes runs Docker containers." It runs OCI containers via containerd or CRI-O. The image format is shared, so the distinction is invisible day to day.
"Removing dockershim broke my images." Images are OCI artifacts and were never affected. Only the deprecated in-cluster Docker integration was removed.
"containerd is a fork of Docker." containerd was extracted from Docker and donated to the CNCF. Docker Engine itself uses containerd internally.
"runc is the container." runc is a short-lived binary that creates the container and exits. The long-lived process is your app, supervised by the shim.
"I need Docker installed on worker nodes." You do not, and you should not. Nodes need a CRI runtime; installing Docker adds a redundant daemon and attack surface.
FAQs
Why did Kubernetes drop Docker support? The dockershim adapter was extra maintenance for the project to keep Docker speaking CRI. containerd speaks CRI natively, so the adapter was removed.
What is the difference between containerd and runc? containerd is the high-level runtime that manages images and lifecycle. runc is the low-level tool that creates the isolated process. containerd calls runc.
Is CRI-O a replacement for containerd? Yes - it is an alternative CRI runtime, purpose-built for Kubernetes, and the default on OpenShift. You pick one per node.
Can I still use Docker on my laptop? Absolutely. Docker Desktop and Docker Engine remain the standard local build and dev experience. The change was only about in-cluster runtimes.
How do I see containers on a node without Docker? Use crictl (CRI-level) or ctr (containerd-native). kubectl remains your primary tool from outside the node.
Does BuildKit produce different images for containerd? No. BuildKit outputs standard OCI images that any compliant runtime can run.
Related
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).