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
- Docker is client-server, not a program
- The
dockerclient - The
dockerddaemon - The REST API and the
/var/run/docker.socksocket - The full flow of a
docker run - containerd and runc: why there are three layers
- The objects the daemon manages
- Registries and their role
- Hands-on demonstration:
docker infoanddocker system df
- 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
dockercommand. - 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.
- The
docker client
docker clientThe client is surprisingly dumb, and that is a good thing. Its responsibilities are:
- Parse what you type (
docker run -d --name web nginx). - Turn it into one or more calls to the daemon's REST API.
- 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:
NAME DESCRIPTION DOCKER ENDPOINT ERROR
default * Current DOCKER_HOST based configuration unix:///var/run/docker.sockHow 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:
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.
- The
dockerd daemon
dockerd daemondockerd 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:
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.
- The REST API and the
/var/run/docker.sock socket
/var/run/docker.sock socketAll 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:
Read it carefully, because it explains things from the previous lesson:
- The leading
smeans it is a socket, not a regular file. - The owner is
rootand the group isdocker. - The
rw-rw----permissions mean: root can read and write, thedockergroup 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:
{"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:
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.sockinside 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.
- The full flow of a
docker run
docker runNow for the complete journey of the course's most common command:
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:
- You type the command. The client parses it and validates the options.
- The client calls the API. It translates your order into HTTP requests against the socket.
- The daemon looks for the image locally. If
nginx:alpineis already downloaded, it jumps to step 5. - 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. - 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. - The daemon asks containerd to start it.
- 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.
- runc finishes and disappears. Its job was to start the process, not to supervise it.
- 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.
- 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
containerdorrunc, 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.
- 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:
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.
- 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:
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.
- Hands-on demonstration:
docker info and docker system df
docker info and docker system dfLet's read the architecture as reflected in the real output of two commands.
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: falseNow 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
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:
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_HOSTset in the environment, you may be querying a different daemon. Check it withdocker context lsandecho $DOCKER_HOST. - Exposing the daemon over TCP without TLS. Opening
-H tcp://0.0.0.0:2375is 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.sockinside 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/dockerby hand. Deleting directories there to "free up space" corrupts the daemon's state. Always use the CLI commands. - Tip:
docker infois 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:
(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:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?Error response from daemon: pull access denied for aurora-api, repository does not exist or may require 'docker login'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/jsonComments:
-ssilencescurl's progress bar so that only the JSON is left.--unix-socketis what makes all of this possible: instead of a TCP connection,curlwrites HTTP directly into the socket file. Thehttp://localhostis 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 ofdocker version(theAPI versionfield).- In (b),
grep -o '"Id"' | wc -lcounts how many objects the array contains. Withjqinstalled 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
-
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 (checkdocker context lsandecho $DOCKER_HOST). Note the difference frompermission denied: that would be a permissions problem on the socket, not a connection problem. -
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 casedocker loginis missing), and if the registry is internal, that it is reachable from the machine. -
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, ordocker psin 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
- What Is Docker?
- Installing Docker
- Docker Architecture
- Basic Docker Commands
- Understanding Docker Images
- Creating Your First Docker Container
- The Course Project: The Aurora Libros Platform
Module 2: Working with Docker Images
- Docker Hub and Repositories
- Building Docker Images
- Dockerfile Basics
- Advanced Dockerfile Instructions
- Managing Docker Images
- Tagging and Publishing Images
Module 3: Docker Containers
- Running Containers
- Container Lifecycle
- Managing Containers
- Inspecting and Debugging Containers
- Docker Networking
- Data Persistence with Volumes
- Resource Limits and Restart Policies
Module 4: Docker Compose
- Introduction to Docker Compose
- Defining Services in Docker Compose
- Docker Compose Commands
- Multi-Container Applications
- Environment Variables in Docker Compose
- Profiles, Overrides and Multiple Environments
- Local Development with Docker Compose
Module 5: Advanced Docker Concepts
- Docker Networking Deep Dive
- Docker Storage Options
- Docker Security Best Practices
- Optimizing Docker Images
- Advanced Builds with BuildKit and Buildx
- Logging and Monitoring in Docker
- The Runtime Inside: Namespaces, Cgroups and Layers
Module 6: Docker in Production
- Preparing an Image for Production
- CI/CD with Docker
- Orchestrating Containers with Docker Swarm
- Introduction to Kubernetes
- Deploying Docker Containers in Kubernetes
- Scaling and Load Balancing
- Deployment Strategies and Rollback
