Skip to content
What actually happens when you type 'docker run'

What actually happens when you type 'docker run'

7 min read Docker

Running a Docker container feels like magic, but it relies on standard Linux primitives. This breakdown traces the exact journey from a simple CLI command to a fully isolated, running Linux process utilizing namespaces and cgroups....

Subscribe to listen
audio-thumbnail
What actually happens when you type 'docker run'
0:00
/0
Clinical Summary
Diagnosis

Treating containers as lightweight virtual machines causes mental model breakdowns during production debugging, leading to confusion when isolated processes hit kernel-level resource limits despite available host capacity.

Prescription
  • Execution Chain: Trace the lifecycle handoff from the CLI to dockerd, containerd, and finally runc.
  • Kernel Isolation: Recognize that Linux namespaces restrict what a process can see, abstracting PIDs, mounts, and networks.
  • Resource Ceilings: Debug out-of-memory errors by checking cgroups accounting limits instead of physical host hardware.
Side Effects

Local development environments on macOS and Windows silently introduce a hidden hypervisor, causing file system and networking behaviors to differ from native Linux production environments.

Script

You type docker run nginx. A split second later, a web server is up, listening on port eighty, ready to serve traffic. For a long time, the easiest way to think about what just happened was to picture a tiny, lightweight virtual machine. It boots up, it has its own isolated file system, it feels like an independent computer.

But that mental model breaks down the minute things go wrong in production. A container is not a machine. It is a standard Linux process wrapped in kernel features and handed off through a surprisingly deep chain of command. Let us trace exactly what happens when you hit enter, right down to the Linux kernel.

The Chain of Command: From CLI to Kernel

It starts with the Docker CLI. The CLI itself does not run your container. It is just a frontend client. When you type docker run, the CLI parses your arguments, builds a JSON payload, and fires an HTTP request over a local Unix socket to the Docker daemon.

This daemon is called dockerd. dockerd is the high-level coordinator. Its first job is to figure out if it actually has the files needed to run your request. It checks its local disk cache for the nginx image. If the image is not there, dockerd reaches out to a registry, like Docker Hub or Amazon ECR, to pull it down.

It does not just download a single massive file. Docker images are built using a stacked file system. They are composed of multiple read-only layers. dockerd pulls these individual compressed tarballs and unpacks them. It then configures a storage driver on the host system to stack these layers on top of each other. The bottom layer might be a basic Debian operating system file system. The next layer adds required network libraries. The top layer adds the nginx binary. Finally, dockerd slaps a thin, empty, writable layer right on top of that stack.

This is why you can write files inside a running container without permanently modifying the underlying image. Any changes you make are caught by that top writable layer.

Once the file system is prepped, dockerd is ready to execute. But dockerd does not start the container directly. It passes the request down the chain to a separate daemon called containerd. containerd manages the actual lifecycle of the container. It takes the prepared image layers and extracts them into a root file system directory. Then, it translates the configuration instructions from dockerd into an open standard called the OCI specification. It creates a configuration file called config.json. This file, combined with the extracted root file system, forms what is known as an OCI bundle.

The bundle contains every exact detail needed to run the process: the executable path, the environment variables, the user ID, and the specific kernel isolation parameters.

You might ask why Docker needs three separate components just to start a single container. Why pass the baton from the CLI, to dockerd, and then to containerd? The answer is crash tolerance and modularity. In the early versions of Docker, the primary daemon did everything. If that daemon crashed, or if you needed to restart it for an upgrade, every single running container on your host died with it. By breaking the architecture apart, dockerd handles the high-level API and image pulling, while containerd focuses strictly on execution state.

But the chain does not stop at containerd. It passes the OCI bundle to one final utility called runc. runc is the lowest-level tool in the stack. Its only job is to interact directly with the Linux kernel to create the isolated environment, start the process, and then immediately exit.

runc reads the config.json, issues the exact system calls required to set up isolation, starts your nginx process, and then terminates. If runc exits, what is keeping track of your running container? containerd injects a tiny background process called a shim between itself and your container. The shim sits there, holding open the standard input and output streams, ready to report the exit status back to containerd if the container crashes. Because of this shim, you can restart the entire Docker daemon, and your nginx container will keep running without interruption.

What Provides the Isolation?

We have traced the command all the way down to the running process. But if a container is just a Linux process, and there is no guest operating system or hypervisor managing it, what actually prevents this process from accessing your host machine? What stops a compromised container from looking at your host file system or killing other processes?

The isolation comes entirely from the Linux kernel, specifically through a feature called namespaces. Namespaces act as a reality distortion field for a process. They limit what the process can see. When runc starts your container, it asks the kernel to create a new set of namespaces.

PID Namespace

The first is the PID, or Process ID, namespace. Your host operating system might see the nginx process running as PID 45200. But because of the namespace, the process inside the container looks around and thinks it is PID 1. It cannot see any other processes on the host. If it tries to send a kill signal, it literally has no way to address processes outside its namespace.

Mount & Network Namespaces

runc also creates a mount namespace. This ensures the process only sees that stacked overlay file system prepared earlier. It cannot see the host root directory, your home folder, or your host configuration files. It creates a network namespace as well. This gives the process a completely isolated network stack. It gets its own loopback interface, its own IP address, and its own routing table. The process is entirely oblivious to the physical network adapters on the host machine.

The Hypervisor Caveat: Linux vs. macOS & Windows

This brings up an important point regarding hypervisors. You will often hear the claim that containers are fundamentally different from virtual machines because there is no guest OS and no hypervisor involved. That statement requires a major caveat. It is only strictly true on native Linux. If your production environment runs on Amazon Linux or Ubuntu, your containers are just isolated processes running directly on the host kernel.

But if you are developing locally on a Mac or a Windows machine, Docker Desktop is absolutely running a hidden Linux virtual machine with a hypervisor under the hood. Mac and Windows kernels do not have Linux namespaces. Docker Desktop spins up a lightweight VM via Hyper-V or the macOS Virtualization framework, and runs your containers inside that guest Linux kernel. Your production environment has no hypervisor, but your local development environment absolutely does. Keeping that distinction clear saves you from a lot of confusion when testing file system performance or network bridges locally.

Resource Limits: Control Groups (cgroups)

Namespaces dictate what a container can see. But they do not dictate what a container can use. That is handled by another kernel feature called cgroups, short for control groups. Cgroups place a hard physical ceiling on resources. They limit CPU usage, memory consumption, and block device IO.

Picture this: It is a Tuesday afternoon, and your primary application container is repeatedly crashing with an Out Of Memory error. You connect to the host machine, check the system resources, and you see thirty-two gigabytes of RAM sitting completely available and idle.

If you are still applying the mini-virtual machine mental model, this situation makes absolutely no sense. The host machine clearly has memory. Why can't the container just use it?

How This Changes Troubleshooting

When you shift your mental model to understand that a container is just a Linux process bounded by a cgroup, the answer reveals itself immediately. You are not debugging a machine running out of memory. You are debugging a process that has hit a cgroup ceiling.

When runc started that container, it told the kernel to enforce a strict memory limit based on your configuration. As soon as your application process attempts to allocate memory beyond that cgroup limit, the Linux kernel Out Of Memory killer wakes up and instantly terminates the process. The host has thirty-two gigabytes of free RAM, but the kernel enforces the cgroup limit regardless of host capacity.

Understanding this architecture completely changes how you troubleshoot. You stop looking for virtual motherboard issues and start looking at the cgroup accounting limits applied to the process. The same logic applies to networking errors. If your container cannot reach an external database, you do not need to debug a virtual network adapter. You need to verify the routing rules inside that specific network namespace. Then, you step outside the namespace and check how the host kernel is forwarding packets via iptables and virtual ethernet pairs.

Conclusion: The Handoff in Review

The running container is simply a regular Linux process, wrapped in specific PID, network, and mount namespaces, bounded by a cgroup limit, and monitored by a shim. Tracing docker run exposes the reality of modern infrastructure. You are initiating a handoff. The CLI talks to dockerd. dockerd pulls the image layers and talks to containerd. containerd builds the OCI bundle and talks to runc. runc configures the kernel namespaces and starts the process.

When you internalize this sequence, you stop treating containers like magic black boxes. You know exactly where to look when an image pull fails, when a configuration is rejected, or when an application is forcefully killed by the kernel. You are no longer managing tiny isolated computers. You are managing Linux processes.

This is TAKEYOURPILLS.TECH. Go ship something.

References

/