Skip to content
Menu

Virtualization5 min read

containerd & CRI

Why runc isn't enough — image management, supervision, and Kubernetes integration

Why runc Isn't Enough

What runc DOES
  • Takes a rootfs + config.json
  • Creates namespaces, cgroups, mounts
  • Starts the container process
  • Exits (it's not a daemon)
What runc DOESN'T Do
  • 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

InterfaceProtocolUse Case
containerd gRPC APIgRPC (not REST)Programmatic access — used by kubelet, Docker, BuildKit
ctrCLILow-level debugging tool (not user-friendly)
nerdctlCLIDocker-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/stderr streams
  • 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

  1. User / kubelet

    Issues API calls

  2. containerd

    gRPC daemon: images, snapshots, tasks

  3. containerd-shim

    Parent process, survives containerd restarts

  4. runc

    Sets up NS/cgroups, execs, then EXITS

  5. Container Process

    Your workload (PID 1 inside container)

Process Tree Visualization

Host OS
containerd (daemon)

Manages images, snapshots, tasks. Communicates via gRPC. Can restart without killing containers.

containerd-shim (per container)

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

RuntimeService

Pod-level:

  • RunPodSandbox — create the pod's network namespace and infra container
  • StopPodSandbox
  • RemovePodSandbox
  • PodSandboxStatus

Container-level:

  • CreateContainer
  • StartContainer
  • StopContainer
  • RemoveContainer
  • ListContainers
  • ContainerStatus
  • ExecSync / Exec
  • Attach / PortForward
ImageService
  • PullImage — download image from registry
  • RemoveImage
  • ListImages
  • ImageStatus
  • ImageFsInfo — 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)

  1. kubelet

    Kubernetes node agent

  2. dockershim

    Built INTO kubelet, translated CRI to Docker API

  3. Docker daemon

    dockerd — full Docker engine

  4. containerd

    Embedded in Docker

  5. 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+)

  1. kubelet

    Kubernetes node agent

  2. containerd

    CRI plugin built-in

  3. containerd-shim

    Parent process

  4. runc

    OCI runtime (exits)

Alternative Path (CRI-O)

  1. kubelet

    Kubernetes node agent

  2. CRI-O

    CRI-native runtime

  3. conmon

    Container monitor (like shim)

  4. 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)

console
console

        $ 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)

console
console

        $ 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)

console
console

        $ 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

  1. Pull manifest — fetch OCI image index / manifest from registry (content-addressable by digest)
  2. Pull config blob — image config contains default CMD, ENV, exposed ports, layer digests
  3. Pull layers — download each layer tarball (gzip-compressed), store in content store
  4. Prepare snapshot — overlay2 snapshotter stacks layers: base → layer1 → layer2 → ... → writable top layer
  5. Generate OCI bundle — create rootfs from snapshot + generate config.json from image config + pod spec
  6. Create shim — spawn containerd-shim-runc-v2 process
  7. Shim invokes runc — runc create (sets up NS, cgroups, mounts) → runc start (execs process) → runc exits
  8. Container running — shim is parent process, reports state to containerd via ttrpc
Solidnines — solidnines.com