Containers & Virtualization Fundamentals
Container Runtimes — Docker, containerd, CRI-O
Understanding low-level runtimes (runc) and high-level orchestrator runtime interfaces (CRI).
Interview: Frequently asked in SRE and Kubernetes Administrator interviews to test your knowledge of container orchestration layers, the CRI standard, and low-level host container interaction.
Introduction
In the early days of containerization, "Docker" was a single, monolithic tool that did everything: it built images, managed storage volumes, ran the API server, pulled registry images, and executed container processes.
As containerization matured and Kubernetes emerged as the dominant orchestrator, this monolithic approach became a bottleneck. The industry moved toward modularity, creating the Container Runtime Ecosystem. Today, container execution is split into two distinct tiers: High-Level Runtimes and Low-Level Runtimes, governed by open standards.
Why It Matters
Understanding the runtime division is critical for building secure environments, debugging cluster node startup failures, optimizing container launch speeds, and configuring custom cluster infrastructure.
The Standards: OCI and CRI
To keep the ecosystem interoperable and prevent vendor lock-in, the industry established two primary specifications:
1. OCI (Open Container Initiative): Defines specifications for how a container image is structured (Image Spec) and how a low-level runtime must unpack it and run it on a host system (Runtime Spec).
2. CRI (Container Runtime Interface): A gRPC protocol defined by Kubernetes. It allows the Kubernetes node agent (kubelet) to communicate with any high-level container runtime without compiling the runtime code directly into the Kubernetes binary.
The Runtime Stack Hierarchy
The container execution flow operates like a relay race:
1. High-Level Runtimes (CRI Runtimes)
High-level runtimes handle the lifecycle and storage management of containers, but they do not execute processes directly. They receive requests from Kubernetes or CLI clients, fetch images from registries, unpack them onto disk, prepare the container root filesystem, and configure network namespaces.
• containerd: Originally extracted from Docker, this is an industry-standard, lightweight runtime used by default in Google GKE, AWS EKS, and Azure AKS.
• CRI-O: A minimal runtime built specifically for Kubernetes. It has no desktop features and is optimized for speed and security, commonly used in Red Hat OpenShift.
2. Low-Level Runtimes (OCI Runtimes)
Low-level runtimes do the actual work of speaking to the host OS kernel. They receive the root file path and configuration from high-level runtimes, call unshare() or clone() to configure namespaces, set up cgroup limits, and launch the entrypoint process.
• runc: The reference OCI implementation. It runs as a transient process, meaning once it boots the container entrypoint process, runc exits.
• crun: A fast, C-based replacement for runc with a smaller memory footprint.
• Secure Sandboxes (Kata/gVisor): Instead of sharing the host kernel directly, Kata Containers boot a lightweight MicroVM per container, and gVisor intercepts system calls via a user-space kernel (Sentry), dramatically increasing multi-tenant security.
Kubernetes Architecture Flow
↓
[kubelet on Node] -> Calls CRI gRPC
↓
[containerd / CRI-O] -> Pulls Image & Prepares filesystem
↓
[runc / OCI Runtime] -> Creates Namespaces/cgroups, runs container process
Practical Example
Let's bypass Docker/Kubernetes and inspect container operations directly inside containerd using its low-level debugging CLI, ctr:
What just happened? We interacted directly with containerd, bypassing high-level clients. Containerd pulled the image layers, mounted the OverlayFS filesystem, and used runc to spawn Redis under isolated namespaces.
Quick Quiz
Q1: Why did Kubernetes deprecate "Dockershim"?
A) Because Docker is no longer supported on Linux.
B) Dockershim was a translation layer keeping Kubernetes compatible with monolithic Docker. Bypassing it to talk directly to containerd via CRI reduces complexity and latency.
C) Because cgroups v2 only support CRI-O.
D) Because runc requires a commercial license.
Answer: B — Talks directly to standard CRI endpoints (like containerd's built-in CRI plugin) to run leaner and faster.
Scenario-Based Challenge
Production Scenario:
Your SaaS product allows users to upload and execute untrusted Python scripts on shared Kubernetes cluster nodes. If a script exploits a zero-day kernel privilege escalation vulnerability, the user could access the node and intercept other customer data. How can you modify the container runtime configuration to prevent this?
View Solution
Since default runtimes like runc share the host OS kernel, a kernel exploit compromises the host. To fix this, you should introduce a sandboxed low-level runtime.
1. Install gVisor on the nodes and define it in Kubernetes as a RuntimeClass (e.g., runtimeClassName: gvisor).
2. gVisor runs a user-space kernel (the "Sentry") that intercepts system calls made by the Python script, blocking direct execution of risky system calls on the host kernel.
3. Alternatively, use Kata Containers, which wraps the script container inside a dedicated microVM, providing a second hardware-virtualized boundary layer.
Interview Questions
1. What is the Container Runtime Interface (CRI)?
CRI is a gRPC API that defines how Kubernetes interacts with high-level container runtimes. It decouples Kubernetes core engine development from container runtime updates, allowing plug-and-play usage of containerd, CRI-O, or any other conforming runtime.
2. What is the difference between a high-level runtime and a low-level runtime?
High-level runtimes (like containerd) manage images, volumes, network setups, and the overall runtime API. Low-level runtimes (like runc) are transient engines that create namespaces/cgroups, execute the container process, and immediately exit.
Production Considerations
In production Kubernetes clusters, troubleshooting container startup issues requires using crictl instead of docker commands, because modern nodes bypass Docker entirely. crictl is designed to be runtime-agnostic, querying containerd or CRI-O directly to inspect pods, containers, and logs. Always verify that your node runtime socket (e.g., unix:///run/containerd/containerd.sock) is configured correctly in kubelet.