Container Runtime Hierarchy
[!NOTE] This module explores the core principles of Container Runtime Hierarchy, deriving solutions from first principles and hardware constraints to build world-class, production-ready expertise.
1. The Great Split
Before 2016, Docker was a monolith. One binary (dockerd) did everything: downloading, unpacking, networking, and running containers.
Today, it is a modular stack of specialized tools.
The Stack
- Docker CLI: The user interface. Sends HTTP JSON to the daemon.
- Dockerd: The high-level manager. Handles images, volumes, and networks.
- Containerd: The industry standard container runtime. Manages the lifecycle (Start/Stop/Kill).
- Containerd-Shim: A tiny process that sits between
containerdandrunc. It allowscontainerd(and Docker) to restart without killing running containers. - Runc: The low-level runtime. It spawns the process, sets up Namespaces/Cgroups, and exits.
2. Interactive: Runtime Flow Simulator
Trace the path of a docker run command.
3. Code Examples
1. Go Implementation (Containerd Client)
This is how Kubernetes (via CRI) talks to containerd directly, bypassing Docker entirely.
package main
import (
"context"
"fmt"
"github.com/containerd/containerd"
"github.com/containerd/containerd/namespaces"
)
func main() {
// 1. Connect to Containerd Socket
client, err := containerd.New("/run/containerd/containerd.sock")
if err != nil {
panic(err)
}
defer client.Close()
ctx := namespaces.WithNamespace(context.Background(), "default")
// 2. Pull Image
image, err := client.Pull(ctx, "docker.io/library/redis:alpine", containerd.WithPullUnpack)
if err != nil {
panic(err)
}
// 3. Create Container (Metadata)
container, err := client.NewContainer(ctx, "redis-server", containerd.WithNewSnapshot("redis-snapshot", image))
if err != nil {
panic(err)
}
// 4. Create Task (Process) -> Calls Runc
task, err := container.NewTask(ctx, containerd.Stdio)
if err != nil {
panic(err)
}
fmt.Printf("Task PID: %d\n", task.Pid())
task.Start(ctx)
}
2. Java Implementation (Docker Socket)
Docker exposes a Unix Domain Socket at /var/run/docker.sock. Java can talk to it directly using HTTP. This is how tools like Testcontainers work.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.UnixDomainSocketAddress; // Java 16+
public class DockerClient {
public static void main(String[] args) throws Exception {
// 1. Create HTTP Client over Unix Socket
HttpClient client = HttpClient.newBuilder().build();
// 2. Construct Request (List Containers)
// Note: Java 16 introduced UnixDomainSocketAddress
// For older Java, use a library like 'junixsocket'
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("http://localhost/containers/json"))
.header("Content-Type", "application/json")
.GET()
.build();
// This is pseudo-code for the transport layer
// In reality, you need a custom BodyPublisher for Unix Sockets
System.out.println("Sending GET /containers/json to Docker Daemon...");
// Response would be JSON: [{"Id": "a1b2...", "Image": "redis", ...}]
}
}
[!NOTE] Why the Shim? The shim allows
containerdto exit (for upgrades) without killing the containers it started. The shim becomes the new parent of the container process, holding its STDIN/STDOUT open.
4. First Principles: Why Layers?
Why separate dockerd from containerd from runc?
- Standardization (OCI):
runcis the reference implementation of the OCI Runtime Spec. Anyone can write a replacement (e.g.,kata-runtimefor VM isolation,gvisorfor sandboxing). - Stability: The massive Docker Daemon can crash or upgrade, but
containerdand the shims keep the containers running.