When you ran docker run hello-world in the previous lesson, the container's own message summed up what had happened: the client contacted the daemon, the daemon pulled an image from the registry, created a container from it and returned its output to you. That sentence hides a complete architecture, and understanding it is what separates someone who memorizes commands from someone who can diagnose problems. In this lesson you are going to open the box: you will discover that docker does not run containers (it only asks for them), who really runs them, what role containerd and runc play, what that socket we keep mentioning is, and why the daemon is the true owner of all your images, containers, volumes and networks. You will finish by interpreting the real output of docker info and docker system df with fresh eyes.

Contents

  1. Docker is client-server, not a program
  2. The docker client
  3. The dockerd daemon
  4. The REST API and the /var/run/docker.sock socket
  5. The full flow of a docker run
  6. containerd and runc: why there are three layers
  7. The objects the daemon manages
  8. Registries and their role
  9. Hands-on demonstration: docker info and docker system df

  1. Docker is client-server, not a program

The first misunderstanding to clear up: when you type docker run, that command does not create any container. All it does is translate your order into an HTTP request and send it to another process, which does the actual work.

Docker follows a client-server architecture with three actors:

Actor What it is Where it lives
Client (docker) A command-line program that translates your orders into API calls Your terminal
Daemon (dockerd) A long-running service that builds, runs and manages everything Usually the same machine; can be remote
Registry A service that stores and distributes images The internet (Docker Hub) or your network

The consequences of this design are very practical:

  • The client can be on a different machine from the daemon. You can manage a remote server from your laptop with the same docker command.
  • Many clients can talk to a single daemon. Your terminal, your IDE and a CI tool can all talk to the same engine at once.
  • If the daemon is down, the client can do nothing. Hence the famous Cannot connect to the Docker daemon.
  • State lives in the daemon, not in your terminal. Closing the terminal does not stop your containers.

  1. The docker client

The client is surprisingly dumb, and that is a good thing. Its responsibilities are:

  1. Parse what you type (docker run -d --name web nginx).
  2. Turn it into one or more calls to the daemon's REST API.
  3. Show you the response in readable form.

Nothing else. It does not know how to create containers, or unpack images, or talk to the kernel.

The client decides which daemon it talks to through what are called contexts. You can list them like this:

docker context ls
NAME       DESCRIPTION                               DOCKER ENDPOINT               ERROR
default *  Current DOCKER_HOST based configuration   unix:///var/run/docker.sock

How to read it: there is a single context, default, marked as active with the asterisk, and it points at the local Unix socket. If you had a remote server configured, you would see another row with a DOCKER ENDPOINT such as ssh://user@server.

You can also force the destination with the DOCKER_HOST environment variable:

DOCKER_HOST=ssh://joan@production-server docker ps

This command lists the containers on the remote server, not yours. The client is exactly the same binary; only who it talks to changes. It is a perfect demonstration that the client runs nothing.

  1. The dockerd daemon

dockerd is the process that does the work. It runs as a system service (on Linux, managed by systemd) with root privileges, because it needs to manipulate the kernel: create namespaces, configure virtual network interfaces, mount filesystems.

You can look at it with the usual system tools:

systemctl status docker
ps -ef | grep dockerd

The first line shows you the service's state (active (running)); the second, the actual process and its arguments. You will see something like /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock, where -H fd:// means it listens on the socket systemd hands it and --containerd= points to which containerd it talks to (we will see that in section 6).

Its responsibilities:

  • Expose the REST API and serve clients.
  • Manage the lifecycle of images, containers, volumes and networks.
  • Pull images from and push images to registries.
  • Build images (delegating to BuildKit, the subject of module 2 and lesson 05-05).
  • Configure the containers' virtual network.
  • Delegate actual execution to containerd.

One important detail: if dockerd restarts, your containers need not die. Thanks to the separation with containerd that you will see shortly, running containers can survive a daemon restart.

  1. The REST API and the /var/run/docker.sock socket

All communication between client and daemon is a REST API over HTTP. The striking part is that, by default, that HTTP does not travel over the network: it travels over a Unix socket, a special file in the filesystem:

ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Aug  4 09:12 /var/run/docker.sock

Read it carefully, because it explains things from the previous lesson:

  • The leading s means it is a socket, not a regular file.
  • The owner is root and the group is docker.
  • The rw-rw---- permissions mean: root can read and write, the docker group can too, and nobody else.

There it is, in a single line, why you needed sudo or membership in the docker group. And also why belonging to the docker group is equivalent to being root: whoever can write to that socket can ask the daemon (which is root) for anything.

The API being HTTP has an amusing consequence: you can talk to Docker without using the client. With curl:

curl --unix-socket /var/run/docker.sock http://localhost/version
{"Platform":{"Name":"Docker Engine - Community"},"Version":"28.1.1","ApiVersion":"1.49","Os":"linux","Arch":"amd64"}

What you just did: --unix-socket tells curl to use that socket file instead of opening a TCP connection; http://localhost/version is the endpoint's path (the host is irrelevant, it is only needed to form a valid URL). The response is raw JSON: exactly the same thing docker version shows you nicely formatted.

Another example, listing containers:

curl --unix-socket /var/run/docker.sock http://localhost/v1.49/containers/json

It returns a JSON array of the running containers: the exact equivalent of docker ps. This is not a party trick: it is how graphical tools, IDE plugins and monitoring systems work under the hood.

Security warning. Mounting /var/run/docker.sock inside a container is a practice you will see in tutorials (CI tools, dashboards). It amounts to giving that container root over the host. It is covered in detail in lesson 05-03.

The daemon can also listen on TCP (-H tcp://0.0.0.0:2376), which is essential for remote access, but never without TLS and authentication: an unprotected daemon exposed on the internet gets compromised in minutes.

  1. The full flow of a docker run

Now for the complete journey of the course's most common command:

docker run -d --name aurora-web-demo -p 8080:80 nginx:alpine
sequenceDiagram
    participant U as You (terminal)
    participant C as docker client
    participant D as dockerd daemon
    participant R as Registry (Docker Hub)
    participant CD as containerd
    participant RC as runc
    participant K as Linux kernel

    U->>C: docker run -d -p 8080:80 nginx:alpine
    C->>D: POST /images/create (if the image is missing)
    D->>D: Is nginx:alpine available locally?
    alt Not present
        D->>R: GET manifest + layers
        R-->>D: Manifest and layers (blobs)
        D->>D: Unpacks and stores layers
    end
    C->>D: POST /containers/create
    D->>D: Prepares filesystem and network config
    C->>D: POST /containers/{id}/start
    D->>CD: Create and start container
    CD->>RC: Create isolated process per OCI spec
    RC->>K: namespaces + cgroups + mounts
    K-->>RC: Process running
    RC-->>CD: Done (runc exits)
    CD-->>D: Container ID and state
    D-->>C: Container ID
    C-->>U: 3f2a9c1b7e4d...

Step by step, in words:

  1. You type the command. The client parses it and validates the options.
  2. The client calls the API. It translates your order into HTTP requests against the socket.
  3. The daemon looks for the image locally. If nginx:alpine is already downloaded, it jumps to step 5.
  4. If it is missing, the daemon contacts the registry. It asks first for the manifest (the list of layers that make up the image) and then downloads the layers it does not already have. Those downloads are what you see as Pull complete.
  5. The daemon creates the container. It prepares its filesystem by stacking the image's layers and adding a writable layer on top, reserves the name aurora-web-demo, and configures the virtual network and the rule mapping the host's port 8080 to the container's port 80.
  6. The daemon asks containerd to start it.
  7. containerd invokes runc, which is the one that actually tells the kernel: create these namespaces, apply these resource limits, mount this filesystem and launch this process.
  8. runc finishes and disappears. Its job was to start the process, not to supervise it.
  9. The daemon returns the ID to the client, which prints it for you.

The fact that runc disappears after starting the container is the reason Docker can be upgraded or restarted without killing containers: the container's process no longer hangs off it.

  1. containerd and runc: why there are three layers

At first glance, three components to start a process seems excessive. The reason is both historical and strategic.

In the early years, Docker was a monolithic binary that did everything. As containers became critical infrastructure, the industry called for standards so as not to depend on a single vendor. That is where the OCI (Open Container Initiative) came from, publishing two key specifications:

  • OCI Image Spec: how an image is structured (layers, manifest, configuration).
  • OCI Runtime Spec: how a container is described and started from a filesystem and a JSON configuration file.

Docker was broken up to fit those standards:

Component Level Responsibility
dockerd High REST API, image building, networks, volumes, local orchestration
containerd Medium Container lifecycle management, image storage, transfer from registries, supervision
runc Low Create one container from an OCI spec by talking to the kernel, then exit

Think of it as a chain of delegation, each link more specific and simpler than the one before:

flowchart TB
    CLI["docker client"] -->|REST API| DAEMON["dockerd<br/>(high level)"]
    DAEMON -->|gRPC| CTRD["containerd<br/>(mid level, ecosystem standard)"]
    CTRD -->|runs| SHIM["containerd-shim<br/>(supervises the container)"]
    SHIM -->|invokes once| RUNC["runc<br/>(low level, OCI Runtime)"]
    RUNC -->|syscalls| KERNEL["Linux kernel<br/>namespaces · cgroups · capabilities"]
    KERNEL --> PROC["Container process"]
    SHIM -.supervises.-> PROC

That intermediate containerd-shim is the piece that explains "restart without casualties": there is one shim per container that stays alive supervising it, collects its exit code and keeps its input/output streams open, so that neither dockerd nor runc needs to remain present.

Why should this matter to you as a user?

  • Because containerd is an ecosystem standard today, and other platforms use it directly (Kubernetes, for instance, talks to containerd without going through Docker). You will see this in module 6.
  • Because it explains why there are compatible alternatives to Docker that run exactly the same images (the subject of lesson 07-05).
  • Because when you read an error mentioning containerd or runc, you will know at which level something is failing.

What exactly happens along that final arrow —what namespaces and cgroups are and how filesystem layers are stacked— is the content of lesson 05-07. Here we stay at the map level.

  1. The objects the daemon manages

The daemon maintains four kinds of object. Everything you will do in this course is creating, listing, inspecting and deleting objects of these four kinds, and the modern CLI is organized literally that way (you will see it in lesson 01-04).

Object What it is Root command Where it is covered in depth
Images Immutable read-only templates docker image Lesson 01-05 and module 2
Containers Running instances of an image docker container Lesson 01-06 and module 3
Volumes Persistent storage independent of the container's lifecycle docker volume Lesson 03-06
Networks Virtual networks connecting containers to each other and to the outside world docker network Lesson 03-05

All of them materialize on disk under the daemon's data directory, usually /var/lib/docker:

sudo ls /var/lib/docker
buildkit  containers  image  network  overlay2  plugins  runtimes  swarm  tmp  volumes

What each relevant item is:

  • overlay2: the image layers and the containers' writable layers. By far the biggest consumer of space.
  • image: the metadata linking images to their layers.
  • containers: configuration and logs for each container.
  • volumes: the volumes' data.
  • network: the virtual networks' configuration.
  • buildkit: the image build cache.

Golden rule: never touch anything under /var/lib/docker by hand. The daemon keeps internal state databases there; editing or deleting files directly is the fastest way to corrupt the installation. Everything is managed with commands.

  1. Registries and their role

A registry is a service that stores and distributes images. It is the architecture's third leg and the one that gives Docker its distribution capability.

The default registry is Docker Hub (docker.io). When you type docker pull nginx:alpine, the client silently completes the name:

nginx:alpine  →  docker.io/library/nginx:alpine

Where docker.io is the registry, library is the namespace of official images and nginx is the repository. That is why in the hello-world output you saw Pulling from library/hello-world.

To use another registry you just name it explicitly:

docker pull ghcr.io/aurora-libros/aurora-api:1.0.0
docker pull registry.internal.auroralibros.local:5000/aurora-api:1.0.0
  • The first pulls from the GitHub Container Registry.
  • The second, from a self-hosted private registry on the company network, listening on port 5000.

Common registry types:

Type Examples When it is used
Managed public Docker Hub, GitHub Container Registry, Quay Base images and open projects
Managed private in the cloud Amazon ECR, Google Artifact Registry, Azure ACR Company images deployed on that cloud
Self-hosted private Harbor, the official registry:2 registry, Nexus Full control, air-gapped networks, regulatory compliance

The registry's role in the architecture is that of store and exchange point: the daemon pushes and pulls images, but does not need it to run what it has already downloaded. A server with no internet connection can spin up containers perfectly well if its images are already local.

Authentication (docker login), publishing and repository organization are covered in module 2, especially in lessons 02-01 and 02-06.

  1. Hands-on demonstration: docker info and docker system df

Let's read the architecture as reflected in the real output of two commands.

docker info

docker info
Client: Docker Engine - Community
 Version:    28.1.1
 Context:    default
 Plugins:
  buildx: Docker Buildx (Docker Inc.)  v0.23.0
  compose: Docker Compose (Docker Inc.) v2.35.1

Server:
 Containers: 4
  Running: 2
  Paused: 0
  Stopped: 2
 Images: 7
 Server Version: 28.1.1
 Storage Driver: overlay2
  Backing Filesystem: extfs
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 containerd version: 05f951a3781f4f2c1911b05e61c160e9c30eaa8e
 runc version: v1.2.5-0-g59923ef
 Kernel Version: 6.8.0-52-generic
 Operating System: Ubuntu 24.04.2 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 8
 Total Memory: 15.35GiB
 Docker Root Dir: /var/lib/docker
 Registry: https://index.docker.io/v1/
 Live Restore Enabled: false

Now that you know the architecture, every block makes sense:

Field What it tells you
Client: / Server: blocks The client-server split in person. They are two different entities
Context: default Which daemon the client is talking to
Containers / Running / Stopped The inventory of "container" objects the daemon holds. Note there are 2 stopped: they exist and they take up disk
Images: 7 The "image" objects stored
Storage Driver: overlay2 How it stacks image layers (lesson 01-05)
Logging Driver: json-file Where container logs go (lesson 05-06)
Cgroup Driver / Version How resource limits are applied (lessons 03-07 and 05-07)
Network: bridge host ipvlan... The available network drivers (lessons 03-05 and 05-01)
containerd version / runc version The two lower layers from section 6, listed explicitly
Kernel Version The host's kernel, which is the one your containers will use
Docker Root Dir The directory from section 7
Registry: https://index.docker.io/v1/ The default registry from section 8
Live Restore Enabled If true, containers survive a daemon restart

On Windows or macOS, notice that Operating System and Kernel Version are not your machine's, but those of Docker Desktop's Linux virtual machine. It is the clearest demonstration of what was explained in lesson 01-02.

docker system df

docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          7         2         1.842GB   1.376GB (74%)
Containers      4         2         12.4MB    8.2MB (66%)
Local Volumes   3         1         248.6MB   102.3MB (41%)
Build Cache     18        0         421.7MB   421.7MB (100%)

This command answers "where is my disk going?". Column by column:

  • TYPE: the object types from section 7, plus the build cache.
  • TOTAL: how many objects of that type there are.
  • ACTIVE: how many are in use. An image is active if any container (even a stopped one) uses it; a volume, if it is mounted by some container.
  • SIZE: total space occupied. Careful: under Images, this number takes into account that shared layers are counted only once (lesson 01-05).
  • RECLAIMABLE: how much you would get back by cleaning up what is unused. In the example, 1.376 GB of images that no container references.

For per-object detail:

docker system df -v

It adds itemized tables: each image with its size and how many containers use it, each container with the size of its writable layer, each volume with its links. It is the tool for pinpointing the specific culprit behind a full disk.

Cleanup (docker system prune) is covered in lesson 01-04.

Common Mistakes and Tips

  • Believing docker "is" Docker. The command is just a client. If you get used to thinking "I ask, the daemon does", you will understand why the daemon can be on another machine, why closing the terminal stops nothing, and why the state is not where you are typing.
  • Confusing "I can't find the container" with "it doesn't exist". If you have several contexts or a DOCKER_HOST set in the environment, you may be querying a different daemon. Check it with docker context ls and echo $DOCKER_HOST.
  • Exposing the daemon over TCP without TLS. Opening -H tcp://0.0.0.0:2375 is handing over the machine. There are botnets continuously scanning that port. If you need remote access, use the SSH context (docker context create --docker host=ssh://...), which is simple and secure.
  • Mounting /var/run/docker.sock inside a container lightly. It is a privilege escalation to root on the host. Sometimes it is necessary, but it must be a conscious decision (lesson 05-03).
  • Touching /var/lib/docker by hand. Deleting directories there to "free up space" corrupts the daemon's state. Always use the CLI commands.
  • Tip: docker info is your first diagnostic. Faced with any odd behavior, look there: version, storage driver, cgroups, architecture and space. Many "mysterious" problems are explained by one line of that output.
  • Tip: remember the delegation chain. dockerd → containerd → shim → runc → kernel. With that outline in your head, low-level error messages stop being noise.

Exercises

Exercise 1: talk to the API without using the client

Without running any docker command (except to compare at the end), obtain via curl over the Unix socket: (a) the daemon's version, (b) the number of running containers, and (c) the list of local images. Then compare each result with its equivalent docker command and explain the relationship between the two.

Exercise 2: follow the layer trail

Run these commands and answer the questions:

docker system df
docker pull nginx:alpine
docker system df

(a) Which figures change and why? (b) How many image objects are active now, and how many in total? (c) If the images' RECLAIMABLE is high, what does that mean exactly in terms of the architecture you have studied?

Exercise 3: locate the failure

For each of these three symptoms, state at which exact point of the chain client → API/socket → daemon → registry → containerd → runc → kernel the problem is, and what you would check:

  1. Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
  2. Error response from daemon: pull access denied for aurora-api, repository does not exist or may require 'docker login'
  3. docker: Error response from daemon: driver failed programming external connectivity on endpoint aurora-web-demo: Bind for 0.0.0.0:8080 failed: port is already allocated.

Solutions

Solution to exercise 1

# (a) daemon version
curl -s --unix-socket /var/run/docker.sock http://localhost/version

# (b) running containers (count of elements in the JSON array)
curl -s --unix-socket /var/run/docker.sock http://localhost/v1.49/containers/json | grep -o '"Id"' | wc -l

# (c) local images
curl -s --unix-socket /var/run/docker.sock http://localhost/v1.49/images/json

Comments:

  • -s silences curl's progress bar so that only the JSON is left.
  • --unix-socket is what makes all of this possible: instead of a TCP connection, curl writes HTTP directly into the socket file. The http://localhost is just syntactic filler to form a valid URL; the hostname is ignored.
  • /v1.49/ is the API version; you can leave it out and the daemon will use the most recent version it supports. If yours differs, check it in the output of docker version (the API version field).
  • In (b), grep -o '"Id"' | wc -l counts how many objects the array contains. With jq installed it would be cleaner: | jq 'length'.

The equivalents are docker version, docker ps and docker image ls. The relationship is direct: the docker client makes exactly these same HTTP calls and merely formats the response into readable tables. Seeing it first-hand is the best way to internalize that the client runs nothing.

Solution to exercise 2

(a) Two things change in the Images row: TOTAL goes up by 1 (there is one more image) and SIZE increases, but usually by less than the image's advertised size, because layers you already had from other Alpine-based images are neither downloaded nor counted twice. On top of that, RECLAIMABLE grows by the new image's size, because you have just downloaded it and no container is using it yet.

(b) TOTAL is the number of stored images; ACTIVE counts only those referenced by some container (even a stopped one). The freshly pulled one adds to the total but not to the active count.

(c) A high RECLAIMABLE means the daemon is holding image layers in /var/lib/docker that no container references. They do no harm beyond the disk they take up, and they act as a cache: if you use that image again, there is nothing to download. They are freed with docker system prune -a, which you will see in lesson 01-04.

Solution to exercise 3

  1. Failure between the client and the socket/daemon. The client cannot even establish the conversation. Two possible causes: the daemon is stopped (check with systemctl status docker; on Windows/macOS, that Docker Desktop is started) or your context points at an endpoint that does not answer (check docker context ls and echo $DOCKER_HOST). Note the difference from permission denied: that would be a permissions problem on the socket, not a connection problem.

  2. Failure between the daemon and the registry. The message begins with Error response from daemon, so the client reached the daemon without trouble; it was the daemon that could not obtain the image. You would check: that the name and tag are correct, that the repository exists, whether it is private (in which case docker login is missing), and if the registry is internal, that it is reachable from the machine.

  3. Failure in the daemon, during the network configuration phase before startup. The container never even got to containerd/runc: the daemon tried to reserve the host's port 8080 and the operating system refused because it is already taken. You would check what is occupying it (ss -tlnp | grep 8080, or docker ps in case it is another of your containers) and either free that port or publish the container on a different one (-p 8081:80).

Conclusion

Docker is not a program: it is a client-server system. The docker client only translates your orders into calls to a REST API that travels over the /var/run/docker.sock socket —whose permissions explain, at a glance, both the use of sudo and the fact that the docker group is equivalent to root. On the other side, the dockerd daemon does the real work and delegates downwards to containerd (lifecycle and storage) and runc (creating the isolated process per the OCI specification), a layered separation that exists to honor standards and that keeps the ecosystem from depending on a single product.

The daemon holds four kinds of object —images, containers, volumes and networks— in /var/lib/docker, and uses registries only to exchange images, not to run them. All of that is reflected, literally, in the output of docker info and docker system df, which you now know how to read.

With the mental map complete, you can start issuing orders with real judgment. In the next lesson, Basic Docker Commands, you will see that the CLI is organized exactly around those four objects (docker <object> <action>), you will learn the essential commands for images, containers and the system, and you will walk through a complete annotated session from start to finish.

Docker: From Beginner to Advanced

Module 1: Introduction to Docker

Module 2: Working with Docker Images

Module 3: Docker Containers

Module 4: Docker Compose

Module 5: Advanced Docker Concepts

Module 6: Docker in Production

Module 7: Docker Ecosystem and Tools

© Copyright 2026. All rights reserved