Search across all documentation pages
docker ps shows nothing on a worker.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.
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.
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.
"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.
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.
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).
Reviewed by Chris St. John·Last updated Jul 19, 2026