Docker, from first container to production
Twelve chapters that explain how Docker really works, each with a broken Dockerfile, Compose file or release script to fix. Real builds, real containers, a real Postgres and a real registry. Every number on this site was measured.
# chapter 09: stopping the API
$ docker stop api
took 10.6 s, exit code 137
took 1.9 s, exit code 0
# chapter 08: the image you ship
$ docker image ls api
1,138.5 MB, gcc inside, runs as root
149.8 MB, no compiler, uid 10001
# chapter 03: rebuild after a one-line change
pip install ran again: 6.2 s
every layer cached: 0.4 s
From one container to a production release
Each chapter explains one idea in depth, then hands you an exercise that gets it wrong in a way real teams do. You fix it, and a checker proves the fix with real containers. The red line on each card is what the exercise prints before you start.
Level 1Foundations
4 chapters, 85 minutes
What a container is, what goes into an image, the build cache and networking.
- 0120 min
Containers are processes
A container is not a small virtual machine. It is one process, with walls around it, and it lives exactly as long as that process does.
dockerlab-01 Exited (0) 3 seconds ago - 0220 min
The build context and .dockerignore
COPY . .copies whatever the build context holds, and the build context is the whole folder unless you say otherwise. That is how secrets and gigabytes end up in images.API_KEY=sk_dockerlab_6e50aac08080d1c62dc6a9b9 - 0325 min
Layers and the build cache
The build cache reuses a layer only if everything before it is unchanged. Put what changes rarely first and what changes on every commit last.
ran 6.2 s RUN pip install --no-cache-dir -r requirements.txt - 0420 min
Ports and networks
Inside a container, 127.0.0.1 is the container's own loopback. A server that listens there is working perfectly and can never be reached from outside.
RemoteDisconnected: Remote end closed connection without response
Level 2Services and data
3 chapters, 65 minutes
Volumes and ownership, Compose with a real Postgres, and secrets at build time.
- 0520 min
Volumes and file ownership
A container's own files disappear with it. Data belongs in a volume, and the user your app runs as must be allowed to write there.
sqlite3.OperationalError: unable to open database file - 0625 min
Compose and start-up order
depends_onwaits for a container to start, not for the service in it to be ready. Readiness has to be checked, and checked the same way your app connects.api Exited (1) 7 seconds ago - 0720 min
Secrets at build time
Build arguments are written into the image's history. A secret the build needs must be mounted for one step and never become part of the image.
ARG PACKAGE_TOKEN=pkg_dockerlab_99b7b8ab9421e672b516dab8
Level 3Production
5 chapters, 115 minutes
Small images, graceful shutdown, health checks, cgroup limits and digests.
- 0825 min
Multi-stage builds
The tools you need to build an app are not the tools you need to run it. Build in one stage, run in another, and copy across only the result.
image size: 1,138.5 MB - 0920 min
PID 1 and graceful shutdown
docker stopsends SIGTERM to PID 1 and nothing else. If PID 1 is a shell that does not pass it on, your server is killed ten seconds later, mid-request.took 10.6 s, exit code 137 - 1020 min
Health checks
A health check is a program Docker runs inside your container, with only the tools your image has. It must pass when the service works and fail when it does not.
exit code 1: /bin/sh: 1: curl: not found - 1125 min
CPU and memory limits
--cpus 1does not hide the host's other CPUs from your program. It sets a quota, and a program that sizes itself by counting CPUs will start too many workers and run out of memory.container: exited, exit code 1, OOMKilled=true - 1225 min
Tags, digests and registries
A tag is a name that the next push can move. A digest is the hash of one exact image. Deploy and roll back by digest.
the running version is 1.5.0
The build cache, one step at a time
BuildKit reuses a layer only when the step and everything before it are unchanged. Build once, then change a line of code and build again. In the Dockerfile as shipped,COPY . .comes before the install, so every commit reinstalls every dependency.
Switch to the fixed Dockerfile and do the same. Then change requirements.txt and watch the install run again, which is exactly when it should.
pip install measured at 6.2 s in chapter 03
- ...
FROM python:3.13.16-slim-trixie - ...
WORKDIR /app - ...
COPY . . - ...
RUN pip install --no-cache-dir -r requirements.txt - ...
CMD ["uvicorn", "app.main:app", ...]
Nothing built yet. Build once to fill the cache.
- Builds
- 0
- pip install runs
- 0
- Seconds in pip install
- 0.0
Run the experiments on your own machine
uv run python -m dock try measures real Docker behaviour where you run it. These are the numbers from the machine the workshop was written on: Docker Desktop with 4 CPUs.
| Base image, same server | Image size |
|---|---|
| python:3.13.16-trixie | 1,112.0 MB |
| python:3.13.16-slim-trixie | 118.3 MB |
| python:3.13.16-alpine | 51.9 MB |
You need Docker, uv and git
Docker Desktop on macOS or Windows, or Docker Engine on Linux. uv installs Python for you, and the lab itself uses only Python's standard library. Everything the lab creates is named dockerlab, and dock clean removes it all.
git clone https://github.com/jars-demo/docker-demo.git cd docker-demo/lab uv sync uv run python -m dock run 1