How Docker Works: A Deep Dive

1. What Docker Is and Its Core Concepts

1.1 What is Docker

Docker is an open platform for developing, shipping, and running applications. Using operating-system-level virtualization, it packages an application together with all of its dependencies into a standardized, lightweight, isolated unit called a container. The same container image can run consistently on any OCI-compliant Linux host without worrying about the specific configuration of the underlying machine.

Unlike virtual machines, Docker containers share the host operating system’s kernel and do not need to carry a complete, independent Guest OS per instance. This compresses container startup time from minutes down to milliseconds or seconds, and reduces memory overhead from hundreds of MB to a few MB.

The Docker architecture has completed a significant decoupling and standardization. Docker is no longer a single do-everything dockerd daemon; it has evolved into a clear separation-of-duties pipeline:

docker CLI  →  dockerd  →  containerd  →  containerd-shim-runc-v2  →  runc
  (client)     (API gateway) (lifecycle)        (process stub)          (OCI runtime)

Each layer has a clear boundary:

  • docker CLI: the user interaction interface, backward compatible with the traditional docker build/run/push commands.
  • dockerd: a REST API gateway that handles image building (delegated to BuildKit), image distribution, and network/volume orchestration. It no longer manages container processes directly.
  • containerd: the industry-standard container runtime daemon, responsible for image transfer and storage, container lifecycle supervision, and snapshot management. After Docker donated containerd to CNCF in 2017, containerd grew independently into core infrastructure of the cloud-native ecosystem.
  • runc: the reference implementation of the OCI runtime spec, which creates and starts the container process via Linux Namespace + Cgroups, then exits immediately and hands the process over to containerd-shim.

This architectural split is not merely an exercise in engineering aesthetics—it lets each Docker component be replaced independently. After Kubernetes removed dockershim in 2020 and talked directly to containerd, dockerd became an optional middle layer in production, yet developers still use the full Docker toolchain to build and test images.

1.2 The core concept map

To understand Docker you need to know the precise meaning and relationships of these key concepts:

ConceptDescriptionAnalogy
ImageA read-only template that follows the OCI Image Spec, containing the complete filesystem and metadata needed to run an application. An image is composed of multiple stacked read-only layers.An executable file
ContainerA running instance of an image, with a writable layer added on top of the image layers to form the complete runtime view.A running process
RegistryThe storage and distribution hub for images, following the OCI Distribution Spec. Docker Hub is the largest public registry.A Git remote repository (e.g., GitHub)
DockerfileA declarative script defining the build steps of an image. BuildKit is the default build engine, supporting parallel builds and advanced cache mounts.A Makefile / build recipe
dockerdThe Docker daemon, providing a REST API gateway responsible for image-build scheduling, image distribution, and network/volume orchestration.An API server
containerdA CNCF graduated project and industry-standard container runtime, managing image transfer, container lifecycle, and snapshot storage.A container runtime engine
runcThe OCI Runtime Spec reference implementation; a lightweight tool that creates the container process via Namespace + Cgroups and exits after creation.A process starter
BuildKitThe next-generation image build engine (default since Docker 23.x+), supporting parallel builds, remote caching, secret injection, and multi-platform images.An incremental compiler

2. Docker’s Underlying Implementation

Docker is often described as “shipping containers”, but a more accurate technical analogy is an organic combination of Linux kernel features. Docker itself invented none of the underlying technology—its core value lies in combining Linux’s three existing capabilities (Namespace, Cgroups, UnionFS) into a smooth developer experience. This section goes deep into the technical details of each component.

2.1 Namespace—Resource Isolation

2.1.1 Understanding Namespace in one sentence

Imagine an apartment building: each resident is in their own room and can only see their own furniture, their own window, and their own door number. They don’t know who lives next door or what is in that room—even though they share the same building’s concrete and steel with everyone else.

The processes inside a container live in such an “apartment”. In Linux this apartment is called a Namespace—it lets a process see only the system resources that belong to it, and remain completely unaware of anything in other Namespaces. A single host can run hundreds of containers at once, each thinking it is the sole user, with no interference.

The Linux kernel provides 8 kinds of Namespace, each isolating a different class of system resources. Let’s go through them one by one.

2.1.2 PID Namespace—Process ID isolation

Every time a program starts, the system assigns it a numeric ID—that’s the PID. Normally a PID can’t repeat across the whole system; if container A has a process with PID=1, container B can’t have another process with PID=1.

PID Namespace breaks that restriction. It gives each container its own independent PID numbering space:

  • Inside the container, your main process sees itself as PID 1
  • On the host, the very same process is seen as another number, say PID 48921
  • The same process has two sets of numbers: one internal to the container, one for the host

This is why running ps aux inside a container shows only the container’s own few processes, while running it on the host shows hundreds of processes across all containers—the two views each see their own world.

2.1.3 Mount Namespace—Filesystem isolation

Mount Namespace controls “which files and directories are visible”.

Each container has its own mount-point view. The container’s /proc, /tmp are completely independent from the host:

  • The container’s top command reads the container’s own /proc and only sees the container’s processes
  • Mounting an external disk inside the container is invisible to the host and other containers
  • At container startup, its entire “root directory” is switched to the filesystem built from the image—the container thinks it is operating on the root directory of a complete machine, but that root directory is actually the overlay of image layers
2.1.4 Network Namespace—Network isolation

Network Namespace gives each container an independent, complete network stack: its own network interface, its own IP, its own port space, and its own firewall rules.

This means container A and container B can both listen on port 80—without conflict, because they live in their own Network Namespaces.

In practice, Docker creates a pair of virtual cables (a veth pair): one end is plugged into the container, the other into the host’s virtual switch (the docker0 bridge). When the container accesses the external network, traffic leaves the container’s NIC, passes through the host’s bridge, and then exits via the host’s physical NIC—just like every apartment having its own router but ultimately sharing one fiber line.

2.1.5 Other Namespaces
NamespaceIsolatesWhy it’s needed
UTSHostnameEach container can have its own hostname without changing the host’s name when the container’s name changes
IPCInter-process communication (shared memory, semaphores, etc.)Prevents a process in container A from accidentally reading container B’s shared memory
UserUser ID mappingroot inside the container can map to an ordinary user on the host. Even if the container is compromised, the attacker doesn’t get real root
Cgroupcgroup info viewThe container only sees its own resource limits, not how much other containers are using

Namespace answers the question “what can I see”. But it doesn’t answer “how much can I use”—that is taken over by Cgroups in the next section.

2.2 Cgroups—Resource Limits and Accounting

If Namespace answers what can I see, then Cgroups (Control Groups) answers how much can I use. Cgroups organize processes into hierarchical groups and impose hard limits on CPU, memory, disk I/O, and network bandwidth for each group.

2.2.1 Cgroups v2: the unified hierarchy model

By 2026, all mainstream Linux distributions default to cgroups v2 (fully ready since kernel 5.2+), and Docker naturally runs entirely on top of cgroups v2.

Core features of cgroups v2:

  • Unified hierarchy: all controllers are mounted under a single hierarchy tree (/sys/fs/cgroup/), giving a clear, consistent management model.
  • No internal process constraint: only leaf nodes may contain processes, ensuring resource control is unambiguous.
  • Pressure Stall Information (PSI): the kernel exposes /proc/pressure/cpu, /proc/pressure/memory, and /proc/pressure/io interfaces to precisely quantify resource congestion. This is the core signal for modern container monitoring and autoscaling.

The data flow of cgroups v2:

userspace process → cgroupfs (/sys/fs/cgroup/) → kernel cgroup unified hierarchy → resource scheduler

Each time Docker (via containerd) creates a container, it creates a corresponding child group in cgroupfs and writes the container process PID into cgroup.procs. Below are the modern interfaces of each resource controller in cgroups v2.

2.2.2 CPU limits

Docker controls container CPU usage in two dimensions through the cgroups v2 CPU controller:

(1) CPU weight (cpu.weight)

cgroups v2 uses cpu.weight for weight-based control. The default weight is 100, ranging from 1 to 10000. Docker’s --cpu-shares parameter maps automatically to this interface. When the system’s CPU is contended, the scheduler allocates CPU time proportionally to the weights.

For example: container A has weight 200, container B has weight 100. When they compete for CPU, A gets about 66.7% of the CPU time and B gets about 33.3%. Weights only take effect when resources are contended; idle resources can still be used by any container.

(2) CPU bandwidth limit (cpu.max)

cgroups v2 controls CPU bandwidth through the unified file cpu.max, formatted as $MAX $PERIOD:

  • Docker parameter --cpus=1.5 → writes cpu.max as 150000 100000, i.e. at most 150ms of CPU time per 100ms period, equivalent to 1.5 cores.
  • Docker parameter --cpus=0.5 → cpu.max = 50000 100000, equivalent to 0.5 cores.
  • When unlimited, cpu.max = max 100000.
  • Docker parameter --cpuset-cpus="0,2" → binds the process to specific cores via cpuset.cpus.
2.2.3 Memory limits

cgroups v2 unifies the memory control interfaces:

cgroups v2 fileDocker parameterMeaning
memory.max--memoryHard limit on physical memory. Exceeding it triggers the OOM Killer
memory.swap.max--memory-swapTotal limit on physical memory + swap
memory.high--memory-reservationSoft limit; the kernel throttles allocation beyond it but won’t directly OOM. Combined with PSI, enables graceful memory-pressure awareness
memory.low—Best-effort minimum memory threshold, preferentially not reclaimed under global memory pressure

Key mechanism: when a container’s memory usage exceeds memory.max, the kernel OOM Killer steps in and kills the process consuming the most memory in the container. The Docker daemon can restart the container on demand.

2.2.4 Block device I/O limits

cgroups v2 limits the read/write rate of block devices through the IO controller (the unified interface file io.max), formatted as device-number rbps=X wbps=X riops=X wiops=X:

  • Docker’s --device-read-bps / --device-write-bps → writes to the rbps / wbps fields of io.max
  • Docker’s --device-read-iops / --device-write-iops → writes to the riops / wiops fields of io.max

In addition, the cgroups v2 IO controller supports weight-based proportional allocation (io.weight) and IO latency-target control (io.latency), providing finer-grained disk QoS for different workloads.

2.3 OverlayFS—Layered Images and Storage Principles

To understand why Docker images are both fast and space-efficient, you need to understand two things: how an image is stored, and how a running container reads and writes files. Both are built on the Linux kernel’s OverlayFS—a “union filesystem”.

2.3.1 Seeing “layers” from a Dockerfile

Let’s start with the simplest Dockerfile and use it to connect the whole storage story:

FROM ubuntu:24.04                        # Layer 1: grab a ready-made Ubuntu
RUN apt-get install -y python3           # Layer 2: install Python
COPY mycode.py /app/                     # Layer 3: drop in your own code

After each instruction finishes building, it becomes a read-only static snapshot—that is a “layer”. Three instructions = three layers, like three sheets of transparent film stacked together:

Layer 3  [ mycode.py ]          ← your application code
Layer 2  [ /usr/bin/python3 ]    ← files newly installed by apt
Layer 1  [ /bin, /usr, /lib... ] ← the complete Ubuntu system

The final image is the overlay of these three layers: it looks like a complete filesystem (system files + Python + your code), but is actually three independent, read-only layers stacked together.

2.3.2 How a container reads and writes these read-only layers

The above describes the image—static and read-only. Now consider the container—the running image—which needs to be able to write files.

OverlayFS’s approach is: on top of this stack of read-only layers, it adds one extra writable layer. The structure looks like this:

┌─────────────────────────────┐
│  Writable layer (container-specific) │ ← all modifications go here
├─────────────────────────────┤
│  Layer 3  mycode.py  [read-only]   │
├─────────────────────────────┤
│  Layer 2  python3    [read-only]   │
├─────────────────────────────┤
│  Layer 1  ubuntu     [read-only]   │
└─────────────────────────────┘

The process running inside the container sees the complete picture formed by overlaying these four layers. It doesn’t need to know the layers exist underneath—it feels just like a normal filesystem.

Let’s look at how “reading”, “modifying”, and “deleting” files works inside the container.

(1) Reading a file

The rule is simple: search from the top down; use the first match.

If the container wants to read /app/mycode.py, it first checks the writable layer. If it’s there, use it directly. If not, keep going down; it’s found in Layer 3, so the Layer 3 copy is used. The container process has no idea which layer the file actually came from.

(2) Modifying a file—Copy-on-Write

Files in the lower layers are read-only and can’t be changed directly. So what if the container wants to modify /app/mycode.py?

OverlayFS’s answer: if the file comes from a lower layer, first copy the entire file into the writable layer, then modify the copy.

This process is called “copy-on-write”: leave it alone and it rests quietly in the lower layer; touch it and a copy is triggered. For small files (most application scenarios), the copy is nearly imperceptible. If the file is very large (e.g., a database’s data file), the first write incurs a one-time I/O cost—in that case a Docker Volume is recommended, writing data directly to a host directory and bypassing the OverlayFS layer.

(3) Deleting a file—the “whiteout” marker

Lower-layer files are read-only and physically can’t be deleted. Yet the container process must “see” the file disappear. How?

OverlayFS uses a clever trick: it creates a same-named marker file (called a whiteout) in the writable layer, which signals “if there is a same-named file below, pretend it doesn’t exist”.

As an analogy: it’s like covering content below with correction fluid on one sheet of transparent film—the writing underneath is still there, but in the picture you see it has disappeared. Deleting a directory works the same way: an “opaque” marker is created, hiding all of the lower layer’s contents for that directory.

2.3.3 Image distribution: what happens when you pull an image

When you run docker pull ubuntu:24.04, what actually happens is:

  1. Download layers: the Registry sends each layer of the image (as a compressed archive) and stores it under the host’s /var/lib/docker/overlay2/ directory.
  2. Layers are independent directories: after each layer is decompressed it becomes an independent directory containing exactly the files of that layer.
  3. Overlay at container start: when you docker run, Docker creates an empty writable-layer directory, then issues a Linux command to “stack” these directories together:
mount -t overlay overlay \
  -o lowerdir=Layer1:Layer2:Layer3,upperdir=writable-layer,workdir=work-dir \
  merged-view
  1. Container sees the merged view: the container process’s “root directory” is this merged view. The process thinks it is operating on an ordinary directory, but underneath it is the result of multiple layers overlaid.

Key point: multiple containers can share the same set of read-only layers at the same time, and each container’s writable layer is independent. Starting a new container only requires creating an empty writable-layer directory + one mount command—almost instantaneous.

2.3.4 Layer caching and build acceleration

The principles above also explain why docker build can be so fast:

  • Each Dockerfile instruction corresponds to a layer, and each built layer gets a unique identifier.
  • On the next build, if an instruction’s input (instruction text + context files) is exactly the same as last time, the existing layer is reused directly and execution is skipped.
  • Once a layer’s cache is invalidated, that layer and all subsequent layers must be rebuilt.

So the best practice for a Dockerfile is: put things that change rarely at the top, and things that change often at the bottom.

FROM ubuntu:24.04                    # almost never changes → always cache hit
RUN apt-get install -y python3       # rarely changes → basically cache hit
COPY requirements.txt /app/          # dependency declaration changes occasionally
RUN pip install -r /app/requirements.txt  # only reinstalls when the declaration changes
COPY . /app/                         # code changes daily → only this layer rebuilds often

The most common everyday scenario: you only changed code, not dependencies → only the last two layers rebuild, everything above hits cache, and the build finishes in seconds.

3. Summary

Docker’s working principles can be summarized as one clear standardized toolchain and a set of unchanging kernel-level fundamentals:

Standardized toolchain:

docker CLI  →  dockerd (API gateway)  →  containerd (lifecycle)  →  runc (OCI runtime)
BuildKit (parallel build)     Distribution (OCI distribution)     Snapshotter (snapshot storage)

Docker has evolved from a single “all-powerful daemon” into a set of clearly-responsible, replaceable standard components. containerd became the industry standard for container runtimes independent of Docker, runc strictly follows the OCI Runtime Spec, and image distribution follows the OCI Distribution Spec. Developers still use the familiar docker build/run/push commands, but underneath lies an open, composable ecosystem.

Kernel-level fundamentals (unaffected by architectural evolution):

  1. Namespace: defines the boundary of the resource view, answering “who am I, what can I see”. The 8 Namespaces each play their role, together building each container’s independent world.
  2. Cgroups v2: defines the boundary of resource usage, answering “how much can I use”. The unified hierarchy model combined with PSI pressure awareness provides precise resource control and monitoring signals.
  3. OverlayFS: defines the filesystem boundary, answering “where are my files and how do they change”. Copy-on-write + layered sharing + pluggable snapshotters underpin the blazing-fast build and distribution of modern container images.

References:

  • Linux Kernel Documentation: Control Groups v2 (Documentation/admin-guide/cgroup-v2.rst)
  • Linux Kernel Documentation: Overlay Filesystem (Documentation/filesystems/overlayfs.rst)
  • Linux Kernel Documentation: Namespaces (man 7 namespaces)
  • OCI Specifications: Runtime Spec, Image Spec, Distribution Spec (github.com/opencontainers)
  • containerd Documentation: Architecture & Snapshotters (github.com/containerd/containerd)
  • BuildKit Documentation (github.com/moby/buildkit)