Skip to content
Menu

Virtualization5 min read

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:

  1. docker CLI

    Parses commands, sends REST API calls

  2. dockerd

    Docker daemon — images, volumes, networks, build

  3. containerd

    Container lifecycle, image storage, snapshots

  4. containerd-shim

    Parent of container process, survives restarts

  5. runc

    Sets up namespaces/cgroups, calls exec(), then exits

  6. 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, then exec()
  • Exits after setup — runc is not a long-running process

docker run Decomposed — 7 Steps

console
console

    $ docker run -d --name web -p 8080:80 nginx
  
  1. CLI parses the command
    docker run -d --name web -p 8080:80 nginx is translated into a REST API call:
    POST /v1.43/containers/create with JSON body containing image, port mappings, name, detach mode. Sent to dockerd via /var/run/docker.sock.
  2. Image resolution
    dockerd checks if nginx:latest exists 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).
  3. Create container
    dockerd calls containerd API to create the container. containerd creates a snapshot: the overlay2 merged view with image layers as lowerdir (read-only) and a new empty upperdir (read-write layer). An OCI runtime spec (config.json) is generated with all namespace, cgroup, and mount configurations.
  4. Network setup
    dockerd creates a veth pair: one end in the container's network namespace, one end attached to the docker0 bridge 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.
  5. Start container
    containerd spawns containerd-shim, which invokes runc. runc performs:
    clone() with namespace flags (CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWNET, ...)
    pivot_root to the overlay2 merged directory
    → apply seccomp filter
    exec(nginx) — the container's entrypoint
  6. runc exits
    After exec(), 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.
  7. 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 at host:8080 is forwarded to container_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:

console
console

    $ 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:

console
console

    # 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

  1. Container Process

    Writes to stdout / stderr

  2. containerd-shim

    Captures output via FDs

  3. 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

Critical Security Risk: Mounting the Docker socket into a container:
bash
docker run -v /var/run/docker.sock:/var/run/docker.sock myapp
This gives the container full root access to the host. The container can:
  • Create privileged containers with host filesystem access
  • Read/write any file on the host
  • Kill any container or process
  • Modify iptables rules, network configuration
Never mount the Docker socket unless you fully trust the container image and understand the implications. Consider alternatives like Docker-in-Docker (dind) with TLS or rootless Docker.
console
console

    # 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.

Solidnines — solidnines.com