Docker & ContainersII Β· Linux InternalsStartup deep-dive
Container startup from CLI to running process
What you'll learn
- Trace `docker run nginx` from CLI invocation to running process
- Identify which component is responsible for which delay
- Diagnose common startup failures at the right layer
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-09
This lesson traces the full lifecycle of a single command:
docker run --name web -p 8080:80 -d nginx:1.27
You should be able to look at any layer of this stack and answer βwhat is happening right now and why is it slow?β
The high-level sequence
sequenceDiagram
participant U as User shell
participant CLI as docker CLI
participant D as dockerd
participant CT as containerd
participant SH as containerd-shim
participant R as runc
participant K as Linux kernel
U->>CLI: docker run nginx
CLI->>D: POST /containers/create (REST, unix socket)
D->>CT: Create container (gRPC)
CT->>CT: Resolve image locally
alt image not cached
CT->>CT: Pull layers from registry (HTTPS)
end
CT->>CT: Construct OCI bundle
D-->>CLI: container ID
CLI->>D: POST /containers/<id>/start
D->>CT: Start container (gRPC)
CT->>SH: Create shim, start container
SH->>R: runc run <bundle>
R->>K: clone(2) with CLONE_NEWNS|NEWPID|NEWNET|NEWUTS|NEWIPC
R->>K: mount overlayfs (upperdir, lowerdir, workdir, merged)
R->>K: write cgroup v2 limits
R->>K: apply seccomp, AppArmor
R->>K: apply capability set
R->>K: execve("/docker-entrypoint.sh")
K-->>R: PID 1 of the new namespace
R-->>SH: container running, PID
SH-->>CT: container started
CT-->>D: container started
D-->>CLI: container started
Phase 1 β CLI parsing and request
curl -s --max-time 5 --unix-socket /var/run/docker.sock \
-w '\nhttp_code=%{http_code}\n' http://localhost/_ping
echo '--- are the workloads still running regardless? ---'
ps -eo pid,ppid,etime,comm | grep -E 'containerd-shim' | head
echo '--- would a daemon restart stop them? ---'
docker info --format 'live-restore={{.LiveRestoreEnabled}}' 2>/dev/null || echo 'daemon not answering'OK
http_code=200
--- are the workloads still running regardless? ---
1971 1 05:12:44 containerd-shim
1978 1 05:12:44 containerd-shim
1992 1 05:12:43 containerd-shim
--- would a daemon restart stop them? ---
live-restore=falseThat capture is from a healthy host, and every line is load-bearing:
OK/http_code=200β the daemon is answering. A hang indocker psafter this is not a daemon problem.- No output, or
http_code=000after five seconds β the daemon is not answering. Now it is a daemon problem, and the next stop isjournalctl -u docker --since '10 min ago', not a restart. PPIDof 1 on every shim β the shims are children of init, not of dockerd. The containers do not depend on the daemon to keep running.live-restore=falseβ the default, and the reason a restart would stop all of them. Turn it on before you need it.
sudo tee /etc/docker/daemon.json <<'EOF'
{
"live-restore": true
}
EOF
sudo systemctl reload docker
docker info --format 'live-restore={{.LiveRestoreEnabled}}'live-restore=trueIllustrative output
Use reload, not restart: reloading applies the change without
the stop you are trying to protect against. And read the value back
from docker info rather than trusting the file β a JSON syntax
error in daemon.json leaves the daemon running on its previous
configuration, and nothing on the command line says so.
Phase 2 β dockerd: create the container record
dockerd receives the create request, validates the spec, and asks containerd to create a container record. dockerd does not run anything yet β it just records βa container with this spec should existβ.
If the image is not local, dockerd asks containerd to pull. Pull is the single largest source of latency on a cold cache:
- DNS resolution for the registry.
- TLS handshake.
- Authentication (OAuth bearer token from the registry).
- Manifest fetch.
- Layer fetch (the actual bytes).
For a multi-layer image on a fresh host, pull can take 5β30 seconds depending on bandwidth and image size.
Phase 3 β containerd: build the OCI bundle
containerd prepares the OCI bundle. An OCI bundle is a directory containing:
config.jsonβ the runtime spec (namespaces, cgroups, capabilities, mounts, etc.).rootfs/β the containerβs root filesystem, mounted via OverlayFS.
containerd does not run anything; it just creates the bundle on disk.
Phase 4 β start request
The CLI sends POST /containers/<id>/start. dockerd forwards it
to containerd. containerd spawns a containerd-shim-<id> process
which is the parent of the future container.
The shim invokes runc run --bundle /var/run/docker/containerd/.../<id> ....
Phase 5 β runc: create namespaces and start the process
runc does the heavy lifting:
- Apply capabilities. Set the capability bounding set for the new namespaces.
- Apply seccomp. Load the BPF filter that will be evaluated on every syscall.
- Apply AppArmor / SELinux. Apply the LSM profile.
- Write cgroup limits. Set
memory.max,cpu.max, etc. - Mount OverlayFS. Compose the lower/upper/work/merged tree.
- Mount /proc, /sys, /dev. Inside the new mount namespace.
clone(2)with namespace flags. The new process is the containerβs PID 1. The old PID namespace is hidden.chdirto rootfs,execvethe entrypoint.
At this point the entrypoint is running as PID 1 in the container.
Phase 6 β entrypoint runs
The entrypoint is whatever the imageβs ENTRYPOINT and CMD
combine to. For nginx:1.27, itβs /docker-entrypoint.sh which
execs nginx -g 'daemon off;'.
nginx reads its configuration, binds to port 80 (the container port), and starts serving.
Phase 7 β port publishing
The -p 8080:80 flag asks dockerd to publish container port 80
to host port 8080. dockerd configures the iptables (or nftables)
rules for this. There are two implementations: the legacy
userland-proxy and the modern iptables-nat (default on Linux
5.x+).
Where time is spent
A cold-cache docker run nginx on a fresh host typically takes:
| Phase | Time |
|---|---|
| CLI parsing | < 10 ms |
| dockerd β containerd RPC | ~5 ms |
| Image pull (5 layers Γ ~25 MB at 100 MB/s) | ~10 s |
| Bundle creation | ~50 ms |
| Shim + runc spawn | ~30 ms |
| OverlayFS mount | ~50 ms |
| cgroup write | ~5 ms |
| clone + execve | ~20 ms |
| nginx startup | ~100 ms |
The image pull dominates. Subsequent runs hit local cache and complete in well under a second.
What can go wrong at each phase
| Symptom | Likely cause |
|---|---|
| βCannot connect to Docker daemonβ | dockerd is not running, or socket perms wrong |
| βimage not foundβ | Wrong image reference, or registry auth issue |
| βpermission deniedβ on volume | Bind mount ownership, SELinux label mismatch, or a seccomp denial surfacing as EPERM |
| Container exits immediately | Entrypoint error; check docker logs |
docker stop always takes exactly 10 s and exits 137 | PID 1 ignores SIGTERM β usually a shell-form CMD. Not an OOM: check State.OOMKilled before reaching for memory |
Container exits 137 with State.OOMKilled true | Genuine OOM kill; check memory.events and dmesg |
docker ps / docker exec / docker logs all hang | The daemon, not the container. curl --max-time 5 --unix-socket /var/run/docker.sock http://localhost/_ping before you restart anything |
| Port not reachable | Firewall (host), iptables (Docker), proxy upstream, or app bound to 127.0.0.1 inside the container |
Knowledge check
Knowledge check Β· 6 questions
Q1. Which phase of `docker run nginx` typically dominates the total wall-clock time on a cold host?
Q2. After the container is running, what is runc's role?
Q3. Name the order of the first three processes spawned by `docker run`, from the inside out.
Q4. `docker ps`, `docker logs` and `docker exec` all hang with no output. What does this tell you?
Q5. Restarting the Docker daemon on a stock host leaves running containers untouched.
Q6. Which process is the parent of a running container, once runc has exited?
Passing score: 75%. Answers are checked in this browser.