Home/Docker
Lab · Manual of Docker

Docker

The engineer of universal packaging.

27 exercises7 Parts5 appendices
The colour code shows the level:FoundationalIntermediateAdvancedCloud Architect
get the exercises
git clone --filter=blob:none --sparse https://github.com/calmict/book_labs.git
cd book_labs
git sparse-checkout set docker/ed1
calm@calmict:~$ cat docker/topics
Part 1

The illusion of isolation

  • 01The bare-hands containerFoundational
  • 02The six roomsFoundational
  • 03The ceiling and the OOMIntermediate
  • 04The overlay by handIntermediate
Part 2

The engine's architecture

  • 05The chain and the custodianIntermediate
  • 06The OCI recipeIntermediate
  • 07Dying gracefullyIntermediate
Part 3

The art of creation

  • 08The manifestIntermediate
  • 09The loading planFoundational
  • 10The captain and the ordersIntermediate
  • 11The warehouse and the light shipIntermediate
  • 12The ship in productionAdvanced
Part 4

Persistence and state

  • 13What stays ashoreFoundational
  • 14Three ways to stowIntermediate
  • 15The number on the badgeAdvanced
Part 5

The labyrinths of the network

  • 16The cable and the switchboardAdvanced
  • 17The private switchboardAdvanced
  • 18Plugged in or unpluggedAdvanced
  • 19On the quay, and beyond the horizonCloud Architect
Part 6

Local orchestration

  • 20The fleet in one fileIntermediate
  • 21The all-clear signalAdvanced
  • 22The safe, not the sticky noteAdvanced
Part 7

Day-2, security and hardening

  • 23King only in his own roomCloud Architect
  • 24The right keys, not all of themCloud Architect
  • 25The logbook and the gaugesAdvanced
  • 26The black box of the mute containerCloud Architect
  • 27Clean the hold, watch the seaCloud Architect
calm@calmict:~$ ls docker/exercises/
01The bare-hands containerFoundational

What you build

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.

Objectives

  • Build a container by hand, with no Docker, using unshare (1.4).
  • Prove from the inside that you are process number 1 of a new world (1.4).
  • Isolate the hostname with a UTS namespace, without touching the host's (1.2, 1.4).
  • Unmask the illusion from the outside: same kernel, different namespace (1.5).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap01/solution
./run.sh

Expected result

  • manibnude.sh builds the container with the correct flags (TODO 1).
  • From the inside the shell is PID 1 and the hostname is nave-cargo (TODO 2).
  • From the host, the hostname is untouched and the PID-namespace inode differs from the inside one (TODO 3).
  • run.sh prints OK 1..4 and ALL CHECKS PASSED, including the contrast check: without --pid the inside PID is no longer 1.
02The six roomsFoundational

What you build

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.

Objectives

  • Build a process isolated in several namespaces at once, with no Docker and no sudo (2.1).
  • Prove each room with its own evidence: PID (PID 1), UTS (hostname), MNT (a private mount), NET (a near-mute stack), USER (the fake root) (2.2-2.7).
  • Use the inode in /proc/self/ns as the metric: a different inode means a separate world (2.1).
  • See that dropping a flag removes a single room, not all of them (2.5).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap02/solution
./run.sh

Expected result

  • lestanze.sh opens the correct namespaces (TODO 1) and creates the private mount (TODO 2).
  • host.txt records the host's inodes (TODO 3).
  • From the inside: PID 1, hostname sei-stanze, marker present, network with only loopback, uid 0.
  • run.sh prints OK 1..6 and ALL CHECKS PASSED, including the contrast: without --net the process shares the host's network again.
03The ceiling and the OOMIntermediate

What you build

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.

Objectives

  • Impose a memory ceiling on a cgroup with systemd-run --user, without sudo (3.4, 3.7).
  • Trigger the OOM killer and recognise its signature: exit code 137 (3.5).
  • Prove the ceiling isolates the damage: without it the same allocation is harmless (3.4).
  • Connect 137 to its cause (128 + 9 = SIGKILL), the diagnosis of chapter 26 (3.5).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap03/solution
./run.sh

Expected result

  • The CAP array imposes MemoryMax=40M and MemorySwapMax=0 (TODO 1).
  • greedy_capped_rc records the greedy exit code under the cap (TODO 2).
  • greedy_uncapped_rc records the greedy exit code without a cap (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED: greedy under the cap is killed (137), the frugal one survives (0), greedy without a cap survives (0).
04The overlay by handIntermediate

What you build

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.

Objectives

  • Mount an OverlayFS by hand: two read-only lowerdirs plus a writable upperdir (4.2, 4.3).
  • Prove Copy-on-Write: writing a "read-only" file leaves the lower intact and copies into the upper (4.3).
  • See that shared lowers make two containers almost free, with private uppers (4.4).
  • Close the circle of chapter 2: the overlay as a root inside a MNT namespace (4.3).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap04/solution
./run.sh

Expected result

  • overlay.sh mounts container A and records the fused view (TODO 1).
  • Writing to a.txt leaves the lower intact and the change in the upper (TODO 2).
  • Container B, with a different upper, sees the original (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
05The chain and the custodianIntermediate

What you build

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.

Objectives

  • Talk to the daemon directly over the socket with curl: the CLI is just an API client (5.2).
  • Verify the same API lists the containers, as docker ps would (5.1, 5.2).
  • Map the chain and prove the container's parent is a containerd-shim, not dockerd (5.3, 5.5).
  • Understand why systemd/containerd, not the daemon, sits above the shim: the basis of live-restore (5.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap05/solution
./run.sh

Expected result

  • lacatena.sh records the version obtained from the socket (TODO 1).
  • It records the container's parent (a shim) (TODO 2).
  • It records the grandparent (systemd/containerd, not dockerd) (TODO 3).
  • run.sh prints OK 1..4 and ALL CHECKS PASSED.
06The OCI recipeIntermediate

What you build

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.

Objectives

  • Build an OCI bundle by hand and run it with runc, with no Docker in the loop (6.3).
  • Read in config.json the Part 1 mechanisms listed as data (namespaces) (6.3).
  • Prove runc is a faithful executor: changing the recipe changes the container (6.3).
  • Understand why the OCI standard makes the parts interchangeable (6.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap06/solution
./run.sh

Expected result

  • laricetta.sh generates the recipe and records the namespaces (TODO 1).
  • run_recipe edits the config.json (command + terminal) (TODO 2) and runs it with runc (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED: the recipe lists the Part 1 namespaces, runc executes it, and changing the recipe changes the output.
07Dying gracefullyIntermediate

What you build

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.

Objectives

  • Observe the docker stop sequence: SIGTERM, wait (the grace period), then SIGKILL (7.3).
  • Recognise the PID 1 trap: a process that ignores SIGTERM waits the whole grace (7.3).
  • Cure the trap with --init (tini) as PID 1, which forwards the signal (7.5).
  • Read exit codes as a diagnosis: 137 (SIGKILL) versus 143 (clean SIGTERM) (7.3).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap07/solution
./run.sh

Expected result

  • measure times the stop with the grace period (TODO 1) and records the exit code (TODO 3).
  • Container B uses --init (TODO 2).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED: A waits the grace and exits 137, B stops at once and exits 143, and A is clearly slower than B.
08The manifestIntermediate

What you build

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.

Objectives

  • See that an image is a config plus a stack of layers, and that every instruction touching the filesystem adds a layer (8.1, 8.2).
  • Read the image ID as the sha256 digest of the config: the image is identified by its content, hence immutable and verifiable (8.3).
  • Recognise layers as content-addressed diffs (each one a digest) (8.2).
  • Prove sharing: an image built on top of another reuses the same layers, without duplicating them — the basis of the cache and of pull by digest (8.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap08/solution
./run.sh

Expected result

  • lanatomia.sh records the number of layers of the image and of the base (TODO 1).
  • It records the image ID and the digest of the top layer (TODO 2).
  • It builds the child image and counts the shared layers (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
09The loading planFoundational

What you build

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.

Objectives

  • Start from a base image with FROM and fix the working directory with WORKDIR (9.1, 9.4).
  • Bring a file from the build context into the image with COPY (9.3).
  • Set an environment variable with ENV, which the application reads at runtime (9.4).
  • Declare the default command with CMD, and see it start when you run the container with no arguments (9.5).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap09/solution
./run.sh

Expected result

  • The Dockerfile copies greet.sh into the WORKDIR (TODO 1).
  • It sets the GREETING environment variable (TODO 2).
  • It declares the default command with CMD (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
10The captain and the ordersIntermediate

What you build

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.

Objectives

  • Tell ENTRYPOINT (the fixed executable) from CMD (the default arguments) and see them combined (10.2, 10.3, 10.5).
  • Observe that arguments passed to docker run override CMD but leave ENTRYPOINT untouched (10.5).
  • Understand exec form versus shell form: exec makes your process PID 1 (10.1, 10.4).
  • Reconnect PID 1 to the signals of chapter 7: whoever is PID 1 receives SIGTERM.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap10/solution
./run.sh

Expected result

  • The Dockerfile copies entry.sh into the image (TODO 1).
  • It declares ENTRYPOINT in exec form (TODO 2).
  • It gives default arguments with CMD (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
11The warehouse and the light shipIntermediate

What you build

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.

Objectives

  • Understand how the cache reuses layers and why instruction order matters (11.1, 11.2).
  • Use a Multi-Stage build: a named build stage and a final stage (11.3).
  • Copy from the build stage only the artifact, with COPY --from (11.4).
  • Get a light final image, free of the build tools (11.5).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap11/solution
./run.sh

Expected result

  • The build stage is named with AS (TODO 1).
  • The final stage copies only the artifact with COPY --from (TODO 2).
  • The dependencies are copied and "installed" before the source (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
12The ship in productionAdvanced

What you build

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.

Objectives

  • Create a dedicated non-root user and run the app as it (12.2).
  • Give the user ownership of only the app directory: least privilege (12.3).
  • Declare the user in the image with USER, so it applies to every container (12.2).
  • See the difference in risk between root and non-root inside the container (12.1).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap12/solution
./run.sh

Expected result

  • The Dockerfile creates a non-root user (TODO 1).
  • It assigns that user ownership of the app directory (TODO 2).
  • It declares USER to run non-root (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
13What stays ashoreFoundational

What you build

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.

Objectives

  • See that the container's writable layer is ephemeral: a new container does not see the files a previous one wrote (13.1, 13.2).
  • Create a named volume and write into it (13.3).
  • Verify the volume survives the removal of the container that wrote it (13.3).
  • Understand that a volume is a first-class object with its own lifecycle, independent of any container (13.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap13/solution
./run.sh

Expected result

  • aterra.sh creates the named volume (TODO 1).
  • It writes a file into the volume from a throwaway container (TODO 2).
  • It reads the file back from a new container mounting the same volume (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
14Three ways to stowIntermediate

What you build

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.

Objectives

  • Use a bind mount: a host folder mounted inside, with two-way writes (14.2).
  • Use a daemon-managed volume, persistent across containers (14.1).
  • Use a tmpfs: an in-memory mount, not persisted and never on disk (14.3).
  • Recognise which to choose and why (14.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap14/solution
./run.sh

Expected result

  • imontaggi.sh writes through a bind mount onto a host folder (TODO 1).
  • It reads back from a new container a file written to a volume (TODO 2).
  • It mounts a tmpfs and reports its type (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
15The number on the badgeAdvanced

What you build

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.

Objectives

  • See that on a shared mount permissions apply by numeric UID/GID, not by user name (15.1).
  • Reproduce the problem: a container with a UID that does not own the folder cannot write (15.2).
  • Fix it by running the container with the UID that owns the files (--user) (15.3).
  • Verify the UID is not translated: the file the container creates is owned by the same UID on the host (15.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap15/solution
./run.sh

Expected result

  • ipermessi.sh reproduces the mismatch: a UID that does not own the folder is refused (TODO 1).
  • It fixes it by running the container with the owning UID (TODO 2).
  • It checks from the host the ownership of the created file (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
16The cable and the switchboardAdvanced

What you build

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.

Objectives

  • See that a container has its own network namespace, different from the host's (16.1).
  • Recognise that each container has its own eth0 and its own address, distinct from the others (16.4).
  • Understand that eth0 is one end of a veth pair: its peer is on the other side, on the host (16.2).
  • Connect it all to the docker0 bridge as the shared switchboard (16.3).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap16/solution
./run.sh

Expected result

  • irete.sh reads the container's network namespace (TODO 1).
  • It reads the eth0 IP of both containers (TODO 2).
  • It reads the veth indices (ifindex and iflink) (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
17The private switchboardAdvanced

What you build

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.

Objectives

  • See that on a bridge network you define, containers resolve one another by name (embedded DNS) (17.2).
  • Verify that on the default bridge name resolution does not work (17.1).
  • Observe the isolation: whoever is not on the network cannot reach its containers, not even by IP (17.3).
  • Understand why a per-application network is the right choice (17.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap17/solution
./run.sh

Expected result

  • irete.sh checks name resolution on the custom network (TODO 1).
  • It checks that on the default bridge the name does not resolve (TODO 2).
  • It checks that a container off the network cannot reach B, not even by IP (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
18Plugged in or unpluggedAdvanced

What you build

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.

Objectives

  • See that the host driver makes the container share the host's network namespace — no isolation (18.1).
  • See that the none driver gives the container its own namespace but no eth0 — no connectivity (18.2).
  • Compare with the default bridge: its own namespace and an eth0 — isolated but connected (18.4).
  • Understand how to choose the driver and why host is powerful but delicate (18.3).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap18/solution
./run.sh

Expected result

  • idriver.sh reads the namespace of the host-driver container (TODO 1).
  • It reads namespace and eth0 of the none-driver container (TODO 2).
  • It reads namespace and eth0 of the default-bridge container (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
19On the quay, and beyond the horizonCloud Architect

What you build

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.

Objectives

  • Give a container a direct address on a parent's network with macvlan (19.1).
  • Verify that each container has its own MAC — an L2 identity of its own on the segment (19.1).
  • Verify that two macvlan containers on the same parent reach each other at layer 2 (19.1).
  • Frame ipvlan (19.2) and overlay (19.3) and understand when they are needed (19.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap19/solution
./run.sh

Expected result

  • imacvlan.sh creates the macvlan network and the two containers (TODO 1).
  • It reads each container's MAC (TODO 2).
  • It verifies L2 reachability between the two (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
20The fleet in one fileIntermediate

What you build

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.

Objectives

  • Describe a multi-service application in a single Compose file (20.1).
  • Define two services with an image and a command (20.2).
  • Declare a dependency between services with depends_on (20.3).
  • See that Compose gives the services an app network where they resolve by name (20.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap20/solution
./run.sh

Expected result

  • compose.yaml defines db and web with a command that keeps them alive (TODO 1, 2).
  • It declares that web depends on db (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
21The all-clear signalAdvanced

What you build

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.

Objectives

  • Tell "started" from "ready" (started vs healthy) (21.1).
  • Declare a healthcheck on a service: how Docker knows it is ready (21.2).
  • Make web depend on db with condition: service_healthy — wait for readiness (21.3).
  • See that the startup order follows readiness, not an arbitrary time (21.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap21/solution
./run.sh

Expected result

  • db has a healthcheck (TODO 1) and becomes ready only after a delay (TODO 2).
  • web waits for db to be healthy with condition: service_healthy (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
22The safe, not the sticky noteAdvanced

What you build

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.

Objectives

  • Pass an environment variable to a service (22.1).
  • Take its value from a .env file, kept out of the repository (22.2).
  • Give sensitive data as a secret, mounted as a file in /run/secrets (22.3).
  • See why a secret does not end up in the environment, unlike an env var (22.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap22/solution
./run.sh

Expected result

  • app receives APP_ENV with a value taken from .env (TODO 1).
  • The secret db_password is defined from a file (TODO 2) and assigned to app (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
23King only in his own roomCloud Architect

What you build

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.

Objectives

  • Enter a USER namespace that maps you to root and see that inside you are uid 0 (23.2).
  • Verify that that root is mapped to your real, unprivileged user on the host (23.3).
  • Observe that that "root" cannot touch the host's root-owned files — it is powerful only inside the namespace (23.3).
  • Understand why this model shrinks the blast radius of an escape (23.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap23/solution
./run.sh

Expected result

  • irootless.sh reads the uid inside the user namespace (TODO 1).
  • It reads the host owner of a file created "as root" inside (TODO 2).
  • It checks whether that "root" can write to the host's /etc (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
24The right keys, not all of themCloud Architect

What you build

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.

Objectives

  • See that root is not monolithic: its powers are separate capabilities (24.1).
  • Drop all capabilities with --cap-drop ALL and watch an operation fail (24.2).
  • Grant back only the needed capability with --cap-add: least privilege (24.2).
  • Frame seccomp (24.3) and AppArmor/SELinux (24.4) as additional layers.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap24/solution
./run.sh

Expected result

  • icapabilities.sh tries the ping with the default capabilities (TODO 1).
  • It retries it with --cap-drop ALL (TODO 2).
  • It retries it with --cap-drop ALL --cap-add NET_RAW (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
25The logbook and the gaugesAdvanced

What you build

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.

Objectives

  • Retrieve with docker logs what a container writes to stdout and stderr (25.1).
  • Recognise the logging driver that keeps those logs — the default json-file (25.2).
  • Read a container's live metrics with docker stats (25.3).
  • Frame observability in production (25.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap25/solution
./run.sh

Expected result

  • iobs.sh reads the container's logs (stdout and stderr) (TODO 1).
  • It reads the logging driver (TODO 2).
  • It reads the memory usage with docker stats (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
26The black box of the mute containerCloud Architect

What you build

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.

Objectives

  • Recognise a "mute" container: the logs are empty, there is nothing to read there (26.1).
  • Read the black box with docker inspect: the exit code, the real diagnosis (26.2, 26.4).
  • Recognise the crash loop from the restart counter and the final state (26.3).
  • Connect the exit code to its causes (chapter 7): 42, 137, 143, 127... (26.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap26/solution
./run.sh

Expected result

  • idiag.sh reads the container's logs (empty) (TODO 1).
  • It reads the exit code from docker inspect (TODO 2).
  • It reads the restart counter and the final state (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.
27Clean the hold, watch the seaCloud Architect

What you build

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.

Objectives

  • Recognise orphans: stopped containers, unused volumes taking up space (27.1).
  • Reclaim space safely, scoped (labels, names), never a global prune on a shared host (27.2).
  • Verify that only your resources were removed (27.2).
  • Frame the horizons: the limits of a single host and the bridge to orchestration (27.4).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd docker/ed1/cap27/solution
./run.sh

Expected result

  • imaint.sh reclaims its own stopped containers with a label-filtered prune (TODO 1).
  • It removes its own named volume (TODO 2).
  • It recounts and confirms nothing of its own remains (TODO 3).
  • run.sh prints OK 1..3 and ALL CHECKS PASSED.