git clone --filter=blob:none --sparse https://github.com/calmict/book_labs.git cd book_labs git sparse-checkout set docker/ed1
The journey begins by dismantling the first illusion: that you have switched on a small machine. In this lab you build a container without Docker, using only the unshare command, and prove with your own eyes what the chapter announces — a container is not a ship of its own, it is an ordinary Linux process that the kernel has told a restricted version of reality. Before you board Docker's engine, feel for yourself what the hold is made of.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap01/solution ./run.sh
In chapter 1 you detached a process from the list of the others with a single flag, and that process found itself PID 1 of a world of its own. But that flag was a door onto a whole family of isolations. In this lab you open almost all of them at once — hostname, processes, mounts, network, and the user mapping — and prove, room by room, that a container's isolation is not one single wall but the sum of several views the kernel agrees not to show.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap02/solution ./run.sh
The namespaces of chapter 2 decide *what* a process sees. But the other half of isolation is missing, and without it a container would be a dangerous neighbour: *how much* it can consume. In this lab you impose a memory ceiling on a process by hand and force it to break through, to watch the OOM killer strike with your own eyes — and to read that exit code 137 you will meet again in production. All rootless, because systemd's delegation (3.7) gives you a piece of the cgroup tree with no need for sudo.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap03/solution ./run.sh
One last illusion remains to dismantle, the most convincing one: when you enter a container and type ls, you see bin, etc, usr — it looks like another Linux installation. In chapter 2 you sensed the how (a MNT namespace with a different root); here you mount by hand the piece that makes it *efficient*: Copy-on-Write with OverlayFS. You will stack read-only layers, write on top, and see the magic — only one page is copied, and only when you write it. All rootless, inside the namespaces of chapter 2.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap04/solution ./run.sh
In Part 1 you drove namespaces, cgroups and overlay by hand. Now you open the hood of the tool that drives them for you — and the first surprise is that "Docker" is not one program but a chain of components that pass the work along. In this lab you follow a request from the socket to the kernel: you talk to the daemon bare-handed, map the chain, and prove the container's parent is the shim, not the daemon. It is the technical proof behind an important promise — you can upgrade Docker without killing your containers.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap05/solution ./run.sh
In chapter 5 you saw a chain of distinct links. But why so much fragmentation? The answer is one word: standards. In this lab you go down to the last link and build and run an OCI container by hand with runc — with no Docker in the loop. You will generate the config.json, the exact recipe into which all of Part 1 condenses, and see that runc does nothing but execute it to the letter. Change the recipe and the container changes: because the config.json is the container.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap06/solution ./run.sh
We close the engine's architecture by following a container from birth to death — and almost every trap has the same root: that PID 1 you met "bare-handed" in chapter 1. In this lab you compare two containers facing docker stop: one whose PID 1 ignores SIGTERM, and one with a real init in the right place. You measure the difference — ten seconds versus an instant — and understand, stopwatch in hand, why so many containers "always take ten seconds" to stop.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap07/solution ./run.sh
With the engine's architecture behind us, the craft begins: images. And the first surprise is that an image is not a monolithic block but a stack of layers plus a manifest that lists them — just as a ship is not a hull filled at random but stacked containers and a manifest saying what is aboard and in what order. In this lab you dissect an image bare-handed: you count its layers, read the sha256 digest that seals it, and prove why two different images share the same layers without copying them.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap08/solution ./run.sh
In chapter 8 you took an image apart into its layers; now you build one yourself, with intent. The Dockerfile is the ship's loading plan: an ordered list of instructions saying which hull to start from, what to load, where to put it, and which command to run on departure. In this lab you complete a Dockerfile with the fundamental instructions — COPY, ENV, CMD — and check that the built image behaves exactly as you declared it: the file in the right place, the variable set, the default command starting on its own.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap09/solution ./run.sh
You gave a default command with CMD; but who really commands at departure? A container has a single process in the place of honour — the PID 1 you met in chapter 7 — and two instructions decide who it is and what it runs: ENTRYPOINT and CMD. The metaphor is the captain and the orders: ENTRYPOINT is the ship's fixed captain, CMD are the default orders, which can be changed at departure. In this lab you combine them, see how arguments passed to docker run override CMD but not ENTRYPOINT, and check that the exact form you write them in decides whether your process is PID 1 or ends up wrapped in a shell.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap10/solution ./run.sh
You can build an image; now you learn to build it fast and ship it light. Two techniques make the difference, and both are about order. The first is the cache: Docker reuses a layer as long as the instruction that produces it and everything below are unchanged — so the order of instructions decides how much work you redo on every build. The second is the Multi-Stage build: you use one stage as a warehouse where you assemble with all the tools, then load onto the final ship — a light one — only the finished goods. In this lab you combine them: put what rarely changes before what changes often, and ship only the artifact.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap11/solution ./run.sh
An image that works on your machine is not yet an image to put to sea. Part 3 closes by taking the idea all the way: in production the ship must be light and well guarded — only the crew you need, no keys too many. The key too many, almost always, is root: by default a container runs as root, and a compromised process that is root inside the container is far more dangerous than one that is not. In this lab you build a production image that runs as an unprivileged user, owning only what it needs — and you verify, permission in hand, that it cannot write where it must not.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap12/solution ./run.sh
A container is a ship: it sets sail, makes its voyage, and sooner or later is scrapped. Everything you write in its hold — the writable layer you met in chapter 8 — goes down with it when you remove it. It is the surprise that catches everyone the first time: you run a database, populate it, remove the container to upgrade it, and the data is gone. Part 4 answers this question — if the container is ephemeral, where do the data live? — and the answer is: ashore. In this lab you see first-hand that the container's layer dies with it, and that a volume, kept by the daemon ashore, outlives the ship.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap13/solution ./run.sh
In chapter 13 you saw that data must be kept ashore, not in the container's hold. But "ashore" has three different addresses, and choosing wrong is costly. The volume is the port warehouse: the daemon manages it, it is portable and made for data. The bind mount is a dock shared with the host: you mount a folder of your machine inside the container, and what you write is seen on both sides — handy in development, delicate with permissions. The tmpfs is the ship's fast locker: it lives in memory, never touches disk, and empties on arrival. In this lab you use all three and verify the trait that sets each apart.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap14/solution ./run.sh
You learned to run as a non-root user (chapter 12) and to mount shared data (chapter 14). Put the two together and you trip over the classic: the non-root container tries to write to the volume and is told "permission denied". The reason is that at a mount's boundary permissions are read not by name but by number: what counts is the UID, a numeric badge. If the container's number does not own the mounted files, it does not write — full stop. In this lab you reproduce the mismatch, fix it by running the container with the right UID, and verify that the number crosses the boundary unchanged: UID N inside is UID N on the host.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap15/solution ./run.sh
So far each container was an island of processes and of data; now you discover it is also an island of network. Part 5 opens the labyrinths of networking, and the first truth is that "giving a container a network" is not magic: Docker uses the same Linux kernel building blocks. Every container gets its own network namespace — a network stack all of its own, with its interfaces, its IP, its routing table — and is joined to the world by a virtual cable, the veth pair: one end inside the container (eth0), the other on the host, attached to the shared switchboard, the docker0 bridge. In this lab you verify it first-hand: two containers, two stacks, two addresses, each with its own cable.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap16/solution ./run.sh
In chapter 16 you saw the shared switchboard: the docker0 bridge, where every container has a number (an IP) but no name. Fine for understanding the mechanism, poor for building on: IPs change on every restart, and every container lands on the same public board. The answer is to open a private switchboard — a bridge network you define. There Docker adds two things that change everything: a directory (an embedded DNS, so containers call each other by name) and an isolated line (containers on one network do not see those on another). In this lab you contrast the two worlds: on the default bridge names do not work, on your bridge they do, and whoever is off the network stays out.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap17/solution ./run.sh
Default bridge, custom bridge: so far every container had its own network stack, isolated and connected. But it is not the only way. There are two extremes, and choosing them is a design decision. On one side the host driver: the container has no network of its own, it is plugged straight into the host's socket — sharing its stack, its interfaces, its ports. No isolation, no NAT, maximum speed, maximum exposure. On the other side the none driver: the container has its own namespace but is unplugged — only loopback, no cable to the world. In this lab you touch the three drivers side by side and see what changes: who shares the host's stack, who has no network at all, and the bridge in between.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap18/solution ./run.sh
Until now containers lived behind the switchboard: a private IP, NAT filtering between them and the real network. Fine for most cases, but sometimes you need more: for the container to appear on the physical network as a machine of its own, with its own address and its own MAC, unmediated. That is the macvlan driver — the container on the quay, no longer behind the glass. In this lab you give two containers a direct address on a parent interface's network and verify that each has its own MAC and that they talk to each other on the same segment. Then you look beyond the single host's horizon: ipvlan (the variant that shares the MAC) and overlay (the network that spans several hosts), the bridge toward orchestration.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap19/solution ./run.sh
So far you commanded one ship at a time: docker run, docker network, docker volume, one piece at a time. But a real application is a fleet — a web, a database, a cache — and coordinating it by hand, command after command, is fragile and unrepeatable. Part 6 introduces the tool that describes the whole fleet in a single file: Docker Compose. In one file you declare the services, and Compose does the rest — it creates for you an application network where services find each other by name (like the custom bridge of chapter 17, but without writing it), respects dependencies, and starts or stops everything with one command. In this lab you design a two-service app and verify that they talk by name and start in the right order.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap20/solution ./run.sh
In chapter 20 you started web after db with depends_on. But "started" is not "ready": a database container can be up while the database inside is still loading, and web connecting at that instant finds the door closed. depends_on on its own waits for the container to exist, not for the service to be ready to accept traffic. You need an all-clear signal. In Docker that signal is the healthcheck: the service declares how you can tell it is truly ready, and whoever depends on it can wait for that all-clear instead of guessing with a sleep. In this lab you give db a healthcheck that turns green only after a delay, and make web wait until db is healthy — not just started.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap21/solution ./run.sh
An application is not only services and networks: it is also configuration. The database address, the log level, the API key — and, among these, things that must not end up in the clear anywhere. Docker Compose offers three tools that are easy to confuse. Environment variables configure the service's behaviour. The .env file keeps the values out of the compose and out of the repository. And secrets are the safe: sensitive data mounted into the container as restricted-permission files, not written into the environment where anyone inspecting the container would see them. In this lab you configure a service with a variable taken from .env and give it a password as a secret — and verify that the secret is in the right file and does not leak into the environment.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap22/solution ./run.sh
We open Part 7 — security and day-2 — from the question underneath everything: who is root, really? In classic Docker the daemon runs as root on the host, and whoever can talk to it (the docker group) is root to all intents. A container running as root is root on the host for the files it mounts, and an escape is an escape from root. Rootless mode flips the picture using the USER namespace of chapter 2: the daemon and the containers run inside a namespace where you are "root", but that root is mapped to an unprivileged user on the host. You are king in your own room, an ordinary user outside. In this lab you touch the mapping first-hand: inside you are uid 0 with all the capabilities, outside you are your own user, and that "root" can do nothing privileged on the host.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap23/solution ./run.sh
In chapter 23 you saw who root is; now you see that root is not one block. The powers of root are split by the kernel into many separate keys — the capabilities: the right to open raw sockets, to bind low ports, to mount filesystems, to change owners. A container almost never needs all of them. The principle is that of a well-made safe: give each one only the key it needs. And above the capabilities are two more layers — seccomp, which filters syscalls, and AppArmor or SELinux, which confine what a process may touch. Defence in depth. In this lab you touch the capabilities first-hand: drop everything, and the same operation fails; grant back the right key, and it resumes — without returning all the others.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap24/solution ./run.sh
Security prevents; observability lets you see. When a service misbehaves in production, the first question is always the same: what is it doing? Docker answers with two tools. The logbook is the logs: everything the container writes to standard output and standard error is captured by the daemon and made available with docker logs — even after the fact, even after the process has died. The gauges are the metrics: docker stats shows, in real time, the CPU, memory and network of each container. In this lab you read a container's logbook — both stdout and stderr — find where Docker keeps it (the logging driver) and read its live consumption.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap25/solution ./run.sh
Sooner or later comes the container that will not start, says nothing, and maybe keeps restarting on its own. The logs are empty — mute — and the instinct is to give up. But a container is never truly mute: even when it writes not a line, it leaves a black box. docker inspect tells how it died — the exit code, which as you saw in chapter 7 is already a diagnosis — and how many times it restarted before giving up, the mark of a crash loop. In this lab you take a container that crashes in silence, with a restart policy that keeps restarting it, and reconstruct its story without a single log line: from the exit code and the restart counter.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap26/solution ./run.sh
Every voyage leaves residue. Containers stopped and never removed, old images no one uses any more, volumes orphaned when their container vanished: over time the hold fills up and the disk runs out. Day-2 — the life after the first deploy — is made of this too: knowing what takes up space and reclaiming it, but with judgement. Because on a shared machine a docker system prune given lightly deletes other people's work as well. In this lab you clean up safely — only the resources that carry your own label — and then you look up: where Docker on a single host ends, and where the horizon of orchestration begins.
Fill in the TODOs in the starter files, then run the solution's test:
cd docker/ed1/cap27/solution ./run.sh