Container Runtimes and the CRI
The kubelet does not run containers. It talks to containerd over a socket, containerd calls runc, and runc is the only thing here that touches a namespace. Four commands on the node show the whole chain.
Containers and Runtimes Guide 11 of 46 Beginner
- Kubernetes1.36.4
- containerdv2.2.6
- runc1.3.6
- crictlv1.36.0
- TimeAbout 16 min
- Reviewed22 August 2026
Written against the versions above. containerd 2.x moved its configuration layout. Paths below are 2.x; on 1.x the same settings live under different table names.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- SSH to a cluster node, with sudo. Everything here is below the API server.
- A running cluster, so there are containers to look at.
-
Three binaries, three jobs
"Kubernetes runs containers" is a useful simplification that stops being useful the moment something breaks. What actually happens is a chain, and each link is a separate program you can run yourself:
- kubelet decides *what* should run, from the API server.
- containerd manages images, snapshots and container lifecycle.
- runc creates the namespaces and cgroups, then execs your process. It runs once per container start and exits.
crictlis not part of the chain at all - it is a client that speaks the same CRI API the kubelet uses, which is exactly why it is the right debugging tool when the kubelet is the thing that is broken.bash Example session sudo -n crictl versionVersion: 0.1.0RuntimeName: containerdRuntimeVersion: v2.2.6RuntimeApiVersion: v1time="2026-08-21T21:01:34Z" level=warning msg="Config \"/etc/crictl.yaml\" does not exist, trying next: \"/usr/bin/crictl.yaml\""time="2026-08-21T21:01:34Z" level=warning msg="runtime connect using default endpoints: [unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock unix:///var/run/cri-dockerd.sock]. As the default settings are now deprecated, you should set the endpoint instead."which runc containerd crictl/usr/bin/runc/usr/bin/containerd/usr/bin/crictlrunc --versionrunc version 1.3.6commit: v1.3.6-0-g491b69baspec: 1.2.1go: go1.25.12libseccomp: 2.6.0Expected resultcontainerd as the runtime behind a CRI v1 API, and runc reporting an OCI runtime spec version. Those are two different standards meeting.
Success conditionAll three binaries resolve and report versions.
-
The socket is the interface
The kubelet finds the runtime through one setting, and it is a Unix socket path - not a hostname, not a port. That has a consequence worth remembering: the runtime must be on the same machine as the kubelet. There is no remote CRI.
The socket permissions are the other half.
srw-rw----owned byroot:rootmeans anything that can talk to this socket can start a privileged container on this node. That is why access to it is effectively root, and whycrictlneeds sudo.bash Example session sudo -n grep -E 'container-runtime-endpoint|containerRuntimeEndpoint' /var/lib/kubelet/kubeadm-flags.env /var/lib/kubelet/config.yaml 2>&1 | head -4/var/lib/kubelet/config.yaml:containerRuntimeEndpoint: unix:///run/containerd/containerd.sockls -l /run/containerd/containerd.socksrw-rw---- 1 root root 0 Aug 21 04:48 /run/containerd/containerd.sockExpected result
containerRuntimeEndpoint: unix:///run/containerd/containerd.sock, and a socket file with no group or world access.Success conditionYou can name the socket the kubelet uses and see its permissions.
-
crictl will guess, and say so
Run
crictlwith no configuration and it prints three warnings before every answer: no/etc/crictl.yaml, so it is guessing the endpoints. The output is still correct, which is why people live with it - but it buries real output in noise, and in a hurry that matters.One file fixes it permanently. Count the warnings before and after.
bash Example session sudo -n crictl ps 2>&1 | grep -c 'level=warning'3printf 'runtime-endpoint: unix:///run/containerd/containerd.sock\nimage-endpoint: unix:///run/containerd/containerd.sock\ntimeout: 10\n' | sudo -n tee /etc/crictl.yamlruntime-endpoint: unix:///run/containerd/containerd.sockimage-endpoint: unix:///run/containerd/containerd.socktimeout: 10sudo -n crictl ps 2>&1 | grep -c 'level=warning'3Expected resultThree warnings, then zero. The second count exits non-zero because grep found nothing to count - which is the point.
Success condition
crictl psprints only the table. -
What crictl sees that kubectl cannot
crictl podslists sandboxes, not Pods - the same objects, seen from below. Andcrictl pslists containers with their runtime IDs.This is the view that survives a broken control plane. If the API server is down,
kubectl get podshas nothing to ask, but the containers are still running andcrictlstill answers - because it is talking to the runtime on this machine, not to Kubernetes.That is the practical reason to know this layer exists at all.
bash Example session sudo -n crictl pods --namespace kube-system --no-trunc | head -6POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIMEf78b2e72fa90e716ed0f1a7e61da14177936709dcf77552ea1aa8e6c6d272ed5 16 hours ago Ready kube-proxy-mzbdk kube-system 0 (default)2e721d7a13ba458b2eb0dbd6bcc5ddc70b6465a29ad37b39c65a048d3735034a 16 hours ago Ready kube-controller-manager-cka1001 kube-system 0 (default)49a1e14c7e36286cc7186efef6e30378f556834723e5d367cfc059fb0890f897 16 hours ago Ready kube-scheduler-cka1001 kube-system 0 (default)b8d63bb2e933a3c0b5436c506e8d1f4aceb85ea8f60e39b6f53d0542d647205e 16 hours ago Ready kube-apiserver-cka1001 kube-system 0 (default)3cdd52e946aec207b59e4d64f75a8c56c3fc7cd673e4ab1112098e8989584af9 16 hours ago Ready etcd-cka1001 kube-system 0 (default)sudo -n crictl ps --name kube-apiserver -o tableCONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE75e2d1310f339 b0f70fa6ec47e 16 hours ago Running kube-apiserver 0 b8d63bb2e933a kube-apiserver-cka1001 kube-systemtime="2026-08-21T21:01:36Z" level=warning msg="Config \"/etc/crictl.yaml\" does not exist, trying next: \"/usr/bin/crictl.yaml\""time="2026-08-21T21:01:36Z" level=warning msg="runtime connect using default endpoints: [unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock unix:///var/run/cri-dockerd.sock]. As the default settings are now deprecated, you should set the endpoint instead."time="2026-08-21T21:01:36Z" level=warning msg="Image connect using default endpoints: [unix:///run/containerd/containerd.sock unix:///run/crio/crio.sock unix:///var/run/cri-dockerd.sock]. As the default settings are now deprecated, you should set the endpoint instead."Expected resultSandbox IDs and a running kube-apiserver container. The IDs are containerd's, not anything kubectl shows you.
Success conditionYou can list running containers without going through the API server.
Troubleshooting
crictl: command not found.Why: crictl ships separately from containerd and kubeadm does not always install it.
Fix:Download the release matching your Kubernetes minor version from
github.com/kubernetes-sigs/cri-tools/releasesand put it on PATH. Version skew is tolerated but a matching minor avoids surprises.connect: permission deniedon the socket.Why: The socket is root-owned with no group access, and you are not root.
Fix:Use
sudo crictl. Adding your user to a group that can reach the socket is granting node-level root, so do it deliberately if at all.crictl psis empty on a node with running Pods.Why: It is talking to a different runtime endpoint than the kubelet - the guess in the warnings picked the wrong socket.
Fix:Write
/etc/crictl.yamlwith the endpoint from/var/lib/kubelet/config.yaml, so both agree.