What Is runc in Depth?

What Is runc in Depth?

Every time Docker or Kubernetes says it "started a container," one small, unglamorous command-line tool is the thing that actually does the work. This is a deep look at runc — the quiet engine sitting at the very bottom of the entire container ecosystem.

When you type docker run nginx or a Kubernetes cluster schedules a new pod, a long chain of software hands the request down through several layers before anything actually happens on the machine. Docker talks to containerd. Containerd talks to a smaller component called a shim. And at the very bottom of that chain, one small program is finally told: “create a new, isolated Linux process, put these exact restrictions on it, and start this exact command inside it.” That program is runc. It is not glamorous, it has no dashboard, and most engineers who use containers every day have never typed its name directly — yet it is the single piece of software that turns the abstract idea of “a container” into a real, running, isolated Linux process. This guide walks through what runc actually is, how it works internally, how it fits into the bigger container ecosystem, and why understanding it makes you a stronger systems engineer.

1Core Concepts: What Runc Actually Is

Strip away every layer of tooling built on top of it, and a “container” is just a regular Linux process wearing very specific restrictions.

runc is a small, low-level command-line tool that creates and runs containers according to the OCI Runtime Specification — a written standard, agreed upon by the container industry, that precisely defines what a “container” must look like on disk and how a runtime must behave when asked to start one. runc is the original, reference implementation of that specification, meaning it is the tool the specification itself was largely modeled after and validated against. In plain terms: runc takes a folder containing a root filesystem and a configuration file, and turns that folder into a genuinely running, isolated process on the host machine.

The crucial thing to understand is that runc does not implement any new isolation technology of its own. Linux has offered process isolation primitives for years — namespaces to give a process its own private view of the system, and control groups (cgroups) to limit how much CPU, memory, and I/O a process can consume. runc’s entire job is to correctly and carefully wire these existing kernel features together, in the right order, with the right configuration, every single time — something that is far more fiddly and error-prone to do by hand than it sounds.

Everyday Analogy

Imagine a hotel room. The building already has walls, locks, plumbing, and electricity — those already existed before any guest arrived, just like Linux namespaces and cgroups already exist in the kernel before any container is created. What runc does is act like the front-desk system that actually assigns you to a specific room, locks every other room’s door from your key, sets your room’s thermostat and minibar limits, and lets you in. The walls and locks were always there; runc is the precise, repeatable process of configuring and handing over one specific, isolated space using them.

i
Key Insight

A “container,” at the very lowest level, is nothing more than a standard Linux process that has been started with a specific combination of namespaces, cgroup limits, and filesystem root changes applied to it. runc’s job is to apply that combination correctly, not to invent a new kind of process.

2Problem & Motivation: Why Runc Exists

runc was not invented in a vacuum — it was extracted from a real, urgent industry problem: fragmentation.

In Docker’s early years, all of the low-level logic for creating namespaces, applying cgroup limits, and starting an isolated process lived inside Docker’s own codebase, in a component called libcontainer. This worked, but it meant that “how a container actually gets created” was defined entirely by one company’s internal implementation. Other projects experimenting with containers — including early competitors and cloud providers — had no shared, agreed-upon definition of what a container even was at the filesystem and process level. Every tool risked reinventing its own slightly different, incompatible version of the same idea.

1

libcontainer Inside Docker (2013–2014)

Docker’s own low-level container-creation code, libcontainer, handled namespaces, cgroups, and process startup internally, tightly coupled to the rest of Docker’s engine.

2

The Open Container Initiative Forms (2015)

Docker, CoreOS, and a broad coalition of other companies founded the Open Container Initiative under the Linux Foundation, agreeing to define open, vendor-neutral standards for container image formats and container runtimes, so the whole industry could build on the same foundation instead of competing formats.

3

runc Is Extracted and Donated (2015)

Docker extracted libcontainer’s core logic into a standalone command-line tool named runc, and donated it to the Open Container Initiative as the reference implementation of the newly forming OCI Runtime Specification.

4

Ecosystem-Wide Adoption

Because runc implements an open specification rather than a proprietary format, higher-level tools — containerd, CRI-O, and eventually Kubernetes itself through the Container Runtime Interface — were all built to work with any OCI-compliant runtime, with runc becoming the overwhelmingly dominant default choice.

This history matters because it explains runc’s entire design philosophy. It is deliberately small, deliberately unopinionated, and deliberately does exactly one job — create and run one container, once, per invocation — leaving every higher-level concern (image pulling, networking setup, scheduling, logging) to the layers built on top of it. That narrow focus is precisely what let an entire ecosystem of different, competing tools all confidently build on the same trustworthy foundation.

3Architecture & Core Components

runc’s entire world revolves around one folder, one file inside it, and a handful of Linux kernel primitives.

Input

Bundle

A directory on disk containing everything runc needs to start a container: a root filesystem (the extracted, unpacked contents of a container image) and a configuration file. Higher-level tools like containerd generate this bundle automatically before ever calling runc.

Input

config.json

The bundle’s configuration file, written according to the OCI Runtime Specification. It precisely describes the command to run, environment variables, which namespaces to create, resource limits, mount points, and security settings like capabilities and seccomp filters.

Kernel Primitive

Namespaces

Linux kernel features that give a process its own private, isolated view of a particular kind of system resource — process IDs, network interfaces, mount points, hostname, and more — so the container believes it is alone on the machine.

Kernel Primitive

Control Groups (cgroups)

Linux kernel features that limit and account for how much CPU, memory, disk I/O, and other resources a group of processes may consume, preventing one container from starving the rest of the host.

Output

Container Process

The actual, running Linux process created by runc, subject to every namespace and cgroup restriction defined in config.json, executing the exact command the bundle specified.

Bookkeeping

State Directory

A directory runc maintains (commonly under /run) tracking the status of every container it has created — its process ID, current state, and bundle path — so later runc commands like status checks or stop requests know exactly what they are talking to.

graph TB
    subgraph HighLevel["Higher-Level Tooling"]
        DOCKER["Docker Engine"]
        CONTAINERD["containerd"]
        SHIM["containerd-shim"]
    end
    subgraph Bundle["OCI Bundle (prepared on disk)"]
        RFS["Root Filesystem (unpacked image layers)"]
        CFG["config.json (OCI runtime spec)"]
    end
    RUNC["runc (OCI runtime)"]
    subgraph Kernel["Linux Kernel Primitives"]
        NS["Namespaces"]
        CG["cgroups"]
        CAP["Capabilities / seccomp"]
    end
    PROC["Isolated Container Process"]

    DOCKER --> CONTAINERD --> SHIM --> RUNC
    RFS --> RUNC
    CFG --> RUNC
    RUNC --> NS --> PROC
    RUNC --> CG --> PROC
    RUNC --> CAP --> PROC
    

Fig. 1 — runc sits at the very bottom of the stack, translating a prepared bundle into a real, restricted Linux process.

?
Interviewer May Ask

“What are the two things an OCI bundle must contain, and what is each one for?” A strong answer names the root filesystem (the container’s unpacked files) and config.json (the OCI-spec-compliant description of how that filesystem and process should be isolated and restricted), and notes that runc itself does not create either one — it only consumes them.

4Internal Working: Namespaces, Cgroups, and Pivot Root

This chapter opens the hood on the exact sequence of kernel-level steps runc performs to turn a bundle into an isolated process.

Creating Namespaces

Linux offers several distinct namespace types, and runc creates whichever combination config.json requests, typically all of the following for a standard container: a PID namespace so the container’s processes get their own independent process-ID numbering starting near 1; a network namespace so the container gets its own private network interfaces and routing table; a mount namespace so the container’s view of mounted filesystems is separate from the host’s; a UTS namespace so the container can have its own hostname; and an IPC namespace isolating inter-process communication mechanisms. Each namespace independently hides one category of system state from the container, and together they create the overall illusion of a private machine.

Setting Up the Root Filesystem: pivot_root

Before the container’s actual command runs, runc must change what the process considers to be the root of the filesystem, from the host’s real root directory to the container’s own root filesystem, extracted from its image layers. runc typically uses a system call called pivot_root to perform this swap, which is more secure than the older chroot mechanism because it fully moves the old root out of the new mount namespace rather than merely changing a path prefix, making it substantially harder for a process to “break out” back to the host’s original filesystem.

Everyday Analogy

pivot_root is like moving into a new house and having the front door to your old house permanently sealed and hidden from view, rather than just being told “please don’t walk back through that door.” chroot, the older and weaker approach, is closer to the second version — the old door is still there, just marked off-limits, which a sufficiently determined process can sometimes find a way around.

Applying Control Group Limits

Once namespaces are established, runc creates a dedicated cgroup for the container and writes the resource limits from config.json into it — maximum memory, CPU share or quota, and I/O weight, among others. The kernel then enforces these limits automatically for every process running inside that container, for the entire lifetime of the container, without runc needing to actively police anything after the fact.

Dropping Privileges and Applying Security Restrictions

Before finally executing the container’s command, runc applies the remaining security settings from config.json: dropping unnecessary Linux capabilities (fine-grained privileges that would normally come bundled with running as root), applying a seccomp filter that blocks the process from making dangerous system calls, and setting up mandatory access control profiles if the platform uses SELinux or AppArmor. Only after every one of these steps has succeeded does runc finally call execve to replace itself with the actual container process.

!
Common Gotcha

Because every one of these steps must happen in a precise order — namespaces before filesystem pivot, filesystem pivot before privilege dropping, privilege dropping before execution — a container runtime that gets the ordering wrong can accidentally leave a window where the process has more access than intended. This ordering discipline is exactly why the OCI Runtime Specification exists: to make every compliant runtime behave identically and safely.

5Data Flow & Lifecycle of a Container Under Runc

runc’s command-line interface maps directly onto the distinct stages a container passes through, from nothing to running to gone.

sequenceDiagram
    participant Caller as containerd-shim
    participant Runc as runc
    participant Kernel as Linux Kernel
    participant Proc as Container Process

    Caller->>Runc: runc create (bundle path, container id)
    Runc->>Kernel: create namespaces, cgroup, pivot_root
    Kernel-->>Runc: process created, paused before exec
    Runc-->>Caller: container created (not yet running)

    Caller->>Runc: runc start (container id)
    Runc->>Proc: release process to execute its command
    Proc-->>Caller: container now running

    Caller->>Runc: runc kill / runc delete
    Runc->>Kernel: send signal, tear down cgroup and namespaces
    Kernel-->>Runc: process terminated, resources reclaimed
    

Fig. 2 — Creation and starting are deliberately separate steps, giving higher-level tools a window to finish setup before code runs.

runc CommandWhat It DoesTypical Caller
runc createSets up namespaces, cgroups, and the root filesystem, and pauses the process immediately before it executes its actual commandcontainerd-shim, during container setup
runc startReleases the already-created, paused container process so it actually begins executingcontainerd-shim, once setup is confirmed complete
runc stateReports the current status of a container: whether it is created, running, or stopped, along with its process IDHigher-level tools polling container health
runc killSends a signal (such as SIGTERM or SIGKILL) to the container’s main processOrchestrators stopping a container gracefully or forcefully
runc deleteCleans up all remaining resources associated with a stopped container, including its cgroup and state trackingcontainerd-shim, after the container has exited
runc execStarts a brand-new process inside an already-running container’s existing namespaces, without creating a whole new containerInteractive debugging sessions, such as those behind docker exec

The deliberate split between create and start is one of runc’s most important design decisions. It exists because the calling tool — usually a lightweight process called a shim — often needs to finish its own setup work, such as wiring up standard input and output streams or registering the container with a higher-level system, after the isolated environment exists but before user code inside it actually starts running. Combining creation and execution into a single step would remove that safe window entirely.

6Design Patterns & Anti-Patterns

The way higher-level tools use runc reveals some clear best-practice patterns — and some equally clear mistakes.

Pattern: The Shim-per-Container Model

containerd does not call runc directly and then wait around for the container to finish. Instead, it launches a small, independent long-running process called a shim for each container, and that shim invokes runc to create and start the container. Once the container is running, runc itself exits — the shim, not runc, stays alive to track the container’s process, forward its exit status, and keep standard input and output streams connected. This means a restart of containerd or even the Docker daemon does not kill already-running containers, because those containers’ survival never depended on runc or the higher-level daemon staying alive.

ADR-021 — Runtime Selection for a Managed Kubernetes Platform Accepted
Context

A platform team building a managed Kubernetes offering needed to select the OCI runtime their node agents would use underneath containerd, balancing broad compatibility against emerging alternatives offering stronger sandboxing.

Decision

The team standardized on runc as the default runtime for general workloads, while adding support for an OCI-compatible sandboxed alternative (such as a virtual-machine-backed runtime) as an optional runtime class for untrusted, multi-tenant workloads requiring stronger isolation than namespaces alone provide.

Consequences

Because both runtimes implement the same OCI Runtime Specification, Kubernetes’ Container Runtime Interface layer could support both without any change to how pods are scheduled or described, giving workloads a per-pod choice of isolation strength without fragmenting the platform.

Anti-Pattern: Calling Runc Directly in Production Automation

Because runc is intentionally low-level and unopinionated, it does not handle image pulling, logging, networking configuration, or restart policies on its own. Building custom production tooling that shells out to runc directly, bypassing containerd or a similar daemon, means reimplementing a large amount of battle-tested logic that the ecosystem has already solved — a classic case of an appealing shortcut creating significant hidden long-term maintenance cost.

Anti-Pattern: Assuming Runc Manages Networking

runc creates the network namespace a container lives in, but it does not configure interfaces, IP addresses, or routing inside that namespace by default — that is deliberately left to a separate component (typically a CNI plugin in the Kubernetes world). Teams occasionally assume runc handles networking end to end and are confused when a freshly created container has an isolated but entirely empty network namespace.

7Advantages, Disadvantages & Trade-offs

runc’s narrow focus is simultaneously its greatest strength and the source of its most common criticisms.

Advantages

  • Being an open, industry-standard OCI implementation means it works interchangeably underneath many different higher-level tools without vendor lock-in
  • Extremely lightweight startup overhead, since it is directly wiring existing kernel primitives together rather than running its own heavyweight supervisor process
  • Widely audited and battle-tested, having been the default runtime for the overwhelming majority of production container deployments for years
  • Small, focused codebase that is easier to reason about and secure than a monolithic runtime trying to do everything at once
  • Deliberate create-then-start separation gives calling tools a safe window to finish their own setup

Disadvantages & Trade-offs

  • Namespace-based isolation is weaker than full virtual-machine isolation, since containers still share the host kernel — a kernel vulnerability can theoretically affect every container on that host
  • Being intentionally minimal means it depends entirely on other tools (shims, daemons, CNI plugins) for a complete, production-usable system; it is a building block, not an out-of-the-box platform
  • Historical vulnerabilities in runc’s own code have, on rare occasions, allowed carefully crafted containers to interfere with the host, prompting the broader move toward optional sandboxed runtime alternatives for untrusted workloads
  • Debugging low-level namespace or cgroup issues surfaced through runc requires real familiarity with Linux kernel primitives, which is a steeper learning curve than higher-level tooling implies

These trade-offs are exactly why the container ecosystem grew a second category of OCI-compliant runtimes — often called sandboxed or “secure” runtimes — that implement stronger isolation, frequently by running each container inside its own lightweight virtual machine, at the cost of somewhat higher overhead per container. runc remains the default for the vast majority of workloads precisely because its speed and simplicity outweigh the isolation gap for most trusted, well-managed environments.

8Security Considerations

Because runc sits directly on the boundary between a container and the host kernel, its security posture matters enormously.

Shared Kernel, Shared Risk

Every container started by runc shares the exact same host kernel as every other container and the host operating system itself. Namespaces and cgroups restrict what a process can see and use, but they do not create a separate kernel per container the way a virtual machine would. This means a sufficiently severe kernel-level vulnerability, if exploited from inside a container, has the potential to affect the entire host, which is the fundamental isolation ceiling of namespace-based containers regardless of how carefully runc itself is implemented.

!
Security Warning

Never run a container with unnecessary Linux capabilities, and avoid the privileged mode that disables most of runc’s default security restrictions, unless a workload genuinely requires direct host-level access. Every capability and restriction runc drops by default exists specifically to shrink what a compromised container process could do.

Seccomp, Capabilities, and Mandatory Access Control

runc’s default configuration, inherited from the OCI specification’s recommended defaults, drops a long list of Linux capabilities that are rarely needed by ordinary application workloads, and applies a seccomp filter blocking system calls that are common vectors for container escapes or kernel exploitation. Platforms layering SELinux or AppArmor profiles on top add a further, independent restriction, so that even if one security layer is bypassed, others remain in place — the same defense-in-depth principle that governs every well-designed container platform.

The Rise of Sandboxed Alternatives

For workloads that must run genuinely untrusted or multi-tenant code — for example, a platform that lets external customers upload arbitrary code to execute — many organizations choose an OCI-compliant sandboxed runtime instead of runc, giving each container the stronger isolation boundary of a lightweight virtual machine while still fitting into the exact same OCI-based tooling and Kubernetes workflows.

?
Interviewer May Ask

“Why is a container generally considered less isolated than a virtual machine?” A strong answer explains that containers created by runc share the host kernel directly, relying on namespaces and cgroups for isolation, whereas a virtual machine runs its own separate kernel, giving it a fundamentally stronger isolation boundary at the cost of higher overhead.

9Monitoring, Logging & Metrics

Because runc itself is short-lived per invocation, meaningful monitoring happens at the layers around it, using data runc exposes.

runc does not run a persistent background process, log continuously, or expose a metrics endpoint of its own — it executes a specific command, does its job, and (aside from the container process it created) exits. Observability therefore comes from two places: the runc state command, which higher-level tools poll to check whether a container is created, running, or stopped, and the underlying cgroup filesystem, which the kernel continuously updates with real-time resource usage figures for every container runc set up, without runc needing to actively report anything itself.

<100ms
TYPICAL TIME FOR RUNC TO CREATE AND START A LIGHTWEIGHT CONTAINER
5+
COMMON NAMESPACE TYPES CONFIGURED PER STANDARD CONTAINER
1
DEDICATED CGROUP CREATED PER CONTAINER FOR RESOURCE ACCOUNTING

Production platforms typically monitor at the containerd or Kubernetes layer rather than watching runc invocations directly, since those higher layers aggregate the per-container cgroup statistics runc’s setup makes available into fleet-wide dashboards covering CPU throttling, memory pressure, and out-of-memory kill events. When something does go wrong specifically inside runc’s own execution — a failed namespace creation, a misconfigured mount, a seccomp filter rejecting an unexpected system call — it typically surfaces as an error returned from the runc create or runc start call itself, which the calling shim then logs and propagates upward.

i
Practical Tip

When debugging a container that fails to start, checking the logs of the shim or containerd, not just the application’s own logs, is essential — the failure often happens at the runc layer, before the application’s own code has even had a chance to run or log anything.

10Best Practices & Common Mistakes

Most day-to-day engineers never call runc directly, but understanding these practices explains a great deal of container platform behavior.

Best Practice

Let Higher-Level Tools Own the Lifecycle

Manage containers through Docker, containerd, or Kubernetes rather than invoking runc directly in production, so image management, networking, restart policies, and logging remain consistent and well-supported.

Best Practice

Keep the Default Security Profile

Avoid disabling runc’s default seccomp filters and dropped capabilities unless a specific, well-understood workload requirement demands it, and prefer the narrowest possible additional privilege rather than full privileged mode.

Best Practice

Choose the Right Runtime Class per Workload

Use runc for trusted, first-party workloads where speed matters most, and reserve sandboxed OCI runtimes for untrusted or multi-tenant code that needs a stronger isolation boundary.

Common Mistake

Assuming a Container Restart Requires the Whole Daemon

Because the shim, not runc or the daemon, keeps a running container alive, restarting Docker or containerd does not necessarily kill already-running containers — a fact that surprises engineers unfamiliar with the shim architecture.

Common Mistake

Confusing OCI Image Spec with OCI Runtime Spec

The Open Container Initiative actually defines two separate specifications — one for image formats and one for runtimes. runc implements only the runtime specification; it has nothing to do with how images are built, pulled, or stored.

Common Mistake

Treating Namespace Isolation as Equivalent to VM Isolation

Designing a security model that assumes runc-based containers are as strongly isolated as virtual machines is a frequent and consequential misconception, given that all containers on a host still share the same kernel.

11Real-World & Industry Examples

runc is not a theoretical piece of infrastructure — it is doing real work, right now, under nearly every major container platform in production.

Docker and containerd

Docker’s engine delegates all actual container creation to containerd, which in turn spawns a shim process per container that calls runc to perform the real namespace and cgroup setup. This layered handoff is exactly why Docker containers keep running even if the top-level Docker daemon is restarted.

Kubernetes via the Container Runtime Interface

Kubernetes nodes run a container runtime — commonly containerd or CRI-O — that speaks the Container Runtime Interface to the kubelet. Underneath both of those options, runc (or an OCI-compatible alternative, selected per pod via a runtime class) is what ultimately creates the isolated processes that make up every running pod across a cluster, at companies operating some of the largest Kubernetes fleets in the world.

Serverless and Function Platforms

Many serverless computing platforms build their rapid, per-request execution environments on the same OCI runtime foundation, sometimes using runc directly for trusted internal workloads and a sandboxed, VM-backed OCI runtime for customer-submitted, untrusted function code, illustrating the runtime-choice trade-off from Chapter 7 playing out at industry scale.

Continuous Integration Build Agents

Large-scale CI platforms that spin up thousands of ephemeral build containers per day rely on runc’s low startup overhead to make that model economically and practically viable — the difference between a build container starting in well under a second versus several seconds compounds enormously at that scale.

Industry Pattern — OCI Compliance as a Portability Guarantee Widely Adopted
Observation

Because runc, containerd, CRI-O, and Kubernetes all agree on the same OCI Runtime Specification, an organization can switch its underlying container runtime without rewriting how it builds images, defines Kubernetes manifests, or manages deployments.

Effect

This standardization is precisely why the broader ecosystem was able to introduce entirely new runtime implementations, including sandboxed, VM-backed alternatives, without fragmenting the tooling or workflows the rest of the industry had already built on top of the OCI standard.

12Frequently Asked Questions

Q1Is runc the same thing as Docker?

No — Docker is a full platform handling image building, networking, volumes, and the user-facing command-line experience. runc is one small component several layers underneath Docker, specifically responsible for the final step of actually creating and starting the isolated Linux process.

Q2Do I need to interact with runc directly to use containers?

No. The vast majority of engineers never call runc directly; it is invoked automatically by containerd, Docker, or Kubernetes behind the scenes. Understanding it is valuable for debugging and system design, not for everyday container usage.

Q3What is the difference between runc and containerd?

containerd is a higher-level daemon that manages the full container lifecycle — pulling images, managing storage, handling networking setup, and supervising containers over time. runc is the low-level tool containerd calls, through a per-container shim, to perform the actual namespace and cgroup creation for a single container at a single moment.

Q4Can runc run containers without any container image at all?

Yes. runc only needs a valid OCI bundle — a root filesystem directory and a config.json file — and does not inherently know or care whether that root filesystem originated from a pulled container image, a hand-built directory, or any other source.

Q5Why does Kubernetes support multiple different OCI runtimes instead of hardcoding runc?

Because the OCI Runtime Specification standardizes the interface, Kubernetes can let different workloads select different runtimes — typically runc for speed on trusted workloads, and a sandboxed VM-backed alternative for workloads needing stronger isolation — through Kubernetes’ runtime class mechanism, without changing how pods are otherwise defined or scheduled.

Q6Does runc provide networking to a container?

runc creates the network namespace a container lives inside, giving it isolation from the host’s networking, but it does not configure IP addresses, interfaces, or routes within that namespace by default. That configuration is handled separately, typically by a CNI plugin invoked by the higher-level orchestrator.

13Summary and Key Takeaways

What to Remember

  • runc is the reference implementation of the OCI Runtime Specification. It is the small, low-level tool that turns a prepared bundle (root filesystem plus config.json) into a genuinely isolated, running Linux process.
  • It does not invent new isolation technology. It carefully wires together existing Linux kernel primitives — namespaces for isolation, cgroups for resource limits, capabilities and seccomp for restricted privileges.
  • Its origin traces to the Open Container Initiative, formed in 2015 to prevent the container ecosystem from fragmenting into incompatible, vendor-specific formats.
  • The create-then-start split, and the shim-per-container model built around runc, are why running containers survive restarts of the higher-level Docker or containerd daemon.
  • Namespace-based isolation has a real ceiling. All containers on a host still share the same kernel, which is exactly why sandboxed, VM-backed OCI runtimes exist as an option for untrusted workloads.
  • runc is intentionally narrow in scope, leaving image management, networking configuration, and orchestration entirely to the tools built on top of it — by design, not by omission.
  • Because it implements an open, shared specification, Docker, containerd, CRI-O, and Kubernetes can all rely on runc — or swap in an OCI-compatible alternative — without changing how the rest of the platform works.