Introduction
Container runtimes are the software that unpack container images, create namespaces and cgroups, and start the processes inside a pod. In Kubernetes, the kubelet calls a runtime through the Container Runtime Interface (CRI). The runtime choice affects security isolation, startup latency, image compatibility, and operational failure modes.
This guide goes beyond the basics. We cover how to verify your runtime configuration, tune performance safely, diagnose common failures, and choose between containerd, CRI-O, and gVisor for specific workloads. Every section includes copy-paste commands, expected output, and recovery steps.
Before changing anything, run the read-only checks below and record the current state. This gives you a rollback point and makes failures visible.
Runtime Architecture and the CRI
The kubelet speaks gRPC to a CRI plugin. containerd supports CRI via a built-in plugin; CRI-O implements CRI natively. Both manage OCI-compliant runtimes like runc (default) or Kata Containers.
The CRI defines two services:
- RuntimeService: manages pod sandboxes and containers (create, start, stop, exec, etc.)
- ImageService: pulls, lists, and removes images
When you create a pod, the kubelet calls RunPodSandbox first. This creates the network namespace, sets up the pause container, and applies pod-level cgroups. Then it creates each container inside that sandbox.
To see which runtime your node uses:
kubectl get nodes -o wide
# The CONTAINER-RUNTIME column shows the container runtime and version.
Expected output (example):
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
node-1 Ready control-plane 10d v1.31.0 192.168.1.10 <none> Ubuntu 22.04.3 LTS 5.15.0-91-generic containerd://1.7.2
On the node itself, check the CRI socket:
sudo crictl info
crictl is a CLI for CRI runtimes. It shows runtime name, version, and configuration. If crictl is not installed, install it from the Kubernetes release assets or your package manager.
Version and Environment Inventory
Before diagnosing any runtime issue, collect precise versions and configurations. Run these commands on a node (via SSH or a debug pod) and save the output:
# Runtime version
sudo crictl version
# Example output:
# Version: 0.1.0
# RuntimeName: containerd
# RuntimeVersion: 1.7.2
# RuntimeApiVersion: v1
# Kernel and OS
uname -a
cat /etc/os-release
# Check if cgroups v2 is enabled
stat -fc %T /sys/fs/cgroup
# If output is "cgroup2fs", you are on cgroups v2. If "tmpfs", cgroups v1.
# List running containers with their runtimes
sudo crictl ps -a
For containerd, inspect the default runtime:
sudo containerd config default | grep -A10 'plugins."io.containerd.grpc.v1.cri".containerd.runtimes'
This shows the runtime handlers. In a typical cluster, you'll see runc as the default. If you have gVisor installed, it appears as an additional handler.
Why this matters: Many mysterious failures come from a mismatch between the runtime's expected cgroup driver and the kubelet's cgroup driver. For example, if the kubelet uses systemd but containerd uses cgroupfs, pods may be killed incorrectly or fail to start. Confirm both settings:
# On the node
sudo cat /var/lib/kubelet/config.yaml | grep cgroupDriver
# Expected often: cgroupDriver: systemd
Reconcile with the runtime config. If they differ, decide which to use (usually systemd on modern distributions) and update both, then restart services carefully.
Diagnosing Runtime Problems
Start with read-only observation. Use kubectl to get pod status and events:
kubectl get pods -n <namespace>
kubectl describe pod <pod-name> -n <namespace>
Look for events like FailedCreatePodSandBox, Failed to start container, or CreateContainerConfigError. These often point to runtime issues.
If a container keeps crashing, check its logs:
kubectl logs <pod-name> -n <namespace> --previous
If the container never starts because the runtime cannot pull the image, you'll see ImagePullBackOff or ErrImagePull. Verify the image name and registry access.
For node-level issues, inspect container states with crictl:
# List pods (sandboxes) on this node
sudo crictl pods
# List containers
sudo crictl ps -a
# Inspect a stopped container
sudo crictl inspect <container-id>
The inspect output includes the OCI runtime spec, which reveals resource limits, mounts, and environment variables. It also includes the last error if the container failed to start.
To test the runtime directly, try running a simple container with crictl:
# Pull a small image
sudo crictl pull busybox:latest
# Create a sandbox
POD_ID=$(sudo crictl runp --runtime runc <(echo '{"metadata":{"name":"test-pod","namespace":"default"}}'))
# Create and start a container in that sandbox
CONTAINER_ID=$(sudo crictl create $POD_ID <(echo '{"metadata":{"name":"test-container"},"image":{"image":"busybox"},"command":["sleep","300"]}') <(echo '{}'))
sudo crictl start $CONTAINER_ID
# Verify it is running
sudo crictl ps
# Clean up
sudo crictl stop $CONTAINER_ID
sudo crictl rm $CONTAINER_ID
sudo crictl stopp $POD_ID
sudo crictl rmp $POD_ID
If this fails, the runtime itself has a configuration or installation problem.
Safe Configuration Changes
When you need to change runtime settings, follow these principles:
- Back up the config file before editing.
- Change one parameter at a time.
- Test on a single node or a small subset using node taints and tolerations.
- Document the expected outcome and how to roll back.
Example: Switching the default runtime from runc to crun (a faster OCI runtime)
crun is a lightweight OCI runtime written in C, often faster than runc for high-density workloads. To test it on one node:
- Install crun:
sudo apt-get install crun # or equivalent for your OS
- Add a runtime handler to containerd. Edit
/etc/containerd/config.toml:
version = 2
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.crun.options]
BinaryName = "/usr/bin/crun"
- Restart containerd:
sudo systemctl restart containerd
- Verify crun is available:
sudo crictl info | jq .config.containerd.runtimes
You should see crun listed.
- Create a pod that uses the new runtime by setting
runtimeClassNamein the pod spec. First, define a RuntimeClass:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: crun
handler: crun
Apply it, then in a pod spec:
spec:
runtimeClassName: crun
containers:
- name: demo
image: busybox
command: ["sleep", "3600"]
Deploy and verify the pod runs. On the node, sudo crictl inspect <container-id> should show the runtime as crun.
Using gVisor for Strong Isolation
gVisor (runsc) is a user-space kernel that intercepts system calls, providing a sandbox. To add gVisor as an alternative runtime:
- Download and install runsc:
(
set -e
ARCH=$(uname -m)
URL=https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH}
wget ${URL}/runsc ${URL}/runsc.sha512
sha512sum -c runsc.sha512
rm -f runsc.sha512
sudo mv runsc /usr/local/bin
sudo chown root:root /usr/local/bin/runsc
sudo chmod 0755 /usr/local/bin/runsc
)
- Configure containerd to use it:
sudo containerd config default > /etc/containerd/config.toml
# Edit the file to add under [plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1"
- Restart containerd.
- Create a RuntimeClass:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
- Use it for pods that need isolation.
Performance note: gVisor adds overhead because it intercepts every syscall. It is not for high-throughput workloads. Test before rolling out.
Performance Tuning and Observability
Runtime performance hinges on image pull speed, storage driver, and CPU/memory allocation. Here are concrete checks and optimizations.
Image Pull Acceleration
Use crictl to pull an image and time it:
time sudo crictl pull nginx:latest
If pulls are slow, consider:
- Registry mirroring: For containerd, add a registry mirror in
/etc/containerd/certs.d/docker.io/hosts.toml:
server = "https://docker.io"
[host."https://registry-mirror.example.com"]
capabilities = ["pull", "resolve"]
- Pre-pulling images on new nodes using a DaemonSet or node initialization script.
- Using a local cache like Dragonfly or Kraken.
Storage Driver
Check the storage driver for containerd:
sudo ctr version
sudo ctr plugins ls | grep snapshot
The default is overlayfs. It is usually the best choice. Avoid devicemapper in production due to stability issues.
Resource Limits and Runtime Overhead
The runtime itself consumes some CPU and memory for every container. For high-density nodes, reduce overhead by using a lighter runtime like crun (as shown earlier). Also ensure the kubelet's --system-reserved and --kube-reserved flags reserve resources for system daemons, including the runtime.
Example kubelet flags in /var/lib/kubelet/config.yaml:
systemReserved:
cpu: 500m
memory: 1Gi
kubeReserved:
cpu: 250m
memory: 512Mi
Adjust based on your node size and runtime.
Monitoring Runtime Metrics
containerd exposes Prometheus metrics on /v1/metrics if enabled. To enable, edit config.toml:
[metrics]
address = "127.0.0.1:1338"
grpc_histogram = true
Restart containerd, then verify:
curl http://127.0.0.1:1338/v1/metrics | grep containerd_runtime
Import these metrics into your monitoring stack. Key metrics: containerd_runtime_metrics_containers (number of containers), containerd_runtime_metrics_cpu_usage_nanoseconds, containerd_runtime_metrics_memory_usage_bytes.
Failure Modes and Recovery
Runtime Crashes or Unresponsive
If crictl ps hangs or returns an error, the runtime daemon may be down. Check:
sudo systemctl status containerd # or crio
sudo journalctl -u containerd -n 100
Look for OOM kills, segfaults, or disk issues. Restart the daemon if needed:
sudo systemctl restart containerd
After restart, verify pods recover:
kubectl get pods --all-namespaces --field-selector=status.phase!=Running
Some pods may be in Error or CrashLoopBackOff. Deleting them will force recreation.
Node Not Ready
If a node goes NotReady, check the kubelet and runtime:
kubectl describe node <node> | grep -A10 Conditions
The condition ContainerRuntimeIsHealthy should be True. If False, investigate the runtime.
Image Pull Failures
If pods cannot pull images, test manually:
sudo crictl pull <image>
If it fails, check registry connectivity and authentication. For private registries, ensure the node has the correct image pull secrets. Example pod spec:
spec:
imagePullSecrets:
- name: my-registry-secret
containers:
- name: app
image: private-registry.example.com/app:v1
Sandbox Creation Failures
If pod sandbox creation fails, check CNI plugins:
ls /opt/cni/bin
Missing plugins cause FailedCreatePodSandBox. Install the required CNI plugins or repair the network configuration.
Rollback Strategy
Always keep a backup of runtime configuration files. For containerd:
sudo cp /etc/containerd/config.toml /etc/containerd/config.toml.bak-$(date +%Y%m%d)
If a change breaks nodes, restore the backup and restart the daemon. For kubelet config, back up /var/lib/kubelet/config.yaml.
Document rollback steps in your runbooks. Test a rollback on a single node before applying changes cluster-wide.
Security Hardening
Runtime Sandboxing
For untrusted workloads, use gVisor or Kata Containers. gVisor is easier to integrate; Kata provides hardware virtualization-backed isolation but requires more setup. Choose based on your threat model and performance needs.
Restricting Runtime Privileges
Even with runc, you can harden containers:
- Set
allowPrivilegeEscalation: falsein pod security contexts. - Use
seccompprofiles. The default profile blocks many dangerous syscalls. For runtime-level seccomp, configure in the runtime handler:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
NoNewPrivileges = true
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SeccompProfile = "/path/to/seccomp.json"
- Drop capabilities:
securityContext:
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
Keeping Runtimes Updated
Runtime vulnerabilities are frequent. Track CVEs for containerd, CRI-O, and runc. Automate updates using your OS package manager and test in staging. Example for Ubuntu:
sudo apt-get update && sudo apt-get upgrade containerd.io
Restart the daemon after upgrade and verify cluster health.
Common Pitfalls
- Configuring cgroup driver incorrectly
- Why it happens: Linux distributions default to systemd cgroups, but some guides still use cgroupfs. This mismatch causes kubelet and runtime to fight over cgroup management.
- How to avoid: Always set cgroup driver to
systemdin both kubelet and runtime configs, unless you have a specific reason to use cgroupfs. - Recovery: Update both configs, restart services, and test with a simple pod.
- Ignoring runtime logs during startup
- Why: Developers focus on pod events, but the root cause may be in runtime logs.
- Avoid: Make it a habit to check
journalctl -u containerd(or CRI-O) when investigating startup failures. - Recovery: Correlate timestamps between pod events and runtime logs.
- Using latest image tags with runtime-dependent features
- Why: Some features require specific runtime versions; using
latestcan break if the runtime doesn't support them. - Avoid: Pin image versions and test runtime compatibility in CI.
- Recovery: Roll back the image tag or upgrade the runtime.
- Overloading nodes without considering runtime overhead
- Why: runc adds some overhead per container. Too many containers can exhaust node resources.
- Avoid: Monitor runtime metrics and set appropriate pod density limits per node type.
- Recovery: Evict pods to other nodes or reduce container count.
- Not using RuntimeClass correctly
- Why: Misconfigured handlers cause pods to fail with
FailedCreatePodSandBox. - Avoid: Test RuntimeClass on a canary pod before assigning to workloads.
- Recovery: Remove the
runtimeClassNamefield to revert to default runtime.
Operations Checklist
Here is a concise checklist for runtime operations. For each item, assign a single owner in your team. Review this checklist monthly or after any incident.
Daily/Weekly Checks (Owner: Platform Engineer)
- [ ] Check node conditions:
kubectl get nodes -o custom-columns='NAME:.metadata.name,READY:.status.conditions[?(@.type=="Ready")].status,RUNTIME:.status.nodeInfo.containerRuntimeVersion' - [ ] Review runtime metrics: CPU, memory, container count, image pull latency.
- [ ] Examine logs for errors:
sudo journalctl -u containerd --since "24 hours ago" | grep -i error(run on nodes via automation). - [ ] Verify backup of runtime configs is up to date.
Before Any Change (Owner: SRE Lead)
- [ ] Capture baseline: runtime version, node count, pod success rate.
- [ ] Test change on one node (using taint/toleration to isolate).
- [ ] Document rollback procedure and exact commands.
- [ ] Announce change and expected impact in communication channel.
- [ ] Set up monitoring alerts for the affected metrics.
After Any Change (Owner: SRE Lead)
- [ ] Verify node is Ready and all pods Running.
- [ ] Run smoke tests: create a pod using
runtimeClassNameif changed. - [ ] Wait for observation period (e.g., 30 minutes) and compare error rates.
- [ ] If issues arise, execute rollback immediately and capture logs for post-mortem.
Quarterly (Owner: Security Engineer)
- [ ] Review runtime CVEs and apply patches.
- [ ] Re-evaluate sandboxing needs and adjust RuntimeClasses.
- [ ] Audit runtime configurations against CIS benchmarks or internal standards.
- [ ] Test disaster recovery: simulate runtime daemon failure and restore.
Conclusion
Advanced container runtime management in Kubernetes requires a disciplined approach. You need to understand the CRI, systematically verify configurations, test changes in isolation, and have robust rollback plans. The commands and checklists in this guide provide a foundation.
Start by auditing your current runtime setup with the inventory commands. Then pick one improvement: switch to a faster runtime like crun, add gVisor for sensitive workloads, or tighten monitoring. Document the change, assign an owner, and track results over a month.
Remember, the runtime is the foundation of every pod. When it works, nobody notices. When it fails, everything stops. Treat it as critical infrastructure: observe, plan, change, and verify.