Docker Architecture
From docker run to container process — every layer of the Docker stack
The Docker Stack
Every docker run traverses this chain of processes before a container exists:
docker CLI
Parses commands, sends REST API calls
dockerd
Docker daemon — images, volumes, networks, build
containerd
Container lifecycle, image storage, snapshots
containerd-shim
Parent of container process, survives restarts
runc
Sets up namespaces/cgroups, calls exec(), then exits
Container
The isolated process running your workload
docker CLI
- Parses user commands (
run,build,ps, etc.) - Translates them into REST API calls to dockerd
- Connects over Unix socket:
/var/run/docker.sock - Stateless — just a client binary
dockerd (Docker daemon)
- The main long-running process
- Handles: image management, volumes, networks, build system
- Exposes REST API on Unix socket (
/var/run/docker.sock) - Delegates actual container operations to containerd
containerd
- CNCF project — not Docker-specific
- Manages complete container lifecycle: create, start, stop, delete
- Handles image pull/push and storage (snapshots)
- Provides gRPC API for higher-level tools
containerd-shim
- One shim per running container
- Becomes the parent process of the container's PID 1
- Allows containerd to restart without killing containers
- Reports container exit status back to containerd
runc
- OCI-compliant low-level runtime
- Sets up namespaces (mount, PID, net, user, UTS, IPC)
- Configures cgroups (CPU, memory, I/O limits)
- Applies seccomp profile, calls
pivot_root, thenexec() - Exits after setup — runc is not a long-running process
docker run Decomposed — 7 Steps
$ docker run -d --name web -p 8080:80 nginx
- CLI parses the command
docker run -d --name web -p 8080:80 nginxis translated into a REST API call:
POST /v1.43/containers/createwith JSON body containing image, port mappings, name, detach mode. Sent to dockerd via/var/run/docker.sock. - Image resolution
dockerd checks ifnginx:latestexists in the local image store. If not, it initiates a pull from the configured registry (default: Docker Hub). The pull goes through containerd, which downloads manifest + layer blobs, verifies digests, and unpacks layers into snapshots (overlay2). - Create container
dockerd calls containerd API to create the container. containerd creates a snapshot: the overlay2 merged view with image layers aslowerdir(read-only) and a new emptyupperdir(read-write layer). An OCI runtime spec (config.json) is generated with all namespace, cgroup, and mount configurations. - Network setup
dockerd creates a veth pair: one end in the container's network namespace, one end attached to thedocker0bridge on the host. An IP address is allocated from the bridge subnet (default:172.17.0.0/16). For port mapping (-p 8080:80), dockerd inserts iptables NAT rules (DNAT) to forward traffic from host port 8080 to the container's IP on port 80. - Start container
containerd spawns containerd-shim, which invokes runc. runc performs:
clone()with namespace flags (CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWNET, ...)
→pivot_rootto the overlay2 merged directory
→ apply seccomp filter
→exec(nginx)— the container's entrypoint - runc exits
Afterexec(), runc's job is done and it exits. The container process (nginx) is now a child of containerd-shim. The shim keeps running, holding stdout/stderr file descriptors and monitoring the container's exit status. - Port mapping active
The iptables DNAT rule is in place:
-A DOCKER -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
Traffic arriving athost:8080is forwarded tocontainer_ip:80. The container is running in detached mode (-d), so the CLI returns the container ID immediately.
docker exec
Running a new process inside an already-running container:
$ docker exec -it web bash
How docker exec Works
- dockerd finds the target container's PID on the host (e.g., PID 4523)
- Uses
setns()to join all existing namespaces of the target container: mount, UTS, IPC, network, PID, cgroup - Then calls
exec()to run the requested command (bash) inside those namespaces - The new process shares the container's filesystem, network, and PID space
The underlying mechanism — you can do the same manually:
# nsenter -t <pid> -m -u -i -n -p -- /bin/bash
-t <pid> target process
-m mount namespace
-u UTS namespace (hostname)
-i IPC namespace
-n network namespace
-p PID namespace
-- command to run after joining
Note
Key difference from docker run: exec does NOT create new namespaces.
It joins existing ones. The new process also does not get its own cgroup — it shares the container's resource limits.
Restart Policies
Set with docker run --restart=<policy> — determines what happens when the container process exits:
| Policy | Behavior |
|---|---|
no |
Never restart (default). Container stays in "exited" state. |
on-failure[:max] |
Restart only on non-zero exit code. Optional max retry count: --restart=on-failure:5 |
always |
Always restart regardless of exit code. Also restarts when the Docker daemon itself restarts. |
unless-stopped |
Like always, but does NOT restart if the container was manually stopped (docker stop)
before the daemon restart. |
Tip
Tip: Use on-failure:5 for most workloads — it prevents infinite restart loops
while still recovering from transient failures. Use unless-stopped for long-running services
that should survive daemon upgrades.
Logging Drivers
How Container Logging Works
Container stdout/stderr → captured by containerd-shim → passed to the configured logging driver
Container Process
Writes to stdout / stderr
containerd-shim
Captures output via FDs
Logging Driver
json-file, journald, syslog, fluentd, ...
| Driver | Destination | docker logs Support |
|---|---|---|
json-file (default) |
/var/lib/docker/containers/<id>/<id>-json.log |
Yes |
journald |
systemd journal | Yes |
syslog |
Syslog daemon | No |
fluentd |
Fluentd collector | No |
gelf |
Graylog Extended Log Format endpoint | No |
awslogs |
Amazon CloudWatch Logs | No |
gcplogs |
Google Cloud Logging | No |
Warning
Warning: docker logs only works with json-file and journald
drivers. If you switch to fluentd or awslogs, docker logs returns nothing.
Plan your log access strategy accordingly.
The Socket File — Security
/var/run/docker.sock
- Unix domain socket for the Docker REST API
- Default permissions: owned by
root:docker - Any process with access to this socket can make any Docker API call
- Docker API = full control over host: mount filesystems, run privileged containers, access all data
Warning
docker run -v /var/run/docker.sock:/var/run/docker.sock myapp- Create privileged containers with host filesystem access
- Read/write any file on the host
- Kill any container or process
- Modify iptables rules, network configuration
# ls -la /var/run/docker.sock
srw-rw---- 1 root docker 0 Mar 31 10:00 /var/run/docker.sock
# curl --unix-socket /var/run/docker.sock http://localhost/v1.43/containers/json
[{"Id":"abc123...","Names":["/web"],"Image":"nginx","State":"running",...}]
Cross-Reference
Note
Related: For Docker networking (bridge, host, overlay, macvlan), see Net 12 — Docker Networking.