What Building a Container Runtime Reveals About Docker’s Quirks

—

제공

안

A developer decided to build their own container runtime from scratch as a learning project, and in the process documented a series of surprising details about how Docker actually works under the hood. The resulting writeup, published on codeandcake.dev, is a reminder that containers are not a single, clean technology but a clever assembly of older Linux kernel features duct-taped together. For developers who use docker run every day without thinking twice, the findings explain why Docker behaves the way it does, and why “lightweight VM” is the wrong mental model entirely.

Containers are just processes, not mini virtual machines

The most fundamental point from the project is that a container is not a separate virtualized machine the way a VM is. A container is simply a regular process running on the host kernel, with extra restrictions layered on top. There is no hypervisor, no separate kernel booting up inside the container. The process is visible to the host kernel just like any other process, and curious developers can actually find a running container’s process listed in /proc on the host. This is why containers start almost instantly and use far fewer resources than a VM: there is no second operating system to boot, just a process with some fences around it.

Five kernel features wearing a trenchcoat

There is no single Linux kernel feature called “containers.” Instead, what we call a container is really the combined effect of several independent kernel mechanisms working together. According to the writeup, the main pieces are:

  • Namespaces, which control what the host and the container process can each see, such as process IDs, mounts, and hostnames
  • Cgroups, which limit how much CPU, memory, and other resources a container can consume
  • A collection of networking features that isolate a container’s network stack from the host, which the author admits is complex enough that even an experienced developer building a runtime finds it hard to fully untangle

Docker, and the many tools built on similar ideas like Kubernetes or Podman, are essentially orchestration layers on top of these primitives. None of this is new technology invented by Docker; namespaces and cgroups existed in the Linux kernel well before Docker made them easy to use. Docker’s real contribution was packaging these low-level features into a simple command-line experience.

What actually happens when you run docker run

Container images are often described in marketing terms, but mechanically they are just folders. When Docker pulls an image, it unpacks it into a directory on disk. Then, when you start a container, Docker uses a system call like chroot or the newer, more secure pivot_root to make that folder the new root filesystem for the container process. So running something like docker run -it --rm ubuntu /bin/bash is, at its core, Docker preparing a folder, applying namespaces and cgroups to isolate it, and then pointing a process’s root directory at that folder before executing your command inside it.

Why Go makes building a runtime surprisingly hard

One of the more technical findings concerns runc, the actual container runtime engine that Docker relies on internally. runc is written in Go, and Go’s design creates real friction for this kind of low-level systems work. Go’s runtime is natively multi-threaded, which is normally a strength, but many of the Linux syscalls a container runtime needs to execute must run in a single-threaded environment. To work around this, runc includes a C module that runs before Go’s own runtime initializes, just so it can safely make those syscalls.

A second complication is that Go does not expose a traditional fork system call. Fork only duplicates the calling thread rather than the whole process, which makes it risky to use directly in a multi-threaded runtime like Go’s. But a container runtime fundamentally wants to fork a process, apply a set of restrictions, and then exec the user’s actual command, which is exactly the classic fork-then-exec pattern. To approximate this, runc instead execs /proc/self/exe with a special command-line argument that marks it as a child process, effectively simulating a fork using the tools Go does provide. It is a workaround, not a native solution, and it illustrates how building systems-level tools in a higher-level language can require unusual engineering detours.

Root access, security, and the rootless networking gap

Many of the Linux kernel APIs that container runtimes depend on have historically required, or still require, root privileges. This is why the Docker daemon typically runs as root, something that is easy to overlook for developers running Docker Desktop on macOS, where the Linux internals are hidden behind a virtual machine anyway. The author notes that rootless container setups have improved significantly in recent years, letting containers run without a root-level daemon for most purposes.

Networking remains the stubborn exception. Because the networking isolation features are tightly coupled to privileged kernel operations, rootless container runtimes often have to reimplement the entire networking stack in userspace just to offer the same functionality that a root-based setup gets for free from the kernel. This is a meaningful caveat for anyone choosing a rootless setup for security reasons: it may come with networking limitations or performance tradeoffs compared to a traditional root-based Docker installation.

Why this matters for developers

None of this changes how anyone uses Docker day to day, but understanding these internals helps explain real-world behavior: why containers aren’t as isolated as VMs for security-sensitive workloads, why rootless Docker setups sometimes feel limited on the networking side, and why alternative runtimes exist at all. It is also a useful case study in a broader lesson for working developers: building even a small, incomplete version of a tool you rely on daily is one of the fastest ways to understand what that tool is actually doing.

Frequently asked questions

Is a Docker container the same as a lightweight virtual machine?
No. A container is a regular process on the host’s existing kernel with isolation restrictions applied through namespaces and cgroups. A VM runs its own separate kernel on virtualized hardware, which is why VMs have more overhead but stronger isolation.

Why does Docker usually need root access?
Many of the underlying Linux kernel features used to isolate containers, especially around networking, have traditionally required root privileges to configure, which is why the Docker daemon has historically run as root.

What is runc?
runc is the low-level container runtime that Docker uses internally to actually create and manage containers using Linux namespaces, cgroups, and related kernel features. It is written in Go.

Source: Docker Is Weird


코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다