crictl
.mdVerified against crictl v1.36.0, flags verified via `crictl --help` and every listed subcommand's · official docs
What it is and where it fits 🎯#
crictl talks directly to a node's container runtime over the CRI (Container Runtime Interface) gRPC
socket — the same interface the kubelet itself uses — bypassing the kubelet and the Kubernetes API server
entirely. That makes it the tool for exactly one situation: something is wrong below Kubernetes' own
view of the world, and kubectl either can't see it or the API server isn't answering at all. kubectl describe pod tells you what Kubernetes thinks is happening to a Pod; crictl inspect tells you what the
runtime is actually doing with the containers behind it — the two can disagree, and when they do, crictl
is how you find out which one is lying. It's runtime-agnostic by design (works identically against
containerd, CRI-O, or any other CRI implementation) — install it alongside kubeadm/kubelet on any node
you'll ever need to debug directly.
Where crictl sits in the stack#
crictl and the kubelet are peers talking to the same socket, not a client-of-a-client — this is why crictl can see (and, dangerously, change) container state the kubelet doesn't know about yet, which is exactly the source of the biggest pitfall covered below.
Installation#
# Pin the crictl version to your cluster's Kubernetes MINOR version, not "latest" —
# cri-tools follows the k8s release cycle and mismatched minors can behave subtly differently.
VERSION="v1.36.0"
curl -LO "https://github.com/kubernetes-sigs/cri-tools/releases/download/${VERSION}/crictl-${VERSION}-linux-amd64.tar.gz"
sudo tar -C /usr/local/bin -xzf "crictl-${VERSION}-linux-amd64.tar.gz"
rm "crictl-${VERSION}-linux-amd64.tar.gz"
crictl --versionNote
There is no arm64/amd64-agnostic install script the way Trivy or cosign have — download the tarball matching the node's actual architecture from the cri-tools releases page.
Configuring the runtime endpoint#
crictl config --set runtime-endpoint=unix:///run/containerd/containerd.sock
crictl config --set image-endpoint=unix:///run/containerd/containerd.sock
crictl config --get runtime-endpoint
crictl config --list
crictl --runtime-endpoint unix:///run/containerd/containerd.sock ps # override per-invocation insteadReal captured output from this sandbox — no config file exists and no CRI socket is reachable, which is exactly the failure shape you'll see running crictl on a machine that either doesn't have containerd/CRI-O running or where you lack permission on the socket:
time="2026-08-29T03:20:09+03:00" level=warning msg="Config \"/etc/crictl.yaml\" does not exist, trying next: \"/home/msalah/.local/bin/crictl.yaml\""
time="2026-08-29T03:20:09+03:00" 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-29T03:20:09+03:00" level=error msg="validate service connection: validate CRI v1 runtime API for endpoint \"unix:///run/containerd/containerd.sock\": rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial unix /run/containerd/containerd.sock: connect: permission denied\""
time="2026-08-29T03:20:09+03:00" level=fatal msg="validate service connection: validate CRI v1 runtime API for endpoint \"unix:///var/run/cri-dockerd.sock\": rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial unix /var/run/cri-dockerd.sock: connect: no such file or directory\""
Important
crictl silently tries three default socket paths in order (containerd, then CRI-O, then cri-dockerd)
when no endpoint is configured — and that default-guessing behavior is itself deprecated. A future
crictl release will require an explicit endpoint. Set runtime-endpoint in /etc/crictl.yaml (or
$CONTAINER_RUNTIME_ENDPOINT) on every node rather than relying on the auto-detection, both to future-proof
the setup and because auto-detection silently picks the first matching socket, which is wrong on a host
running more than one candidate runtime.
Config file format#
# /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 10
debug: false
pull-image-on-create: falsetimeout is the connection timeout in seconds (default 2s) — worth raising on a node under heavy load
where the runtime's gRPC server is slow to respond, rather than crictl reporting a spurious connection
failure that's really just a timeout.
Listing pods, containers, and images#
crictl pods # every pod sandbox the runtime knows about
crictl ps # running containers only
crictl ps -a # every container, including exited/created ones
crictl ps --state Running --label app=catalog-api # filter by state and label, same idea as `kubectl get pods -l`
crictl images # every image cached on this node
crictl images --filter dangling=true # untagged/orphaned image layers
crictl stats # live CPU/memory per container, like `docker stats`crictl ps output columns (CONTAINER, IMAGE, CREATED, STATE, NAME, ATTEMPT, POD ID, POD)
map closely to what kubectl get pods -o wide shows for a Pod's containers, but sourced straight from the
runtime — useful specifically when the two disagree.
Illustrative crictl ps -a table shape (this node has no CRI runtime reachable, so this reflects the
documented column layout rather than a live capture — mark it as such if you paste it anywhere):
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD
a1b2c3d4e5f6a catalog-api:1.4.2 10 minutes ago Running catalog-api 0 9f8e7d6c5b4a3 catalog-api-7d9f8c6b5-x2k9p
b2c3d4e5f6a1b redis:7.2-alpine 2 hours ago Running cache 0 1a2b3c4d5e6f7 cache-0
Inspecting and debugging#
crictl inspect <container-id> # full container JSON: config, mounts, env, state
crictl inspectp <pod-id> # the pod sandbox's own network namespace/config
crictl logs <container-id> # like `kubectl logs`, but reads straight from the runtime's log path
crictl logs -f --tail 100 <container-id>
crictl exec -it <container-id> /bin/sh # like `kubectl exec`, no API server round-trip
crictl attach <container-id> # attach to PID 1's stdio instead of spawning a new process
crictl info # runtime version, config, and CNI plugin status for the whole nodeTip
crictl info is often the fastest way to confirm a CNI misconfiguration — its JSON output includes
the runtime's own view of network plugin readiness, which surfaces a broken CNI config before you'd ever
see it from a Pod stuck in ContainerCreating.
Managing images directly#
crictl pull myregistry.io/myapp:latest # pull without creating a container from it
crictl pull --creds myuser:mypass myregistry.io/myapp:latest # authenticated pull, credentials never touch a container's env
crictl rmi <image-id> # remove one image
crictl rmi --prune # remove every image not referenced by any container/podWarning
crictl rmi on a tag removes the whole image, every tag included, not just the one you named — this
is a real, documented CRI API limitation, not a crictl design choice: the CRI spec identifies images by
ID, and removing by tag resolves to that ID first. docker rmi/nerdctl rmi/ctr image rm all remove
only the specified tag; crictl rmi cannot replicate that distinction. If a node has myapp:v1 and
myapp:v2 both pointing at tags of the same underlying image and you only want v1 gone, use the
runtime's own native CLI (nerdctl, ctr, or crictl rmi --prune once nothing references the old tag
anymore) instead.
Low-level container lifecycle: create, run, and stop#
crictl runp pod-config.yaml # create a new pod sandbox (network namespace, etc.)
crictl create <pod-id> container-config.yaml pod-config.yaml # create a container inside an existing sandbox
crictl start <container-id> # start a created-but-not-started container
crictl run container-config.yaml pod-config.yaml # runp + create + start in one call — the closest thing to `docker run`
crictl stop <container-id>
crictl stopp <pod-id> # stop every container in a pod sandbox at once
crictl rm <container-id>
crictl rmp <pod-id>These commands take structured JSON/YAML config files (a PodSandboxConfig/ContainerConfig, the same
shapes the kubelet itself builds from a Pod spec), not simple image-name-and-flags the way docker run
does — crictl create <id> --help lists a jsonschema subcommand that prints the exact schema expected.
Reach for these only for genuinely low-level runtime testing/debugging (confirming the runtime itself can
start a container at all, independent of anything Kubernetes-shaped) — see the CAUTION above on why this
isn't a routine workload-management path.
Streaming events and forwarding a port for debugging#
crictl events # stream container/pod lifecycle events live, like `kubectl get events -w` but runtime-sourced
crictl port-forward <pod-id> 8080:80 # forward a local port into a pod sandbox, bypassing kube-apiserver entirelycrictl port-forward is the direct-to-node equivalent of kubectl port-forward — genuinely useful when the
API server itself is the thing that's down and kubectl port-forward can't establish its usual proxied
connection through it.
Real-world scenario: debugging when the API server is unreachable#
A control-plane outage means kubectl can't talk to anything, but workloads on healthy worker nodes are
still running — SSH onto the affected node and go straight to the runtime:
crictl ps -a # is the container actually still running, or did it crash silently?
crictl inspect <container-id> # exact exit code, OOM status, and restart count from the runtime's own record
crictl logs --tail 200 <container-id>This is the scenario crictl exists for — every one of these facts is normally surfaced through kubectl,
but only because the kubelet reports them up to an API server that, in this scenario, isn't there to
receive the report.
Real-world scenario: confirming what image tag actually got pulled#
A team suspects a :latest-tagged deployment is running stale code because the registry's :latest moved
but the node's local image cache didn't get refreshed:
crictl images --digests | grep myapp
crictl inspecti myapp:latest | grep -A2 '"repoDigests"'Comparing the repoDigests crictl reports against the digest currently pushed in the registry proves
definitively whether the node is running the image it thinks it's running — a kubectl describe pod alone
only shows the tag, which by definition can't prove what it currently points to.
Real-world scenario: recovering a node with a wedged container#
A container is stuck in Terminating for far longer than its terminationGracePeriodSeconds, and
kubectl delete pod --force isn't clearing it because the kubelet itself is the thing stuck waiting on the
runtime:
crictl ps -a --state Running | grep <pod-name>
crictl stop <container-id> # ask the runtime directly to stop it
crictl stop --timeout 0 <container-id> # skip graceful shutdown entirely if it's truly hung
crictl rm <container-id>Caution
Stopping or removing a container directly with crictl does not tell Kubernetes about it. The kubelet
reconciles actual runtime state against the desired Pod spec on its own schedule, but forcing state
changes underneath it — outside of a genuine "the kubelet itself is stuck" incident — can produce a
confusing window where kubectl get pods and crictl ps disagree about what's actually running. Treat
direct crictl stop/rm/create/run as a last-resort incident tool, not a routine way to manage
workloads; kubectl delete pod is the correct tool whenever the kubelet is healthy enough to act on it.
Shell completion#
source <(crictl completion bash) # bash, current session
crictl completion zsh > "${fpath[1]}/_crictl" # zsh, persistent
crictl completion fish | source # fishCommon pitfalls#
- Running crictl without root or the right group membership on the CRI socket — the real captured error
above (
connect: permission denied) is exactly this; the socket is root-owned by default on most distributions. - Trusting default endpoint auto-detection — see the IMPORTANT callout above; set
runtime-endpointexplicitly. - Mismatched crictl/Kubernetes minor versions — an old crictl against a newer CRI API (or vice versa) can silently miss fields the newer/older side added; pin crictl's version to the cluster's.
- Using
crictl rm/stopas routine Pod management — see the CAUTION above; it fights the kubelet's own reconciliation instead of working with it. - Forgetting
-aonps/podsand concluding a container "doesn't exist" when it's actually just not in theRunningstate crictl shows by default. - Expecting
crictl rmi <tag>to behave likedocker rmi <tag>— see the WARNING above; it removes the whole image, every tag, not just the one named.
Choosing a debugging tool at this layer#
| Tool | Use when... |
|---|---|
kubectl logs/exec/describe | The API server is healthy — always the default, RBAC-aware path |
crictl | The API server is down, or the kubelet's view of a container needs cross-checking against the runtime's own — works identically across containerd/CRI-O |
ctr | Containerd-specific low-level operations (namespaces, snapshotters) crictl doesn't expose — containerd-only |
nerdctl | A Docker-CLI-compatible experience specifically for containerd, for someone who wants docker-shaped ergonomics without CRI's structured-config model |
docker/podman | Only relevant if the node's actual runtime is one of these directly (rare on a modern kubeadm-built node, which defaults to containerd) |
Exit codes and when to reach for something else#
0 on success; non-zero on any RPC failure to the CRI socket (connection refused, permission denied,
timeout) or a not-found ID. The verbose error text (see the captured example above) almost always states
which of the three things went wrong — no socket, no permission, or no such runtime — directly in the
message.
Reach for kubectl logs/kubectl exec/kubectl describe first, always — they're the supported, RBAC-aware
path and work identically regardless of which CRI implementation a node runs. Drop down to crictl
specifically when the API server is unreachable, when you suspect the kubelet's view of a container has
drifted from reality, or when debugging the runtime/CNI layer itself rather than a specific workload.
kubeadm (this category's companion cheat sheet) is what installs the node in the first place; crictl is
what you reach for once something on that node needs a closer look.