What Docker Actually Adds to Linux Isolation Primitives
The Linux kernel had namespaces and cgroups years before Docker existed. Why writing raw syscalls wasn't enough.
Before you start: this article assumes you have run a few shell commands, know that a process is one running instance of a program, and have typed docker run at least once. Every Linux term is explained where it first appears.
Think of a container as a hotel room. You share the building (the machine) with other guests, but your room has its own door, its own bathroom, and a meter that limits how much water you can use. Nothing in the building was invented for you. The hotel just arranges existing walls and plumbing so that your room feels private.
Docker didn’t invent container isolation.
A container is an ordinary program run so that it sees only its own files, its own network, and a limited share of the machine’s resources, as if it had a computer to itself. Linux can do this without Docker.
The kernel is the core of the operating system: the part that controls the hardware, the memory, and the running programs on behalf of every other program. A process is one running instance of a program, and Linux gives each process a number called a process ID (PID).
By the late 2000s, the Linux kernel already had the building blocks for running an isolated process:
- Namespaces (
clone(2)flags likeCLONE_NEWPID,CLONE_NEWNET, andCLONE_NEWNS) to give processes their own isolated view of system trees. A namespace gives a process its own private view of one kind of system resource, such as the list of process IDs, the network interfaces, or the mounted filesystems.clone(2)is a system call, which is a request a program makes to the kernel for something it cannot do itself, such as creating a process. TheCLONE_NEW...flags ask the kernel to create new namespaces for that process. The(2)is the section of the Linux manual where system calls are documented. - Control Groups (cgroups) to cap CPU cycles, RAM, and block I/O. A cgroup is a kernel feature that sets limits on how much processor time, memory, and disk reading and writing (block I/O) a group of processes may use.
pivot_rootandchrootto swap root filesystems. Both are system calls that change what a process treats as the top-level folder/, so it cannot see files outside that folder. A root filesystem is the full directory tree a program expects to find, starting at/.
Yet almost nobody was deploying microservices in containers back then. Microservices means building one application as many small programs that talk to each other over a network.
The reason was practical ergonomics: stitching those primitives together by hand was a nightmare. Setting up an isolated environment meant calling those system calls yourself, mounting virtual /proc filesystems manually, setting up virtual ethernet pairs with raw ip link commands, and tweaking pseudo-files inside /sys/fs/cgroup/.
Some of those terms need unpacking. Mounting attaches a filesystem to a folder so its contents appear there. /proc and /sys are special folders where Linux exposes kernel information as files, and writing to some of them changes kernel settings. A virtual ethernet pair (veth) is two software network ports joined like the two ends of a cable: whatever enters one end comes out the other. ip link is a command-line tool for managing network interfaces.
Miss a single cleanup step in a bash script (a text file of shell commands that run in order), and your process leaked network interfaces into the host namespace or left root mount points exposed. The host is the real machine, or the operating system, that runs the containers. Leaking means leaving stray network devices or mounts behind on it.
Docker succeeded because it solved the distribution and developer experience problem. It took the disconnected kernel interfaces and wrapped them in a consistent runtime model. A runtime is the software that actually starts and manages containers, and distribution means getting software onto other machines.
The Raw Primitive Gap
To see why the abstraction matters, contrast what the kernel expects with what Docker presents to the user. It is like cooking from raw ingredients versus ordering a ready meal: the left side is the manual steps, and the right side is the one Docker command that does them all.
RAW LINUX KERNEL CALLS DOCKER RUNTIME ABSTRACTION
+---------------------------------------+ +---------------------------------------+
| sys_clone(CLONE_NEWPID | NEWNET...) | --> | docker run -d --name web -p 80:80 \ |
| /sys/fs/cgroup/memory/my_app/ | | -m 512m nginx |
| mount -t overlay ... | | (Single declarative execution) |
+---------------------------------------+ +---------------------------------------+
In the Docker command, -d runs the container in the background, --name web names it, -p 80:80 connects port 80 on the host to port 80 in the container, -m 512m caps its memory at 512 megabytes, and nginx is the image to run. nginx is a popular web server program, and an image is the packaged software the container starts from (more on images below).
Without Docker or a container manager, running an isolated web server requires orchestrating five separate subsystems:
- Process Isolation: Calling
clone(2)orunshare(2)with half a dozen flags to detach PID, mount, IPC, and network trees.unshare(2)is a system call that detaches the calling process from namespaces it shares with others. PID means process ID numbers, and IPC (inter-process communication) means shared memory and message queues. - Filesystem Pivoting: Unpacking a rootfs tarball, setting up
tmpfsmounts, and executingpivot_root(2)so the process can’t escape into the host root. A tarball is a single archive file (.tar) that bundles many files, andtmpfsis a filesystem kept in memory instead of on disk. - Resource Guardrails: Writing memory limits into
/sys/fs/cgroup/memory.maxand writing the target process ID intocgroup.procs. In plain terms, you write a number into one file and the process’s ID into another, and the kernel reads those files as instructions. - Network Plumbing: Giving the container a working network connection and making it reachable from outside. In plain terms, this takes five steps:
- Create a
vethpair, the two linked virtual ports described earlier. - Move one end into the container’s network namespace, so the container owns it.
- Attach the other end to a bridge on the host. A bridge is a software network switch that connects several virtual ports.
- Give the container an IP address from a subnet. An IP address identifies a machine on a network, and a subnet is a block of related addresses.
- Add an
iptablesrule for port forwarding.iptablesis the Linux firewall and packet-rewriting tool. DNAT (destination network address translation) rewrites a packet’s destination address, which is how traffic arriving at a port on the host gets sent on to the container. A port is a numbered door on an IP address that a program listens on.
- Create a
- Artifact Packaging: Figuring out how to ship dependencies and runtime binaries in a repeatable bundle. Dependencies are the other pieces of software a program needs to run, and binaries are compiled, ready-to-run program files.
Docker packaged those responsibilities into four core components. These are the four numbered sections below. They are a different grouping from the five jobs above, not a one-to-one match, so do not expect job 1 to line up with section 1.
1. The Portable Root Filesystem (OCI Image Format)
A shipping container holds goods in a standard box that any ship or truck can carry. A Docker image does the same for software: the program plus everything it needs, in one standard package. OCI (Open Container Initiative) is the published standard for that image format.
Before container images became standard, distributing software meant building RPMs, Debian packages, or shipping fat tarballs. RPM and Debian packages are the install formats used by the Red Hat and Debian families of Linux. If the host machine had a mismatched glibc version or was missing libssl.so.1.1, the deployment broke on startup. glibc is the standard C library most Linux programs depend on, and libssl.so.1.1 is a shared library file (a file of code that many programs load) used for encryption. If the machine has a different version than the program expects, the program fails.
Docker paired the root filesystem with a JSON manifest, a small text file that describes what the image contains and how to run it. The image contains:
- The exact root directory tree (
rootfs) required by the app. - Every shared library, binary, and configuration file.
- Metadata describing entrypoints, environment defaults, and exposed ports. The entrypoint is the command that starts when the container runs, and environment variables are named settings passed to the program.
Because the process runs against its own bundled rootfs rather than the host’s /usr/lib (the folder where Linux keeps shared libraries for installed programs), it runs with identical behavior on a local laptop or an AWS instance running a completely different Linux distribution. A distribution is a packaged version of Linux, such as Debian or Fedora, and an AWS instance is a virtual server rented from Amazon Web Services.
2. Copy-on-Write Layering via OverlayFS
Shipping a full root filesystem on every minor code change would slow deployment and waste disk space.
Imagine a printed page covered by clear plastic sheets. You write on the top sheet and never touch the page underneath. Layers work like those sheets. Copy-on-write means a file is copied only at the moment someone first changes it. OverlayFS is the Linux filesystem that stacks layers so they appear as one folder.
Docker solved this with layered storage drivers (primarily OverlayFS). When building an image, each instruction produces an immutable layer identified by a SHA-256 digest. Immutable means never changed after creation. A SHA-256 digest (a hash) is a fixed-length fingerprint computed from content, so identical content always gets the same fingerprint. If five different services build on debian:bookworm-slim (the name and tag of a small Debian base image), they all share the exact same base layer on disk and in the page cache, which is the memory the kernel uses to keep recently read file data:
+---------------------------------------------------------------+
| Container Writable Layer (Read/Write Delta) |
+---------------------------------------------------------------+
| Layer 3: Application Binary (12 MB) |
+---------------------------------------------------------------+
| Layer 2: Go Runtime / Libraries (110 MB) |
+---------------------------------------------------------------+
| Layer 1: Debian Base OS (55 MB) [Shared across all containers]|
+---------------------------------------------------------------+
The sizes in this diagram are illustrative example values, not measurements. The example assumes the app was written in Go, a programming language, so layer 2 holds the Go runtime and libraries. The top box is the container’s own writable layer, where its changes are stored.
The container only writes changes to its thin top layer. The heavy base remains read-only and shared. If a program edits a file that lives in a lower layer, that file is first copied up into the top layer and edited there, and the lower layers stay unchanged.
3. Automated Bridge Networking and Embedded DNS
Each container is like a separate house that needs a street, an address, and a way for mail to reach it. Docker lays the street (a bridge), assigns the addresses, and forwards the mail (ports).
Configuring Linux network namespaces by hand is tedious and error-prone. Here, <container_pid> stands for the process ID of the container’s main process, and br0 is a bridge you created earlier:
# Manual Linux Networking setup (WITHOUT Docker):
# 1. Create a linked pair of virtual ports, veth0 and veth1.
ip link add veth0 type veth peer name veth1
# 2. Move one end (veth1) into the container's network namespace.
ip link set veth1 netns <container_pid>
# 3. Give that end an IP address. The /24 means the first 24 bits
# identify the network (CIDR notation for the subnet size).
ip addr add 192.168.1.2/24 dev veth1
# 4. Plug the other end (veth0) into the bridge br0.
ip link set veth0 master br0
# 5. Forward TCP traffic arriving on port 80 to the container.
iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.1.2:80
With Docker, -p 8080:80 triggers all of this automatically. It means connections to port 8080 on the host go to port 80 inside the container. The daemon (a program that runs continuously in the background; Docker’s is called dockerd) manages virtual bridge devices (docker0), allocates private IPs from an internal subnet, sets up kernel forwarding rules, and runs an internal DNS resolver at 127.0.0.11 so containers talk to each other by container name instead of volatile IP addresses. DNS translates names into IP addresses. The 127.x.x.x addresses refer to the local machine, so 127.0.0.11 is reachable only from inside the container. Volatile means the address can change each time a container restarts.
4. Content-Addressable Distribution (Registry Protocol)
A registry is like a library of images: you push to upload one and pull to download one. Content-addressable means each item is looked up by a fingerprint of its content, not by a name someone chose.
Docker standardized how images get pushed, pulled, and verified through the Docker Registry HTTP API V2. HTTP is the standard protocol web browsers use, and this API is how Docker talks to a registry over it.
Because every layer is identified by its SHA-256 hash, pushing an update transfers only the modified layer delta, meaning only the part that changed. If your base layers didn’t change, the registry skips them entirely. Before uploading each layer, Docker checks whether the registry already has one with the same fingerprint, and skips it if so.
What Comes Next
Understanding these boundaries changes how you debug container issues. When an application gets killed with exit code 137, that’s not a mystery inside Docker. An exit code is the number a program returns when it stops, and 137 means the process was killed by signal 9 (SIGKILL), because shells report 128 plus the signal number. One common cause is the kernel’s cgroup memory controller enforcing a memory limit, but any SIGKILL produces 137, so treat it as a hint and check the container’s memory limit and logs. When a port is unreachable, the culprit is usually an iptables rule or a missing route on the bridge. A route is a rule telling the kernel where to send packets for a given address.
In Part 2, we trace the execution path from the Docker CLI command down through dockerd, containerd, and runc (the three programs in Docker’s execution chain, each explained there) to see what happens the moment you press enter.
💬 Discussion & Errata
Have questions, feedback, or spotted a bug in this breakdown? Reach out directly:
Part 2: What Happens When You Type docker run? From CLI to containerd to runc
Continue to Part 2 →