Most people learn docker run as a magic word. It is worth about twenty minutes to learn what it actually calls, because almost every confusing Docker error comes from one specific layer of the stack, and knowing the layers tells you which log to read.
Topic 1: Client–Server, Not a Single Program
Docker uses a client-server architecture. The docker binary you type is a client. It does almost nothing itself.
| Component | Role |
|---|---|
| Docker CLI | Parses your command, turns it into an HTTP request |
| REST API | The interface the daemon exposes; everything the CLI can do, a program can do |
Docker daemon (dockerd) | Long-running process that builds images, manages containers, networks and volumes |
On Linux the CLI talks to the daemon over a Unix socket at /var/run/docker.sock. That single fact explains two things you will meet later:
docker version # prints Client and Server blocks separately — they are separate programs
docker info # daemon-side view: storage driver, cgroup driver, container counts
If docker version shows a client but errors on the server block, the daemon is not running or you cannot reach its socket. That is a completely different problem from an image or container fault, and it is the first thing to check.
Why you are told to add your user to the docker group:
sudo usermod -aG docker "$USER" # log out and back in for it to take effect
sudo systemctl enable --now docker
Membership of the docker group grants write access to the daemon socket. Be clear-eyed about what that means: anyone who can talk to that socket can start a container that mounts the host’s root filesystem, which is effectively root on the host. Adding a user to the docker group is granting root. It is fine on your laptop; on a shared build server it is a security decision, not a convenience.
Topic 2: What docker run hello-world Actually Does
Walk the whole path once:
- The CLI serialises your command into a REST call and sends it to
dockerd. - The daemon looks for the image
hello-worldin its local image store. - Not there? The daemon contacts the configured registry — Docker Hub by default — and pulls the image, layer by layer.
- The daemon asks containerd to create a container from that image.
- containerd unpacks the image layers into a filesystem bundle and calls runc.
runcforks, sets up namespaces and cgroups for the new process, andexecs the container’s entrypoint. Then runc exits — it is not a supervisor.- Output is streamed back up: container → containerd → daemon → your terminal.
Run the same command a second time and step 3 disappears. The image is already local; the daemon goes straight to creation. That is why the second run feels instant.
docker CLI ──REST──▶ dockerd ──gRPC──▶ containerd ──▶ runc ──▶ your process
(exits after setup)
Topic 3: Why the Runtime Is Split Up
Early Docker created containers by shelling out to LXC. Linux releases kept changing LXC’s behaviour underneath Docker, so Docker wrote its own container-creation library — runc — and later donated the runtime layer to the community. The result is the layering above, and it buys two concrete things:
Standardisation. runc implements the OCI runtime specification and images follow the OCI image specification. That is why Podman, containerd, CRI-O and Kubernetes can all consume the images you build with Docker. You are not producing a Docker-proprietary artefact; you are producing an OCI image.
Daemon-independent container lifetime. Because runc exits after setup and containerd supervises containers separately from dockerd, you can upgrade or restart the Docker daemon and running containers keep running. Try it — it is the hands-on task for this lesson. This is not a trivia point; it is what makes patching the Docker engine on a production host something other than an outage.
Topic 4: Images and Containers Are Different Things
The single most common conceptual mix-up, so state it precisely:
- An image is a read-only packaging format: your application code or binaries, the runtime, the dependencies, and the filesystem around them. It is inert. It cannot be edited — only deleted or replaced by building a new one.
- A container is a running (or stopped) instance of an image, with a thin writable layer on top.
One image, many containers. Start ten containers from nginx:1.27 and you have one copy of the image on disk plus ten small writable layers. This is the density story from lesson 1 made concrete.
docker pull nginx # gets nginx:latest — pin the tag instead
docker pull nginx:1.27-alpine # name:tag, the naming convention for every image
docker images # local image store: repository, tag, image ID, size
docker rmi nginx:1.27-alpine # delete by name:tag, or by image ID
docker rmi nginx with no tag deletes nginx:latest specifically, not everything called nginx. That surprises people at least once.
Topic 5: The Container Lifecycle and Its States
A container is not simply on or off. There are six states, and docker ps -a will show you every one of them:
| State | Meaning |
|---|---|
created | Created (e.g. by docker create) but never started |
running | Currently executing its main process |
paused | Processes frozen with the freezer cgroup — still resident, not scheduled |
restarting | In the middle of a restart, driven by a restart policy |
exited | Ran and stopped; the writable layer still exists |
dead | The daemon tried to stop it and failed, usually a busy device or mount |
The lifecycle verbs map onto those states directly:
docker create alpine sleep 1d # → created
docker start <id> # → running
docker pause <id> # → paused
docker unpause <id> # → running
docker stop <id> # SIGTERM, wait, then SIGKILL → exited
docker kill <id> # SIGKILL immediately → exited
docker restart <id> # stop then start
docker rm <id> # destroy, releasing the writable layer
Three details that come up constantly:
docker run is create + start. Nothing more mysterious than that.
You cannot docker rm a paused container. It must be stopped first. docker rm -f will force it by killing the container, which is exactly what you are trying to avoid on anything that matters.
Prefer stop then rm over rm -f. docker stop sends SIGTERM and gives the process its grace period (10 seconds by default) to flush buffers, close connections and exit cleanly. rm -f does not extend that courtesy in the same way. On a database or a queue consumer, the difference is between a clean shutdown and a recovery on next start.
docker stop api && docker rm api # the polite version
docker rm -f $(docker ps -aq) # the "clear my laptop" version
Topic 6: Reading the State of a Host
Four commands, and you can describe any Docker host you are dropped onto:
docker ps # running containers
docker ps -a # every container, including exited ones
docker info # daemon-wide: driver, cgroup version, counts, warnings
docker stats # live CPU, memory, network and block I/O per container
docker events # a live stream of everything the daemon is doing
docker info deserves a slow read the first time. It tells you the storage driver (should be overlay2 on any modern Linux — lesson 5), the cgroup version (v2 on current distributions, which changes how memory limits are reported), and the counts of running, paused and stopped containers. Warnings at the bottom of that output are real warnings; do not scroll past them.
docker stats and docker events are the two monitoring primitives the engine gives you: stats for resource usage, events for daemon activity. Neither is a monitoring system, but at 3am on a single host, both are faster than one.
Try it yourself: Run docker events in one terminal, then in another run docker run --rm alpine echo hi. Watch the create, attach, start, die and destroy events go past in order. That stream is the daemon narrating the lifecycle from Topic 5.
Common mistake: Assuming docker ps showing nothing means the host is clean. docker ps hides exited containers, and every exited container is still holding its writable layer on disk. docker ps -a is the honest view — and a host that has been running CI for a year without docker system prune is usually where the missing disk space went.