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.io- the engine and thedockercommand.docker-buildx- BuildKit, the modern image builder (the thing that runs when you typedocker build).docker-compose-v2- Compose, a tool that starts several containers from one file. You will not need it until Ch 11.
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:
Restart=always- systemd restarts dockerd if it dies.Delegate=yes- Docker manages its own cgroups below the unit.KillMode=process- stopping dockerd does not kill every container process in its cgroup.
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
- Install Docker on Ubuntu and name the four programs behind the
dockercommand. - Tell "not in the docker group" from "daemon is down" by the error text.
- Explain why the docker group is equivalent to root.