CertGrid CertGrid
Hands-on Lab·Kubernetes and Cloud Native Associate

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

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.

Every command here runs on the node itself. This layer is below the API server, so kubectl cannot see it.
Server NameIP AddressOSRolesCPURAMHDD
CKA1001192.168.0.175Ubuntu 26.04 LTSControl Plane Node2 Core4 GB50 GB
CKA1001-NODE01192.168.0.176Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB

Before you start

  1. 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.

    crictl is 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.0

    Expected 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.

  2. 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 by root:root means 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 why crictl needs 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.sock

    Expected resultcontainerRuntimeEndpoint: 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.

  3. crictl will guess, and say so

    Run crictl with 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'3

    Expected resultThree warnings, then zero. The second count exits non-zero because grep found nothing to count - which is the point.

    Success conditioncrictl ps prints only the table.

  4. What crictl sees that kubectl cannot

    crictl pods lists sandboxes, not Pods - the same objects, seen from below. And crictl ps lists containers with their runtime IDs.

    This is the view that survives a broken control plane. If the API server is down, kubectl get pods has nothing to ask, but the containers are still running and crictl still 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

Official sources