Home/handbooks/Docker
Lab · Manual of Docker

Docker

The engineer of universal packaging.

27 exercises7 Parts5 appendices
The colour code shows the level:FoundationalIntermediateAdvancedCloud Architect
● On sale on AmazonPaperback →246 pages · ISBN 9798172001000
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

  • barehands.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

  • namespaces.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 read the two pieces of evidence that identify it: exit code 137 and the oom_kill counter in memory.events (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).
  • oom_kill_count records the oom_kill value read from memory.events after the kill (TODO 4).
  • run.sh prints OK 1..4 and ALL CHECKS PASSED: greedy under the cap is killed (137), the frugal one survives (0), greedy without a cap survives (0), and memory.events records at least one oom_kill.
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

  • chain.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).
  • Change the UTS namespace in the recipe and observe its effect on the hostname (6.3).
  • Run the same bundle with two different runtimes and prove their interchangeability (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

  • recipe.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).
  • recipe.sh changes the UTS namespace and hostname field as parts of the same contract (TODO 4).
  • recipe.sh runs the same bundle with runc and a second OCI runtime (TODO 5).
  • run.sh prints OK 1..5 and ALL CHECKS PASSED: the recipe lists the Part 1 namespaces, its changes alter the container, and two runtimes execute the same bundle with identical 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).
  • The script counts zombies without init and with --init (TODO 4).
  • run.sh prints OK 1..4 and ALL CHECKS PASSED: A waits the grace and exits 137, B stops at once and exits 143, A is clearly slower than B, and tini leaves no zombies behind.
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

  • anatomy.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).
  • It proves that one tag can point to two different digests and that the first digest still resolves the same content (TODO 4).
  • run.sh prints OK 1..4 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).
  • Compare the weight produced by separate RUN instructions with a concatenated RUN that removes the data in the same layer (9.2).
  • Verify that ARG is build-time data, does not reach the runtime environment, but its build value remains readable in history (9.4).

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).
  • The two RUN variants produce images with a measurable size difference (TODO 4).
  • The ARG value is readable in history but not in the runtime environment, while GREETING set with ENV is present (TODO 5).
  • run.sh prints OK 1..5 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. Finally, you build a real entrypoint: it waits for a local dependency and hands control to the application with exec.

Objectives

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

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).
  • It compares shell-form and exec-form CMD stop times (TODO 4).
  • The entrypoint waits for the dependency with a timeout and hands over with exec (TODO 5 and 6).
  • The shell and exec variants expose the application's actual PID (TODO 7).
  • run.sh prints OK 1..5 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).
  • The final image remains below the calibrated 3 MiB threshold.
  • run.sh prints OK 1..4 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 declares USER to run non-root (TODO 1).
  • It declares a real HEALTHCHECK (TODO 2).
  • It declares the three OCI labels (TODO 3).
  • The final stage contains neither a shell nor a package manager.
  • run.sh prints OK 1..6 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.3).
  • Distinguish stopping, which preserves the writable layer, from removing the container, which deletes it (13.2).
  • Observe the volume count reported by docker system df in structured form, without depending on machine-specific disk usage (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

  • persistence.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).
  • It proves stop and start preserve data written once to the layer, while removing and recreating the container deletes it (TODO 4).
  • It reads the volume count from structured docker system df output (TODO 5).
  • run.sh prints OK 1..5 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, keeps nothing after the container, 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, which can however end up in the host's swap (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

  • mounts.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 that, without user namespace remapping, the UID is not translated: the file the container creates is owned by the same UID on the host (15.1).

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

  • permissions.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).
  • It checks the third remedy: entrypoint chown followed by su-exec (TODO 4).
  • run.sh prints OK 1..4 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 first inspect it on two real containers, then rebuild it by hand in an isolated environment, add NAT towards a simulated external network, and finally compare your own rule with the one Docker really wrote on the host.

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.1-16.2).
  • Understand that eth0 is one end of a veth pair: its peer is on the other side, on the host (16.1).
  • Connect it all to the docker0 bridge as the shared switchboard (16.2).
  • Build namespaces, veth and a bridge by hand without real privileges.
  • Apply a MASQUERADE rule and prove it with end-to-end traffic to a simulated external network.
  • Compare the hand-built topology with Docker's real bridge, read-only.
  • Read the NAT rules Docker writes on the host, from a throwaway container holding NET_ADMIN alone, and recognise in them the rule you wrote by hand.

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

  • network.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).
  • wiring.sh builds namespaces, veth and a bridge inside unshare -Urnm (TODO 4).
  • Traffic reaches the simulated external network through MASQUERADE (TODO 5).
  • The structure is compared with docker0 without changing the real network (TODO 6).
  • network.sh reads the MASQUERADE rule Docker wrote for its own bridge (TODO 7).
  • run.sh prints OK 1..7 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

  • bridges.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).
  • It compares 127.0.0.1 and 0.0.0.0 exposure (TODO 4).
  • run.sh prints OK 1..4 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).
  • Demonstrate that host directly occupies host ports and ignores -p (18.1).
  • Qualitatively compare host and bridge paths to the same service, measuring latency without using it as an assertion (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

  • drivers.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).
  • It demonstrates the host-port conflict and ignored -p, with their positive controls (TODO 4).
  • It compares host-loopback, bridge-loopback, and bridge-gateway paths to the same service and prints both latency measurements (TODO 5).
  • run.sh prints OK 1..5 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 feel the price of that choice — the container does not talk to its own host, not even when the host sits on the same subnet — and you swap the driver to see where ipvlan differs. Finally you look beyond the single host's horizon: 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).
  • Measure the hairpin limit: a macvlan child and its parent do not reach each other, in either direction (19.2).
  • Tell ipvlan from macvlan on what actually matters: ipvlan children share the parent's MAC (19.2).
  • Frame overlay (19.3) and understand when it is 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

  • macvlan.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).
  • It measures the two blocked directions between child and parent (TODO 4).
  • It creates the ipvlan network and reads the parent's and children's MACs (TODO 5).
  • run.sh prints OK 1..5 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).
  • The debug service is profile-controlled and the override changes LAB_MODE.
  • run.sh prints OK 1..5 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. Finally, you distinguish what .env interpolates from what env_file injects and verify two real precedence relationships among configuration sources.

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).
  • Distinguish .env interpolation from env_file injection (22.2).
  • Verify that environment overrides env_file and that the process shell overrides .env during interpolation (22.2).

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).
  • The key present only in .env does not enter the container, while the key in env_file does (TODO 4).
  • environment overrides env_file, and the process shell overrides .env during interpolation (TODO 5).
  • run.sh prints OK 1..5 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).
  • Prove that whoever controls the rootful Docker daemon can read the host filesystem as uid 0 while remaining an unprivileged host user (23.1).
  • Isolate the cause by running the same mount as an unprivileged UID (23.4).
  • 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

  • rootless.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).
  • It contrasts direct denial with daemon-mediated uid 0 access through a read-only host filesystem mount (TODO 4).
  • It repeats the same mount as the unprivileged UID and gets denial again (TODO 5).
  • run.sh prints OK 1..5 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

  • capabilities.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).
  • It compares the normal internal mount and --privileged (TODO 4).
  • run.sh prints OK 1..4 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

  • observability.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).
  • It directly compares files without/with rotation and links LIMIT to --memory.
  • run.sh prints OK 1..5 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 reconstruct the story of containers with different symptoms: a silent crash, an OOM kill, a missing command and an image without a shell. The diagnosis starts from observable facts and leads to a targeted remedy each time.

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).
  • Confirm an OOM kill with exit 137 and State.OOMKilled, not with the number alone (26.4).
  • Distinguish the 127 returned by a shell from rejection of a missing direct executable (26.4).
  • Observe a shellless container's processes by sharing its PID namespace (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

  • troubleshoot.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).
  • It verifies exit 137 together with State.OOMKilled (TODO 4).
  • It distinguishes the shell's 127 from direct-exec rejection (TODO 5).
  • It inspects the shellless container's processes through a shared PID namespace (TODO 6).
  • run.sh prints OK 1..6 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).
  • Back up a volume and restore it into a new volume with a utility container (27.3).
  • Exercise the tag, push, removal and pull cycle with a local private registry (27.3).
  • 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

  • maintenance.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).
  • It restores the known content from the archive into a new volume (TODO 4).
  • It completes a tag round trip through the local registry (TODO 5).
  • run.sh prints OK 1..5 and ALL CHECKS PASSED.