Layer 4 — runc (Low-level OCI Runtime)

Doosan published on
6 min, 1001 words

Categories: post

Part 4 of the Docker layers series. From top to bottom, the stack is:

  1. docker CLI → 2. dockerd → 3. containerd → 4. runc
  2. Linux kernel. See the overview article.

Identity

runc is the low-level runtime: the program that actually creates containers. Everything above it, including the CLI, dockerd, and containerd, is orchestration. runc is the point where a container stops being an abstract idea and becomes a real, isolated process on top of the Linux kernel.

It is small, does one job, and exits immediately after starting the container. It is the reference implementation of the OCI Runtime Spec.

What "creating a container" actually means

A container is not a virtual machine. It is an ordinary Linux process that has been lied to about the surrounding world. runc tells that lie using three Linux kernel features:

  • namespaces: isolate what the process can see. The OCI spec defines eight namespace types: PID (its own PID 1), mount (its own filesystem view), network (its own interfaces), user, hostname (UTS), IPC, cgroup (its own cgroup view), and time (its own clock).
  • cgroups, or control groups: limit what and how much the process can use: CPU, memory, PID count, and I/O. This is how --memory=512m is enforced.
  • root filesystem, or rootfs: runc uses pivot_root into the filesystem that containerd prepared from image layers, so the process sees the image's files as / instead of the host's files.

It also applies security layers: capabilities for dropping root privileges, seccomp for syscall filtering, and optionally AppArmor/SELinux. Then it execs the container's entrypoint as PID 1 inside that constructed world, and exits.

Why it almost never appears in ps

runc is not long-lived. It runs only while the container is being created, hands off to the shim, and exits. So:

$ colima ssh -- sh -c 'ps -e -o comm | grep -E "dockerd|containerd|runc"'
dockerd
containerd
containerd-shim-runc-v2     # long-lived parent
# runc is usually not here — it already exited

To catch runc in the process list, you have to look while docker run is starting something slow. After that, the container process is reparented to the shim and runc disappears. This is intentional. See the shim explanation in Layer 3.

OCI Runtime Spec: why runc can be replaced

runc is only the reference implementation of the Open Container Initiative (OCI) Runtime Specification. The spec defines a standard "bundle", meaning rootfs + config.json, and the commands that a runtime must support, such as create, start, kill, and delete.

Because containerd speaks through this spec, any OCI-compliant runtime can be swapped in:

Low-level runtimeWhat changes
runcDefault. Standard Linux namespaces + cgroups. Written in Go.
crunSame model, written in C: faster startup and lower memory use.
gVisor (runsc)Puts a userspace kernel in front of the real kernel: stronger isolation with some overhead.
Kata ContainersRuns each container inside a lightweight micro VM: VM-grade isolation with container ergonomics.
youkirunc-compatible runtime written in Rust.

Because they implement the same OCI interface, changing the runtime, for example for stronger isolation, does not require touching the CLI, dockerd, or containerd. You only change the runtime-class configuration.

Seeing the spec consumed by runc

You can inspect runc directly inside the VM:

$ colima ssh -- sh -c 'command -v runc; runc --version'
/usr/bin/runc
runc version 1.x.x
spec: 1.x.x          # <- OCI Runtime Spec version it implements

# runc consumes a "bundle": rootfs directory + config.json
# config.json describes namespaces, cgroups, mounts, capabilities, and the process

That config.json is what containerd creates and passes to runc. runc reads it and creates exactly the container described there.

Full handoff flow

docker run nginx
  └─ dockerd                    [Layer 2] image interpretation, network/volume setup
       └─ containerd            [Layer 3] image → unpack rootfs, create "task"
            └─ containerd-shim-runc-v2    (resident process)
                 └─ runc        [Layer 4] namespace/cgroup, pivot_root, exec → exit
                      └─ nginx  [       ] PID 1 inside the container
                           └─   [Layer 5] Linux kernel

Summary

  • runc is where a container becomes a real isolated Linux process: namespaces for what it sees, cgroups for what it uses, and rootfs for its filesystem.
  • It is short-lived: it creates the container, hands off to the shim, and exits, so it usually does not appear in ps.
  • It implements the OCI Runtime Spec, so it is replaceable without changing upper layers: crun, gVisor, Kata, youki.
  • When using runc, the Linux kernel underneath it, Layer 5, is the non-swappable foundation. Its namespaces and cgroups make all of this possible. gVisor can abstract that foundation with a userspace kernel, and Kata can abstract it with a guest kernel.

References