Logo
Overview
Docker Engine: Under the Hood — From Monolith to Modular Architecture

Docker Engine: Under the Hood — From Monolith to Modular Architecture

February 2, 2026
8 min read

Docker Engine: The TLDR

The Docker Engine is the core software that runs and manages containers. Think of it like ESXi for virtual machines—it’s the foundation everything else sits on.

The Docker Engine is modular by design, built from many small, specialized tools. Most of these are based on open standards maintained by the Open Container Initiative (OCI).

Key components:

  • Docker daemon – Manages images, builds, APIs, orchestration
  • containerd – Handles container lifecycle (start, stop, pause, rm)
  • runc – Creates and runs containers
  • Plugins – Networking, storage, logging

Together, these components work like a car engine — many small specialized parts creating something powerful and efficient.


Part 1: The Evolution — From Monolith to Modular

The Original Architecture (Early Docker)

When Docker was first released, it was simple but monolithic:

┌─────────────────────────────────┐
│ Docker Daemon (monolith) │
│ - Client │
│ - API │
│ - Runtime │
│ - Image builds │
│ - Much more │
└────────────┬────────────────────┘
┌────────────┐
│ LXC │ (Linux Container)
└────────────┘
┌────────────────────────┐
│ Linux Kernel │
│ - namespaces │
│ - cgroups │
└────────────────────────┘

The Problem: LXC was:

  1. Linux-specific (Docker wanted to be multi-platform)
  2. An external dependency (risky for core functionality)
  3. A limitation on development velocity

The First Refactor: Replacing LXC with libcontainer

Docker, Inc. developed libcontainer as a platform-agnostic replacement for LXC. It provided access to kernel container primitives (namespaces, cgroups) without external dependencies.

This replaced LXC in Docker 0.9.

But the real problem remained: The daemon was still monolithic.

The Big Refactor: Breaking Apart the Monolith

Over time, the monolithic daemon became problematic:

  1. Hard to innovate — One huge codebase
  2. Slower — More code = more overhead
  3. Not what the ecosystem wanted — The industry was moving toward modularity

Docker, Inc. embarked on a massive refactoring effort following the Unix philosophy: build small specialized tools that can be pieced together.

The result: container execution and runtime code was entirely removed from the daemon and refactored into specialized tools.


Part 2: The Modern Docker Engine Architecture

Docker Engine Architecture

The current architecture consists of:

Docker Daemon (dockerd)

Remains in the daemon:

  • Image management
  • Image builds
  • REST API
  • Authentication & security
  • Networking (core)
  • Orchestration

Removed from the daemon:

  • Container runtime code
  • Container lifecycle management
  • Low-level container creation

The daemon is now focused on higher-level operations, while lower-level operations are delegated to specialized tools.

containerd

Purpose: Manage container lifecycle operations (start, stop, pause, rm, etc.).

Originally designed to be lightweight and single-purpose, but has evolved to include:

  • Image pulls and pushes
  • Volume management
  • Networking
  • Storage plugins

Why it evolved: Projects like Kubernetes benefit from having these capabilities bundled together. But all extra functionality is modular and optional, so you can pick and choose what you need.

Key fact: containerd communicates with the daemon via gRPC (a high-performance RPC framework).

Status: Developed by Docker, Inc. and donated to the CNCF. Now a fully graduated CNCF project (stable and production-ready).

runc

Purpose: Create and run containers.

runc is the reference implementation of the OCI Container Runtime Specification. It’s essentially a lightweight CLI wrapper around libcontainer.

Key characteristics:

  • Single, focused purpose: create containers
  • Fast and efficient
  • Bare-bones (no fancy features)
  • Standalone tool (you can use it independently)
  • Heavily used in the industry

When you create a container, containerd forks a new runc instance for each container. Once the container is created, the parent runc process exits—allowing you to run hundreds of containers without hundreds of runc processes.

Plugins

The Docker Engine also supports pluggable components:

  • Networking plugins – Custom networking backends
  • Storage plugins – Custom storage drivers
  • Logging plugins – Custom log destinations
  • Volume plugins – Custom volume management

Part 3: Creating a Container — End-to-End Flow

Let’s trace what happens when you run:

Terminal window
docker container run --name ctr1 -it alpine:latest sh

Step 1: Docker CLI to Docker Daemon

The Docker CLI converts your command into an API payload and POSTs it to the Docker daemon.

Where does it send it?

  • On Linux: /var/run/docker.sock (Unix socket)
  • On Windows: \pipe\docker_engine (named pipe)

Step 2: Docker Daemon to containerd

The daemon receives the request but doesn’t contain any code to create containers.

Instead, it calls containerd via a gRPC API with the request.

Step 3: containerd to runc

containerd cannot actually create containers either. It’s a manager, not a creator.

It does three things:

  1. Converts the Docker image into an OCI bundle
  2. Calls runc with this bundle
  3. Monitors the container’s lifecycle

Step 4: runc Creates the Container

runc interfaces with the Linux kernel to assemble all the container constructs:

  • Namespaces (PID, network, mount, etc.)
  • cgroups (resource limits)
  • Chroot/pivot_root (filesystem isolation)

runc then forks a process to start the container and immediately exits.

containerd
├─ forks runc
│ └─ runc
│ ├─ Creates namespaces, cgroups
│ ├─ Starts container process
│ └─ exits (parent of container becomes shim)
└─ Container running (parent is shim, not runc)

Step 5: The Shim Takes Over

Once runc exits, the containerd-shim becomes the container’s parent process.

The shim’s responsibilities:

  • Keeps STDIN/STDOUT streams open (so daemon restarts don’t kill the container)
  • Reports exit status back to the daemon
  • Acts as a bridge between the container and containerd

Visualization:

Container Creation Flow


Part 4: The Genius of Daemonless Containers

Here’s why this architecture is brilliant:

The Old Problem

In early Docker, the daemon contained all runtime code. This meant:

  • Starting/stopping the daemon = killing all containers on that host
  • Every daemon upgrade = all containers die
  • Production nightmare!

The New Solution: Daemonless Containers

By removing runtime code from the daemon and using containerd + runc + shim:

  1. The daemon can be restarted without killing containers
  2. You can upgrade Docker without downtime
  3. Containers are decoupled from the daemon
Old model:
Daemon dies → All containers die (bad)
New model:
Daemon dies → Containers keep running (good)
Daemon restarts → Attaches to existing containers

This was a game-changer for production environments. It’s why Docker upgrades are no longer scary.

Note

The shim’s crucial role:

The shim is what makes daemonless containers possible. By becoming the container’s parent after runc exits, it:

  • Keeps I/O streams alive during daemon restarts
  • Manages the container’s lifecycle independently
  • Reports status back when the daemon reconnects

Part 5: Binary Layout on Linux

On a Linux system, the Docker Engine consists of separate binaries:

Terminal window
/usr/bin/dockerd # Docker daemon
/usr/bin/docker-containerd # containerd
/usr/bin/docker-containerd-shim # Shim
/usr/bin/docker-runc # runc

You can see these running with ps:

Terminal window
ps aux | grep docker

Which ones are running depends on your containers:

  • dockerd – Always running
  • docker-containerd – Always running (daemon mode)
  • docker-containerd-shim – One per running container
  • docker-runc – Only briefly during container creation

Example with 2 running containers:

dockerd # 1 daemon
docker-containerd # 1 manager
docker-containerd-shim # 2 shims (one per container)
docker-containerd-shim #
container process 1 # Your app
container process 2 # Your app

Part 6: Data Flow — The Complete Picture

[Include your complete data flow diagram here]

Let’s understand the request journey:

Incoming Request

User: docker container run ...
Docker CLI
REST API payload
POST /var/run/docker.sock

Processing

Docker Daemon
├─ Validates request
├─ Checks authentication
├─ Manages image
└─ Calls containerd via gRPC
containerd
├─ Creates OCI bundle
├─ Forks runc
│ └─ runc creates container
└─ Starts shim as container parent

Container Running

containerd-shim (parent)
Container process (your app)
├─ PID namespace
├─ Network namespace
├─ Mount namespace
└─ cgroup limits

Container Output

Container process outputs
shim captures (STDIN/STDOUT)
containerd monitors
Daemon logs
Docker CLI / Docker API

Part 7: Why This Architecture Matters

Modularity

Each component has a single responsibility:

  • runc – Create containers (does it really well)
  • containerd – Manage lifecycle
  • daemon – Higher-level orchestration

If you want to swap components:

  • Use cri-o instead of containerd? Possible (with work)
  • Use kata runtime instead of runc? Yes, it’s OCI-compliant
  • Use containerd in Kubernetes? Yes, it’s a first-class runtime

Stability

Running containers are decoupled from the daemon:

  • Daemon crashes? Containers keep running
  • Daemon upgrades? No downtime
  • Daemon restarts? Reconnects to running containers

Performance

Each tool is optimized for its job:

  • runc is fast at container creation
  • containerd is efficient at lifecycle management
  • daemon focuses on orchestration

Open Standards

By following OCI standards:

  • Docker images run on other runtimes
  • Tools can be mixed and matched
  • No vendor lock-in

Part 8: What’s Still in the Daemon?

You might wonder: with all that removed, what’s left?

At the time of writing:

Still in the daemon:

  • Image management (pull, push, tag, rm)
  • Image builds (docker build)
  • REST API endpoints
  • Authentication & security
  • Core networking
  • Orchestration (Swarm)
  • Plugin management

Removed to other components:

  • Container runtime (runc)
  • Container lifecycle (containerd)
  • Low-level I/O management (shim)

Over time, even more might be modularized. The daemon’s role is increasingly orchestration and higher-level logic.


Conclusion: Mastery Comes From Understanding

You can use Docker without knowing any of this. It just works.

But if you want learn how the system works, you need to understand:

  1. Why the architecture evolved — Monolith to modular
  2. How containers are created — The step-by-step flow
  3. What each component does — daemon, containerd, runc, shim
  4. Why daemonless containers matter — Production reliability
  5. How standards enable portability — OCI and the ecosystem

Further Reading