How Linux Spawns OCI Containers Using runc

This article explores the technical mechanics behind how the Linux operating system uses the runc CLI tool to create and manage Open Container Initiative (OCI) compliant containers. It covers the consumption of the standard OCI container bundle, the invocation of core Linux kernel isolation primitives such as namespaces and cgroups, the filesystem pivoting process, and the execution transition to the target containerized workload.

The OCI Container Bundle

The deployment process begins with the OCI container bundle. Before runc interacts with the kernel, it requires a standardized bundle directory containing two primary components:

  1. rootfs/: A directory containing the complete root filesystem of the container image.
  2. config.json: A specification file that details process execution parameters, environment variables, capabilities, mount points, resource limits, and kernel features to enable.

Higher-level runtimes like containerd or CRI-O unpack container image layers into this filesystem layout and generate the config.json file according to the OCI Runtime Specification before handing execution off to runc.

Invoking runc and libcontainer

When invoked through commands such as runc run <container-id> or runc create <container-id>, the runc binary parses the config.json file. It then uses its internal Go package, libcontainer, to interact directly with the Linux kernel via system calls. runc acts as an ephemeral wrapper: its job is strictly to configure the kernel primitives, launch the containerized process, and optionally exit while handing supervisory duties over to a monitoring process (like shim).

Kernel Isolation via Namespaces

To isolate the new container process from the host and other containers, runc uses the clone(2) and unshare(2) system calls with specific flag arguments to instantiate Linux namespaces:

Resource Constraints with Cgroups

Before the workload executes, runc applies resource boundaries using Linux Control Groups (cgroups v1 or cgroups v2).

  1. runc creates a new cgroup directory under /sys/fs/cgroup/ (in cgroups v2, unified hierarchy).
  2. It writes limits defined in config.json—such as memory limits, CPU shares, I/O bandwidth, and pids limits—to corresponding control files (e.g., memory.max, cpu.weight).
  3. The process ID of the newly spawned setup process is added to the cgroup's cgroup.procs file, ensuring all subsequently spawned child processes inherit these restrictions.

Filesystem Jail and Security Hardening

Once isolated by namespaces and constrained by cgroups, runc secures the filesystem and restricts kernel privileges:

  1. Pivoting the Root: Using the pivot_root(2) system call (preferred over chroot), runc swaps the container's rootfs with the host's root filesystem, unmounts the old host root, and confines the process entirely to the new root directory.
  2. Mounting Virtual Filesystems: runc mounts container-specific pseudo-filesystems, including /proc, /sys, and /dev, ensuring device nodes and system information reflect the container's isolated context.
  3. Capabilities: It drops unnecessary Linux capabilities (e.g., CAP_SYS_ADMIN, CAP_NET_ADMIN) using capset(2) to limit the operations the containerized process can execute, even if running as UID 0.
  4. Seccomp Filters: runc parses system call rules defined in the specification and uses prctl(2) with PR_SET_SECCOMP to apply BPF (Berkeley Packet Filter) profiles, blocking restricted system calls at the kernel boundary.
  5. LSM Profiles: It configures Linux Security Modules like AppArmor or SELinux by writing the appropriate profile attributes to /proc/self/attr/.

Final Execution: execve

With all security boundaries, namespaces, mounts, and resource controls established, the initialization process calls execve(2). This system call replaces the temporary runc initialization process with the container's actual user-defined entrypoint binary (such as /bin/sh or a compiled service). The application process now runs natively on the Linux kernel with near-zero virtualization overhead, fully confined within the OCI-compliant boundaries enforced by the OS.