What Actually Happens When You Run docker run
You type docker run -d --memory 512m myapp:latest and hit Enter. A second later, a container is running. It feels like one action. It is not.
Behind that single command, four separate programs hand work down a chain, an image gets pulled apart into layers, a bundle of files gets written to disk, and finally the Linux kernel is asked to put one process into its own little world. Nothing here is magic, and every step is something you can watch on a real machine.
The 30,000-foot view
Here is the chain a single docker run travels before your process exists:
Step 1: the CLI is just a REST client
The docker binary does not create containers. It turns your command into an HTTP request and sends it to the Docker daemon over a local Unix socket at /var/run/docker.sock. You can make the exact same call by hand:
curl --unix-socket /var/run/docker.sock \
-X POST /ContainerCreate \
-d '{"Image": "nginx", "Cmd": ["nginx", "-g", "daemon off;"]}'
The CLI parses your flags (-d, --memory, -p), constructs a gRPC request, and sends it downstream. It does no work itself.
Step 2: dockerd prepares the work and pulls the image
dockerd is the long-running engine. It receives the request, parses your flags (the -p 8080:80 port map, env vars, mounts), and checks whether the nginx image is already on disk.
If the image is missing, the daemon pulls it. An image is not one file. It is a manifest plus a stack of read-only layers, each identified by a digest. The daemon downloads only the layers it does not already have, which is why the second image that shares a base layer pulls almost instantly.
Step 3: dockerd hands off to containerd
Here is the part that surprises people: dockerd does not start your process either. It delegates to containerd, a separate daemon that owns the container lifecycle. containerd unpacks the image layers into a snapshot (a stack of directories unioned together with overlayfs), tracks container state, and prepares everything the runtime needs.
Your Docker containers live under containerd's moby namespace, and you can list them with containerd's own CLI:
containerd ctr containers list
Step 4: the OCI runtime bundle
containerd now assembles an OCI bundle, the standard, tool-agnostic description of a container. It is two things:
-
config.json— the OCI runtime spec: which process to run, which namespaces and cgroups to create, which mounts to set up, which capabilities to keep. -
rootfs— the container's root filesystem: the image's read-only layers plus a fresh writable layer on top, unioned together.
The interesting part is the linux.namespaces block. This is the container's isolation, declared before the container exists:
"linux": {
"namespaces": [
{"type": "pid"},
{"type": "network"},
{"type": "mount"},
{"type": "uts"},
{"type": "ipc"}
]
}
Step 5: runc creates the container, then gets out of the way
containerd calls runc, the low-level OCI runtime and the piece that actually talks to the kernel. runc reads config.json and, in order:
-
Creates the namespaces listed in the spec (a new PID namespace, network namespace, mount namespace, and so on). One
clone()syscall creates all five new namespaces simultaneously. -
Sets up the cgroup that will cap the container's CPU and memory. In cgroup v2, the container's PID is placed into a cgroup directory, and writing to
memory.maxorcpu.cfs_quotaenforces the limits. -
pivot_rootinto therootfsso the process sees the container's filesystem as/. The host root is swapped out and becomes unreachable — not by file descriptor, not by.., not by climbing out of a chroot. - Drops Linux capabilities it should not have.
-
execveyour process (nginx, which becomes PID 1 inside its new PID namespace).
Then runc exits. It is not a supervisor. A small containerd-shim process stays behind to keep the container attached to containerd and to reap it when it ends.
Step 6: it is a normal process on the shared kernel
This is the whole point. There is no guest operating system and no virtual hardware. nginx is a regular process on your host. Find its real PID:
id=$(docker run -d -p 8080:80 nginx)
pid=$(docker inspect --format '{{.State.Pid}}' "$id")
ps -o pid,comm -p "$pid"
# there it is
# 12345 s nginx <- in the host process table
What makes it a "container" is only the kernel features wrapped around that process. Look at the namespaces it lives in:
sudo lsns -p "$pid"
# NS TYPE NPROCS PID COMMAND
# 4026531840 pid 1 ... nginx
# 4026532210 net 1 ... nginx <- its own network stack
# 4026532208 mnt 1 ... nginx <- its own filesystem view
And the cgroup that caps what it can use (cgroup v2):
cat /sys/fs/cgroup/system.slice/docker-$id.scope/memory.max
Mental model checklist
Next time you run docker run, you can trace the chain in your head:
| Step | Who does it | What happens |
|---|---|---|
| 1 |
docker CLI |
Parses flags → gRPC over Unix socket |
| 2 | dockerd |
Pulls image layers (deduped), builds config |
| 3 | containerd |
Unpacks layers to overlayfs snapshot, assembles OCI bundle |
| 4 | runc |
Creates namespaces, cgroups, pivot_root, drops caps, execve
|
| 5 | Kernel | Your process runs — isolated but native |
No VM. No guest OS. One shared kernel. Five isolation features. That is all a container is.
*Originally published on dev.to. This article is part of the "What Actually Happens?" series explaining the real mechanisms behind everyday developer tools.
Top comments (0)