Kubernetes Container Runtimes¶
What You'll Learn¶
- The path from a Pod spec to a running process on a node
- What the Container Runtime Interface (CRI) is, and which runtimes implement it
- What the removal of dockershim actually changed — and what it didn't
- How to inspect containers on a Kubernetes node with
crictl - How RuntimeClass selects gVisor or Kata Containers for specific workloads
From Pod Spec to Process¶
flowchart TB
API[kube-apiserver] -->|Pod assigned to node| K[kubelet]
K -->|CRI gRPC| C["CRI runtime:<br>containerd (CRI plugin) or CRI-O"]
C -->|pull via OCI Distribution| R[(Registry)]
C --> S["Pod sandbox<br>(pause container: network namespace)"]
C --> O["OCI runtime: runc / crun"]
O --> P1[app container]
O --> P2[sidecar container]
S -.shared namespaces.- P1
S -.shared namespaces.- P2
- The scheduler assigns a Pod to a node; the kubelet on that node sees it.
- The kubelet calls the runtime over the CRI, a gRPC API: create a pod sandbox, pull images, create and start containers.
- The runtime creates the sandbox — a tiny
pausecontainer holding the Pod's network namespace — then starts each container joined to it. That's why containers in a Pod sharelocalhost. - An OCI runtime (
runcorcrun) creates each container process, exactly as in Architecture.
The CRI Runtimes¶
| Runtime | Notes |
|---|---|
| containerd | The most common default (EKS, GKE, AKS, kubeadm, kind, k3s). CRI support is a built-in plugin |
| CRI-O | Built specifically for Kubernetes; the runtime in OpenShift |
| cri-dockerd | An adapter that lets the kubelet use Docker Engine — only for environments that truly need Docker on nodes |
The dockershim Story¶
Early Kubernetes talked to Docker Engine directly through built-in code called dockershim. Docker Engine doesn't implement the CRI, so dockershim translated. Kubernetes removed dockershim in v1.24.
What that changed:
- The kubelet no longer talks to Docker Engine unless you install
cri-dockerd. docker pson a node shows nothing — the containers are managed by containerd or CRI-O.- Tools that mounted
/var/run/docker.sockinside Pods (for builds or log collection) stopped working on those nodes.
What it did not change:
- Images built with Docker run exactly as before. They're OCI images; containerd and CRI-O pull and run them.
- Developers can keep using Docker locally to build, test, and push.
The confusion came from mixing up Docker Engine (a daemon and API) with the image format (a standard).
Inspecting a Node With crictl¶
crictl speaks CRI directly, so it works with any CRI runtime:
# On a node (for kind: docker exec -it kind-control-plane bash)
crictl info | head -20
crictl pods # pod sandboxes
crictl ps # running containers
crictl ps -a --name coredns # include exited
crictl images
crictl logs <container-id>
crictl inspect <container-id> | jq '.info.pid'
crictl pull docker.io/library/nginx:stable
| Docker habit | On a Kubernetes node |
|---|---|
docker ps |
crictl ps |
docker images |
crictl images |
docker logs |
crictl logs (or kubectl logs) |
docker exec -it |
crictl exec -it (or kubectl exec / kubectl debug) |
docker inspect |
crictl inspect |
containerd keeps Kubernetes containers in the k8s.io namespace, separate from Docker's moby namespace:
Prefer kubectl for day-to-day work; use crictl when the kubelet or API server can't give you answers — for example a node that's NotReady. See Cluster and Node Problems.
Node Runtime Configuration¶
Registry mirrors and private CA trust are configured on the runtime, not in Pods:
server = "https://registry-1.docker.io"
[host."https://mirror.internal.example.com"]
capabilities = ["pull", "resolve"]
Pull credentials for private registries come from imagePullSecrets or the node's cloud identity (for example, an EKS node role reading ECR).
RuntimeClass: Stronger Isolation per Workload¶
Standard containers share the node's kernel. For untrusted or multi-tenant code, a sandboxed runtime adds a boundary:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc # must match a runtime handler configured in containerd
---
apiVersion: v1
kind: Pod
metadata:
name: untrusted-job
spec:
runtimeClassName: gvisor
containers:
- name: job
image: registry.example.com/tenant-a/job:2.1.0
| Handler | Isolation | Cost |
|---|---|---|
runc (default) |
Namespaces + cgroups + seccomp, shared kernel | Lowest overhead |
runsc (gVisor) |
System calls handled by a user-space kernel | Some syscall-heavy workloads slow down |
kata (Kata Containers) |
Each Pod in a lightweight VM with its own kernel | More memory per Pod, slower start |
Building Images in Clusters Without Docker¶
With no Docker socket on nodes, in-cluster builds use daemonless builders: BuildKit (rootless), Buildah, or Kaniko-style tools running as ordinary Pods. Better still, build in CI and deploy the pushed digest — see CI/CD Pipelines for Kubernetes.
Common Mistakes¶
- Believing Docker-built images stopped working after Kubernetes 1.24.
- Running
docker pson a node to debug Pods and concluding nothing is running. - Mounting
/var/run/docker.sockinto Pods for builds — it doesn't exist on containerd nodes, and where it does, it's root on the node. - Using
ctrto delete containers the kubelet manages. - Choosing gVisor or Kata cluster-wide without measuring the performance impact.
Check Your Understanding¶
- Which component on a node talks to the container runtime, and over what API?
- What's the purpose of the
pausecontainer in a Pod? - Why do images built with Docker still run on a containerd-only cluster?
- When would you set
runtimeClassNameon a Pod?
Next¶
Continue to macOS and Apple Container Technology.