OnCallReady

Lesson 10.1 · Images & Builds · 21 min read

Installing Docker, the daemon, and the socket

In plain words

Think of a restaurant. You sit at a table and tell the waiter what you want; the waiter carries your order to the kitchen, and the cooks do the actual work. The waiter can only get into the kitchen through one door, and only staff with a badge may use it.

The docker command is the waiter: a client that only passes on orders. dockerd is the kitchen (with containerd and runc as the cooks), running as root under systemd. The door is the Unix socket /run/docker.sock, owned by root and the docker group. Joining that group gives you a badge for the door. And since the kitchen runs as root, that badge is effectively root on the whole box.

Why this chapter exists

An app that runs on your laptop often fails on the server: a different library version, a missing package, a different Java. Docker fixes that by packing the program together with every file it needs into one sealed bundle, an image, that runs the same way on any Linux box. A running copy of an image is a container. This lesson installs Docker and shows you the parts it is made of, so its first two errors do not scare you.

What you need to know already: apt and sudo (Ch 1, "Packages and sudo"), systemd units, systemctl and journalctl (Ch 2), users, groups and file permissions (Ch 4, "Permission bits and octal").

Three packages

On Ubuntu the Docker engine comes from the normal package archive as docker.io. Two helpers are separate packages:

$ sudo apt update
$ sudo apt install -y docker.io docker-buildx docker-compose-v2

docker-buildx matters more than it looks. Without it, docker build falls back to the old legacy builder, prints a deprecation warning, and none of the BuildKit features in this chapter (build checks, cache mounts, secret mounts, skipping unused stages) exist:

# on a box with docker.io but without docker-buildx (not this one any more)
docker build -t app .
DEPRECATED: The legacy builder is deprecated and will be removed in a future release.
            Install the buildx component to build images with BuildKit:
            https://docs.docker.com/go/buildx/

Sending build context to Docker daemon  3.07kB
Step 1/4 : FROM eclipse-temurin:21-jre

If you ever see Sending build context to Docker daemon and Step 1/4, you are on the legacy builder. (What those lines mean comes later in this chapter.)

What actually got installed

The docker command you type is only a client: it sends requests and prints the answers. The real work is done by a daemon - a background program that runs all the time, started by systemd, exactly like the services in Ch 2:

$ systemctl status docker --no-pager
● docker.service - Docker Application Container Engine
     Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; preset: enabled)
     Active: active (running) since Wed 2026-09-23 09:12:40 UTC; 3min ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 2210 (dockerd)

(--no-pager prints straight to the terminal instead of opening less.) TriggeredBy: docker.socket means systemd starts dockerd the first time someone connects to it. There are four programs behind docker:

dockerd      the Docker engine: images, networks, volumes, the HTTP API
containerd   the container supervisor dockerd delegates to
runc         the tiny binary that actually creates namespaces and cgroups
containerd-shim-runc-v2   one per container: the parent of your container's PID 1

An API is the set of requests a program accepts from other programs; dockerd accepts HTTP requests like the ones you read with curl in Ch 9. Namespaces and cgroups are the kernel features that turn a process into a container - the next lesson proves it. You met cgroups already as the MemoryMax= limits of a systemd unit (Ch 2, "Hardening and cgroup limits").

Everything you learned about systemd applies: journalctl -u docker is where the daemon logs, systemctl restart docker restarts it, and the unit file is in /usr/lib/systemd/system/docker.service. Read it once with systemctl cat docker. Three lines stand out:

The socket, and the error everybody meets on day one

The client talks to dockerd over a Unix socket: a special file that two programs on the same machine use to talk to each other, like a TCP port (Ch 9) but addressed by a path instead of a number. Look at its permissions:

$ ls -l /run/docker.sock
srw-rw---- 1 root docker 0 Sep 23 09:12 /run/docker.sock

The first letter s means socket. Owner root, group docker, mode 660 (read and write for owner and group, nothing for others). So as a normal user:

$ docker ps
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.47/containers/json": dial unix /var/run/docker.sock: connect: permission denied

(docker ps lists running containers. /var/run is a symlink to /run, so both paths are the same file.) Read the error: it is a plain filesystem permission on a socket, the same rules as Ch 4. The fix is group membership:

$ sudo usermod -aG docker $USER

usermod changes a user account; -G docker sets the supplementary groups, -a means append to the ones you have. Without -a, -G docker replaces your supplementary groups - and removes you from sudo. That is a way to lock yourself out of your own server.

Then the part people miss: group membership is read at login. Your current session still has the old group list:

$ id
uid=1000(learner) gid=1000(learner) groups=1000(learner),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),1002(ops)
$ newgrp docker          # or log out and ssh back in
$ id
uid=1000(learner) gid=989(docker) groups=989(docker),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),1000(learner),1002(ops)

id prints your user id, your primary group and all your groups. newgrp docker starts a new shell with docker as the primary group, so you do not have to log out.

The docker group is root. Literally.

dockerd runs as root, and it does whatever the socket asks. So anyone who can talk to the socket can do anything root can do. For example, mount (make visible) the host's /etc inside a container and read it:

$ cat /etc/shadow
cat: /etc/shadow: Permission denied
$ docker run --rm -v /etc:/host-etc alpine:3.20 cat /host-etc/shadow
root:*:20340:0:99999:7:::
daemon:!:20340:0:99999:7:::
...

docker run starts a container from an image (here alpine:3.20, a tiny Linux image) and runs a command in it (cat ...). --rm deletes the container when the command ends. -v /etc:/host-etc makes the host's /etc appear inside the container at /host-etc.

No password was asked. Treat membership of docker exactly like an entry in sudoers, and treat -v /var/run/docker.sock:/var/run/docker.sock in somebody's config the same way: that container can now control the host. (Rootless Docker and Podman, a Docker alternative, exist largely because of this.)

Checking the install

$ docker version
Client:
 Version:           27.5.1
 API version:       1.47
 ...
Server:
 Engine:
  Version:          27.5.1

Two sections: Client: is the docker command, Server: is dockerd. If you only get Client: followed by Cannot connect to the Docker daemon or a permission error, the client works and the daemon is not reachable.

docker info prints the engine's settings. Four you will meet again: the storage driver (overlay2, next lesson), the cgroup driver (systemd), the cgroup version (2) and the Docker root dir (/var/lib/docker, where images and containers live on disk).

$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
...
Hello from Docker!
This message shows that your installation appears to be working correctly.

"Pulling" means downloading the image from the internet; you will read that output properly in the images lesson.

Two errors to recognise instantly

permission denied while trying to connect to the Docker daemon socket ...
    -> you are not in the docker group (or have not logged in again)

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
    -> dockerd is down: systemctl status docker, journalctl -u docker

The second one often follows a bad edit to /etc/docker/daemon.json, dockerd's settings file. Invalid JSON (Ch 7, jq) and dockerd refuses to start. journalctl -u docker -n 20 (-n 20: the last 20 lines) shows the exact parse error.

What you can now do

Why it helps

Day one on any new build agent or VM, you meet one of two errors: permission denied ... docker.sock (you are not in the group, or your session predates it) or Cannot connect to the Docker daemon (dockerd is down, often from a broken /etc/docker/daemon.json). Knowing which is which saves an hour, and journalctl -u docker is the same move as for any systemd service. Knowing the socket is root also matters in reviews: when a teammate's setup mounts /var/run/docker.sock into a CI or monitoring container, or someone asks to be added to the docker group on a shared host, you can explain that this hands out root without a password. And if builds print Sending build context to Docker daemon, you know BuildKit is missing and none of its features exist.

Commands in this lesson

apt systemctl ls docker usermod id newgrp cat

FAQ

Why do I get permission denied right after usermod -aG docker $USER?

Group membership is read when you log in. Your current shell still carries the old group list, which id confirms. Log out and SSH back in, or run newgrp docker for a subshell with the new group. It is the same rule as any Unix group change, not something special to Docker.

What goes wrong if I forget the -a in usermod -aG?

-G alone sets your supplementary groups to exactly the list you give. So usermod -G docker learner removes you from sudo, adm and everything else. On a server where you are the only admin, the next login leaves you without sudo and without a way back except the console or recovery mode. Always -aG, and check with id before you log out.

Is being in the docker group really the same as root?

Yes. The daemon runs as root and does what the socket asks. docker run -v /:/host alpine gives you the host's whole filesystem, --privileged gives you the devices, and none of it asks for a password or leaves a sudo log line. Treat the group like a sudoers entry. Rootless Docker and Podman exist largely to remove this problem.

Why do I need docker-buildx if docker build already works?

Without the buildx plugin, docker build falls back to the deprecated legacy builder. It still builds, but without BuildKit: no build checks, no cache or secret mounts, no parallel stages, no skipping unused stages, no multi-platform builds. The tell is the output format: Sending build context to Docker daemon and Step 1/4 mean legacy; [+] Building and => [1/4] mean BuildKit.

What are dockerd, containerd, runc and the shim, and why so many?

dockerd is the Docker engine: API, images, networks, volumes. It delegates starting and stopping containers to containerd, which other container tools also use directly. runc is a small binary that creates the namespaces and cgroups and starts your process, then exits. The shim stays as the parent of each container's PID 1, so containers keep running when dockerd or containerd restarts.

In an interview Junior

docker ps fails. How do you tell whether it is a permissions problem or the daemon being down?

The docker command is only a client; it talks to dockerd over the Unix socket /run/docker.sock (owner root, group docker, mode 660). Read the error:

docker version shows both: a Server: section only when the daemon answers. And remember the docker group is equivalent to root.

Also asked: Why is membership of the docker group equivalent to root? · What is the difference between the docker client and dockerd? · What is the risk of mounting /var/run/docker.sock into a container?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.