containerd & CRI
Why runc isn't enough — image management, supervision, and Kubernetes integration
Why runc Isn't Enough
- Takes a rootfs + config.json
- Creates namespaces, cgroups, mounts
- Starts the container process
- Exits (it's not a daemon)
- Pull or store images
- Manage container lifecycle (restart, logs)
- Provide an API for orchestrators
- Handle snapshots or layer storage
- Survive daemon restarts
Note
The gap: runc is a low-level tool. You need a higher-level runtime to bridge the gap between "run this bundle" and "manage containers as a service." That's what containerd and CRI-O provide.
containerd Architecture
Origin & Status
- Originally extracted from the Docker daemon (2016)
- Donated to the CNCF — graduated project
- Used by Docker Engine, Kubernetes, and standalone deployments
Core Responsibilities
Image Pull & Push
- Implements OCI Distribution spec
- Content-addressable storage for blobs
- Pulls manifests, image configs, layer tarballs
- Supports multiple registries and auth
Snapshots
- Manages overlay2 filesystem layers
- Operations:
prepare,commit,remove - Each container gets a thin writable layer on top of read-only image layers
Task Management
- Create / start / stop / delete containers
- Uses runc (or other OCI runtimes) under the hood
- Manages container state and exit codes
- Streams stdout/stderr logs
Content Store
- Stores blobs: image layers, configs, manifests
- SHA256 content-addressable
- Deduplicates identical layers across images
- Garbage collection of unreferenced content
API & CLI Tools
| Interface | Protocol | Use Case |
|---|---|---|
| containerd gRPC API | gRPC (not REST) | Programmatic access — used by kubelet, Docker, BuildKit |
ctr | CLI | Low-level debugging tool (not user-friendly) |
nerdctl | CLI | Docker-compatible CLI for containerd (drop-in replacement) |
containerd-shim — Decoupling Runtime from Containers
Warning
The problem: If containerd restarts (e.g., during an upgrade), all containers would die if containerd were their parent process. You can't restart your container manager without killing all containers.
The Solution: containerd-shim
A small process that sits between containerd and the container, acting as the container's actual parent process:
- One shim per container (lightweight, ~10MB RSS)
- runc exits after starting the container — the shim takes over as parent
- If containerd restarts, shims keep running, containers stay alive
- Shim reports container state back to containerd via ttrpc (lightweight gRPC variant)
- Handles
stdin/stdout/stderrstreams - Reaps zombie processes (acts as init/subreaper for container PID 1)
Note
Current implementation: containerd-shim-runc-v2 — the v2 shim protocol. Other runtimes implement their own shims (e.g., containerd-shim-kata-v2, containerd-shim-runsc-v1 for gVisor).
The Full Stack — Who Does What
User / kubelet
Issues API calls
containerd
gRPC daemon: images, snapshots, tasks
containerd-shim
Parent process, survives containerd restarts
runc
Sets up NS/cgroups, execs, then EXITS
Container Process
Your workload (PID 1 inside container)
Process Tree Visualization
Manages images, snapshots, tasks. Communicates via gRPC. Can restart without killing containers.
Container A
PID 1: nginx
Container B
PID 1: python app.py
Note: runc is NOT in this tree — it already exited after setting up each container.
CRI — Container Runtime Interface
Why CRI Exists
- Kubernetes needed a stable, runtime-agnostic API to manage containers
- Early Kubernetes was tightly coupled to Docker — adding new runtimes required modifying kubelet
- CRI = a gRPC API specification that any runtime can implement
- kubelet speaks CRI; the runtime translates CRI calls into actual container operations
CRI Services
Pod-level:
RunPodSandbox— create the pod's network namespace and infra containerStopPodSandboxRemovePodSandboxPodSandboxStatus
Container-level:
CreateContainerStartContainerStopContainerRemoveContainerListContainersContainerStatusExecSync/ExecAttach/PortForward
PullImage— download image from registryRemoveImageListImagesImageStatusImageFsInfo— filesystem usage stats
The ImageService is simpler — just CRUD on images. All the heavy lifting (layer deduplication, content-addressable storage) is handled by the runtime internally.
Historical vs Modern Runtime Path
Old Path (before Kubernetes 1.24)
kubelet
Kubernetes node agent
dockershim
Built INTO kubelet, translated CRI to Docker API
Docker daemon
dockerd — full Docker engine
containerd
Embedded in Docker
runc
OCI runtime
Warning
The problem: kubelet → dockershim → Docker → containerd → runc = 4 layers of indirection. Docker added features (build, swarm, CLI) that Kubernetes didn't need. The dockershim was removed in Kubernetes 1.24 (April 2022).
Current Path (Kubernetes 1.24+)
kubelet
Kubernetes node agent
containerd
CRI plugin built-in
containerd-shim
Parent process
runc
OCI runtime (exits)
Alternative Path (CRI-O)
kubelet
Kubernetes node agent
CRI-O
CRI-native runtime
conmon
Container monitor (like shim)
runc
OCI runtime (exits)
CRI-O
CRI-O — Purpose-Built for Kubernetes
- Built by Red Hat, specifically for Kubernetes
- Implements only CRI — nothing extra (no build, no push, no standalone container management)
- Simpler codebase than containerd (does less, so less attack surface)
- Uses
conmon(container monitor) instead of containerd-shim — same role, different implementation - Default runtime in OpenShift and RHEL-based Kubernetes distributions
- Version-locked to Kubernetes (CRI-O 1.28 for K8s 1.28)
Tip
conmon vs containerd-shim: Both serve the same purpose — act as the parent process of the container after the OCI runtime exits. conmon is written in C, containerd-shim in Go. conmon also handles logging (writing container stdout/stderr to disk).
Comparison: Docker vs containerd vs CRI-O
| Feature | Docker | containerd | CRI-O |
|---|---|---|---|
| Image pull | Yes | Yes | Yes |
| Image build | Yes (docker build) |
No (use BuildKit separately) | No (use Buildah, kaniko) |
| CRI support | Via dockershim (removed K8s 1.24) | Built-in CRI plugin | Native (CRI is all it does) |
| CLI tool | docker |
ctr / nerdctl |
crictl |
| Container monitor | containerd-shim (via containerd) | containerd-shim | conmon |
| Standalone use | Yes (docker run) | Yes (nerdctl run) | No (Kubernetes only) |
| Used by | Docker Desktop, dev machines | Most K8s clusters, Docker Engine | OpenShift, RHEL K8s |
| Scope | Full platform (build, run, push, compose, swarm) | Container runtime + image management | CRI implementation only |
CLI Tools Quick Reference
ctr (containerd)
$ ctr images pull docker.io/library/nginx:latest
$ ctr containers create nginx:latest myng
$ ctr tasks start myng
$ ctr tasks list
$ ctr tasks kill myng
Low-level, for debugging. No docker-compose equivalent.
nerdctl (containerd)
$ nerdctl run -d --name myng nginx
$ nerdctl ps
$ nerdctl logs myng
$ nerdctl stop myng
$ nerdctl compose up
Docker-compatible CLI. Supports compose files.
crictl (any CRI runtime)
$ crictl pull nginx:latest
$ crictl images
$ crictl pods
$ crictl ps
$ crictl logs <container-id>
Works with containerd AND CRI-O. K8s debugging tool.
How containerd Processes an Image into a Container
- Pull manifest — fetch OCI image index / manifest from registry (content-addressable by digest)
- Pull config blob — image config contains default CMD, ENV, exposed ports, layer digests
- Pull layers — download each layer tarball (gzip-compressed), store in content store
- Prepare snapshot — overlay2 snapshotter stacks layers: base → layer1 → layer2 → ... → writable top layer
- Generate OCI bundle — create rootfs from snapshot + generate config.json from image config + pod spec
- Create shim — spawn containerd-shim-runc-v2 process
- Shim invokes runc — runc create (sets up NS, cgroups, mounts) → runc start (execs process) → runc exits
- Container running — shim is parent process, reports state to containerd via ttrpc