Quick Answer: A Docker container is an ordinary Linux process with restrictions applied by the kernel. Namespaces control what it can see, cgroups control how much it can use, and its filesystem comes from stacked image layers. Docker hands this setup to a low-level runtime called runc, which is why containers feel like tiny virtual machines but behave very differently.
Most explanations of how Docker containers work start with a diagram of boxes inside boxes and move on. That picture is useful, but it hides the strange part. Developer Code and Cake spent time building a toy container runtime and came away with a list of surprises in their write-up on Docker. Their conclusion is the one that matters: Docker is not magic, it's a pile of Linux features with a friendly interface on top. Here's what that means in practice, and where the usual advice goes wrong.
Containers Are Just Processes
The lazy version of the story says containers are lightweight virtual machines. They aren't. A VM runs its own kernel on virtualized hardware. A container shares the host kernel and is just a process that the kernel has been told to restrict.
You can verify this yourself. Start a container, then look on the host. The process is there, with a normal PID, visible under /proc. Your kernel is still running it, scheduling it, and handling its system calls.
This matters because it changes what "isolation" means. A container boundary is a set of kernel checks, not a hardware boundary. When a container escapes, it's usually because a kernel bug or a misconfigured capability let it step outside those checks. That is a very different threat model from a hypervisor escape, and it's why you should treat kernel patch levels as part of your container security posture.
That said, containers are still far lighter than VMs for most workloads. You're paying for a process, not a whole operating system boot.
Containers Are Five Kernel Features in a Trenchcoat
Here's where it gets interesting. There's no single Linux feature called "containers." The namespaces(7) man page lists eight namespace types on current kernels: mount, UTS, IPC, PID, network, user, cgroup, and time. Docker uses most of them, along with control groups and a few more restrictions.
Each piece does one job:
- Namespaces change what a process can see. A PID namespace gives the container its own process tree, so
psinside shows PID 1 even though the host assigns it something else. - cgroups limit resource use. Memory caps, CPU shares, and I/O throttling all live here. Most current distributions use the unified cgroup v2 hierarchy by default.
- Capabilities split root's powers into smaller units, such as
CAP_NET_BIND_SERVICE. Docker drops many of them by default. - seccomp filters which system calls the process can make. Docker's default profile blocks a few dozen.
- Network plumbing (virtual ethernet pairs, bridges, and iptables or nftables rules) gives each container its own network stack.
| Mechanism | Kernel feature | What it controls | Example you can run |
|---|---|---|---|
| PID isolation | PID namespace | Process tree visibility | docker run --rm alpine ps |
| Filesystem view | Mount namespace | Mount table and root directory | docker run --rm alpine ls / |
| Network stack | Network namespace | Interfaces, routes, ports | docker run --rm --network none alpine ip a |
| Memory limit | cgroup v2 memory.max | RAM ceiling | docker run -m 256m alpine |
| Syscall filter | seccomp-bpf | Allowed system calls | Inspect the default profile in Docker's repo |
Notice that none of these is a VM. Each one is a narrow lens on the same kernel. If you're debugging a container, the question is usually "which lens is misconfigured?", not "what's wrong with the container?"
This next part trips up almost everyone: the network namespace is the most complicated piece of the stack, and it's where most real-world surprises come from.
Images Are Layers, Not Just Folders
Code and Cake's note describes images as folders that Docker unpacks and then chroots into. That's a fair mental model for the final result, but it skips the part that explains most image behaviour.
An image is a stack of read-only layers. Each layer is a filesystem diff, typically distributed as a compressed tarball. When you run a container, Docker (on Linux, usually with the overlay2 storage driver) mounts those layers using overlayfs, adds a thin writable layer on top, and presents the merged result as the container's root.
Then runc switches into that root. The runtime uses pivot_root(2) rather than plain chroot, which is safer because it swaps the root mount and lets the old root be unmounted. The OCI runtime specification defines this flow, so any compliant runtime behaves similarly.
Here's the counter-intuitive part: a container's writes don't touch the image at all. They land in the writable layer, which is discarded when the container is removed. That's why docker run --rm leaves no trace, and why a database writing to a container's default path loses data on removal unless you attach a volume.
Here's the practical rule. Treat image layers as immutable, put state in volumes, and remember that every RUN line in a Dockerfile creates a new layer. Fewer, well-ordered layers mean smaller images and faster pulls.
Why Go Makes a Tricky Container Runtime
The post makes a point that deserves more precision. runc is written in Go, and Go's runtime is multithreaded from the start. Many Linux operations that a container runtime needs, such as joining namespaces, must happen in a single-threaded process. You can't reliably do that after the Go runtime has spun up its threads.
The runc project's answer is unusual. It includes a small C constructor that runs before the Go runtime initializes, performs the namespace setup, and then lets Go take over. Anyone reading runc's source will find that C code early and wonder why it's there. Now you know.
The fork issue works the same way. Go's standard library does have syscall.ForkExec, but it's designed to fork and exec immediately. A runtime that wants to fork, apply restrictions, and then exec the user's program needs more control than that. In a multithreaded process, a raw fork only copies the calling thread, leaving the child with locks and state that other threads held. That's a footgun.
runc's workaround is to re-execute /proc/self/exe with a special argument that marks the new process as the init stage. It's an approximation of fork-then-configure-then-exec, and it's a good example of working around a runtime's constraints instead of fighting them.
Is Go the wrong choice? Not necessarily. containerd, Docker's daemon, and Kubernetes' tooling are all Go, and the ecosystem benefits. The honest trade-off is that a runtime's low-level code sits awkwardly in a language built around goroutines.
Why Docker Wants Root and What Rootless Costs
Most people first notice Docker's root requirement on macOS, where Docker runs inside a Linux VM and the daemon's root status is hidden behind the desktop app. On a Linux server, the daemon runs as root by default, and any user in the docker group can effectively become root on the host. Adding a user to that group is a privilege grant, not a convenience.
Why root? Creating namespaces, mounting filesystems, and configuring cgroups have historically required privileges. Modern kernels have loosened some of that through user namespaces, and Docker supports a rootless mode that runs the daemon as an unprivileged user.
Rootless mode has real costs. Networking is the big one. A rootless process can't create the bridges and privileged interfaces that Docker normally uses, so tools like slirp4netns or pasta implement a userspace network stack. Performance is usually fine for development, but port handling, source IP visibility, and some network modes behave differently. Check the Docker rootless documentation for the current limitations before you rely on it in production.
Here's the decision framework I'd use:
- Use rootful Docker on trusted hosts where you need full networking and simple operations.
- Use rootless Docker for multi-user development machines or hardened environments, accepting networking trade-offs.
- Use neither for untrusted workloads without an additional sandbox such as gVisor or Kata Containers.
Most guides present rootless as a free security upgrade. It's a meaningful reduction in blast radius, but it isn't free, and the networking gap is where teams get surprised.
Frequently Asked Questions
What is a Docker container, really?
A Docker container is a normal Linux process that the kernel has restricted using namespaces, cgroups, capabilities, and seccomp filters. It shares the host kernel, unlike a virtual machine, which runs its own. Docker packages these features behind a single command-line interface and an image format.
How do I run a Docker container without root?
Install Docker's rootless mode, which runs the daemon as an unprivileged user using user namespaces. Expect limitations, especially around networking, since rootless mode uses userspace tools such as slirp4netns or pasta. Check the official rootless documentation for supported features before migrating a workload.
Why does Docker need root privileges?
Docker needs privileges to create namespaces, mount filesystems, and configure cgroups and networking, which the kernel has historically restricted to root. Rootless mode uses user namespaces to avoid this, but it changes how networking works. Any user in the docker group can effectively gain root on the host.
Why do Docker image changes disappear when a container stops?
Container writes go to a thin writable layer on top of read-only image layers. When you remove the container, that layer is discarded. To keep data, store it in a named volume or bind mount, since the image layers themselves are never modified.
Understanding how Docker containers work is less about memorising commands and more about knowing which kernel feature is responsible for each behaviour. Next time something breaks, ask which namespace, cgroup, or mount is involved before you blame the container. Try running docker run --rm alpine ps and then ps aux on your host, and notice the same process from two different views. Then read our guide to Linux namespaces and cgroups for container debugging next, or pass this along to a teammate who's been treating Docker like a black box.