Docker Demystified - Part 4: The Runtime Confusion — Engines, High-Level, and Low-Level Runtimes

The “Runtime” Confusion

This is where terminology gets messy. Everyone calls different things a “runtime.”


The Problem

"Container Runtime" — but which one?

  Docker?        ← Sometimes called a runtime
  containerd?    ← Called a "high-level runtime"
  runc?          ← Called a "low-level runtime"

All three get called "runtime" in different contexts.

The Solution: Think in Layers

TermWhat It Actually MeansExamples
Container EngineFull platform with CLI, image building, UXDocker, Podman
High-Level RuntimeManages container lifecycle, images, storagecontainerd, CRI-O
Low-Level Runtime (OCI)Spawns processes with namespaces/cgroupsrunc, crun, youki, gVisor
Language RuntimeExecutes application codeJVM, Node.js, .NET Core runtime

Note: In the context of containers, we focus on container runtimes (engine, high-level, low-level). Language runtimes like JVM or Node.js run inside the container to execute your application code. You can ignore them when discussing container architecture, but it’s useful to know they exist so you don’t confuse “container runtime” with “language runtime.”


Low-Level Runtime: The Actual “Runtime”

A low-level runtime does exactly one thing:

Input:  OCI bundle (config.json + root filesystem)
Output: Running Linux process with isolation

That's it. Nothing else.

It doesn’t know about:

  • Images or registries
  • Networking configuration
  • Storage drivers
  • Logging

runc is the reference implementation. But there are alternatives:

RuntimeWritten InKey Feature
runcGoReference OCI implementation
crunCFaster, lower memory footprint
youkiRustMemory-safe, modern
gVisorGoSandboxed kernel for extra isolation
Kata ContainersGoEach container gets a micro-VM

High-Level Runtime: The Manager

A high-level runtime handles everything the low-level runtime doesn’t:

containerd's responsibilities:
├── Pull images from registries
├── Unpack image layers
├── Manage container lifecycle (create → start → stop → delete)
├── Handle storage (snapshots, overlay filesystem)
├── Set up networking
├── Stream logs
└── Call runc when it's time to actually spawn the process

containerd is the most common high-level runtime. CRI-O (from Red Hat) is the alternative, designed specifically for Kubernetes.


The Complete Picture

┌─────────────────────────────────────────────────────┐
│              CONTAINER ENGINE                       │
│         (Docker, Podman, nerdctl)                   │
│                                                     │
│  • User-facing CLI                                  │
│  • Image building (Dockerfile → image)              │
│  • Developer experience                             │
│  • docker-compose / orchestration                   │
└────────────────────┬────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────┐
│           HIGH-LEVEL RUNTIME                        │
│         (containerd, CRI-O)                         │
│                                                     │
│  • Image management (pull, store)                   │
│  • Container lifecycle                              │
│  • Storage (snapshots, overlayfs)                   │
│  • Networking setup                                 │
│  • Calls low-level runtime                          │
└────────────────────┬────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────┐
│           LOW-LEVEL RUNTIME (OCI)                   │
│      (runc, crun, youki, gVisor, Kata)              │
│                                                     │
│  • Reads OCI bundle (config.json + rootfs)          │
│  • Creates namespaces                               │
│  • Sets up cgroups                                  │
│  • Spawns the actual process                        │
│  • Exits immediately                                │
└────────────────────┬────────────────────────────────┘
                     │
                     ▼
              ┌─────────────┐
              │  CONTAINER  │
              │  (process)  │
              └─────────────┘

Why This Matters

For Debugging

When something breaks, knowing the layers helps:

Container won't start?
├── Is the image corrupted? → High-level runtime issue
├── Can't create namespace? → Low-level runtime / kernel issue
├── Network not working? → High-level runtime / Docker networking
└── Process crashes immediately? → Application issue

For Understanding Kubernetes

Kubernetes talks to the high-level runtime via CRI (Container Runtime Interface):

kubelet → CRI API → containerd or CRI-O → runc

Kubernetes doesn’t care about Docker’s CLI or image building. It only needs lifecycle management.


Choosing a Low-Level Runtime

RuntimeWhen to Use
runcDefault choice, battle-tested, reference implementation
crunWhen you need faster startup or lower memory (OpenShift default)
youkiWhen you want memory safety guarantees (Rust)
gVisorWhen you need extra security isolation (untrusted workloads)
KataWhen you need VM-level isolation with container UX

For most use cases, runc is perfectly fine. The alternatives optimize for specific scenarios.


Single-Sentence Truth (Memorize This)

containerd manages containers; runc creates containers.

That one sentence resolves 90% of confusion.


Key Takeaways

  1. “Runtime” is overloaded - Always clarify: engine, high-level, or low-level
  2. Low-level = process spawner - Just creates the isolated process
  3. High-level = lifecycle manager - Handles images, storage, networking
  4. Container engine = user interface - CLI, UX, image building
  5. runc is the standard - Reference OCI implementation, used by default
  6. containerd is the standard manager - Used by Docker, Kubernetes, and clouds

The OCI Standard: Why This All Works

OCI (Open Container Initiative) - Linux Foundation project (2015) that standardizes three things:

  1. Runtime Spec - How to run containers (config.json + rootfs → process)
  2. Image Spec - How images are structured (layers, manifests, config)
  3. Distribution Spec - How registries work (push/pull, auth)

Why it matters: Docker donated runc as the reference implementation, ensuring all OCI runtimes are interchangeable.

# Same image works everywhere
docker build -t myapp .
podman run myapp        # Works
nerdctl run myapp       # Works
kubectl run myapp       # Works

Without OCI: Vendor lock-in. With OCI: Build once, run anywhere.

Comments