DEV Community

Syed Anzar
Syed Anzar

Posted on

What Actually Happens When You Run `docker run`

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;"]}'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 4: the OCI runtime bundle

containerd now assembles an OCI bundle, the standard, tool-agnostic description of a container. It is two things:

  1. 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.
  2. 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"}
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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.max or cpu.cfs_quota enforces the limits.
  3. pivot_root into the rootfs so 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.
  4. Drops Linux capabilities it should not have.
  5. execve your 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And the cgroup that caps what it can use (cgroup v2):

cat /sys/fs/cgroup/system.slice/docker-$id.scope/memory.max
Enter fullscreen mode Exit fullscreen mode

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)