Docker explained
Tuesday, 10:40: release 2.14 passed every test on your laptop, yet one of StayHub's three servers crashes on start and 1 in 3 bookings fail. Same code, three machines, three different setups.
Why software that works on your laptop breaks on a server, and how Docker packs everything into one box that runs the same everywhere.
- Problem
The booking app works on your laptop and on two servers, and crashes in production with a different Node version and a missing system library.
- Why the simple system fails
Installing dependencies by hand on each machine drifts: different OS packages, runtime versions and config. Every environment is a unique snowflake.
- Engineering concept
Package the app with its runtime and dependencies into an immutable image; run identical, isolated copies (containers) anywhere.
- Technology
Dockerfile → image (layers) → registry → container, plus volumes, networks and Docker Compose.
- Trade-offs
Consistent, fast deploys in exchange for image hygiene, a build pipeline, and the reminder that containers share the host kernel and are not full VMs.
The problem
2 min- Omar · On-call SREapp-3 is crash-looping since 2.14. 1 in 3 booking requests fail. app-1 and app-2 are fine.
- You · Engineer at StayHubIt passed every test on my laptop. Same commit on all three servers.
- Lina · Senior engineer, your mentorSame code, yes. Guess what's different.
The big idea
4 minDon't install on each machine. Build one image and run it everywhere.
Installing Node 22 on app-3 fixes today's difference; the next server drifts some other way. With images, the question "what's installed here?" becomes "which image is running?", which has one exact answer.
How it works
5 min- 1Write the recipe
A says: start from Node 22, install libvips, install dependencies from the lockfile, copy our code, start with
node server.js. - 2Build the image once
A is a frozen, versioned package: code plus runtime and libraries.
booking-api:2.14.0, about 380 MB, never changes after it's built. - 3Layers
Each recipe step makes an : base 50 MB, Node 100, dependencies 220, our code 10. A release that changes only code ships about 10 MB.
- 4Push it to a registry
A is a shelf for images; every server pulls the exact same version.
- 5Run containers
A is the image running. It sees only the image's files (Node 22 everywhere) but shares the host's Linux kernel, so it starts in under a second, unlike a VM.
- 6Roll back by tag
Bad release? Run
2.13.2again: it's still on disk, about 20 s per server.
Watch a decision being made
4 minLina moves the booking API to images
Make all three servers run exactly the same thing, and make rollback fast.
- 1List what the app needs
Node 22, libvips for
sharp, the npm dependencies, the start command, port 3000.Why: Whatever isn't on this list is what drifts between servers.
- 2Write the recipe
Turn the list into a Dockerfile, reviewed in git next to the code.
dockerfileFROM node:22-slim WORKDIR /app RUN apt-get update && apt-get install -y libvips \ && rm -rf /var/lib/apt/lists/* COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY . . USER node CMD ["node", "server.js"]Why: The environment becomes versioned like code.
- 3Build once, tag with a version
CI builds
registry.stayhub.dev/booking-api:2.14.0on merge and pushes it.Why: The exact bytes that passed the tests ship; the tag says what's running.
- 4Run the same tag everywhere, prove the rollback
Each server runs the image with secrets passed at start (
--env-file). Then run2.13.2once: about 20 s.Why: Servers only need Docker now. A rollback you haven't tried is a hope.
In real life
3 minGoogle: containers for almost everything
- Has publicly said for years that nearly everything it runs is in containers
- Its cluster system (Borg) inspired Kubernetes
- Thousands of services deployed constantly
Worth it when many services share many machines.
Stack Overflow: a few hand-tuned servers
- Has publicly described running its main site on a small number of big servers for years
- A small team knew every machine well
- Fewer moving parts, careful setup
Worth it with a tiny, stable, well-known fleet.
What goes wrong
3 minAny layer containing a secret is readable by anyone who can pull.
.env* in .dockerignore; inject secrets at runtime; --mount=type=secret for build steps.
A moving label: servers can run different code, and you can't tell what to roll back to.
Immutable version or commit tags.
The writable layer dies with the container.
Volumes, or managed services and object storage.
Trade-offs
3 min| Container | Virtual machine | |
|---|---|---|
| Isolation | Process-level, shared kernel | Own kernel, stronger boundary |
| Start time | Milliseconds to seconds | Tens of seconds to minutes |
| Size | MBs to hundreds of MBs | GBs (full OS) |
| Typical use | Packaging and running apps | Hosting containers; isolating tenants |
- Karim · CTOContainers mean a pipeline, a registry and a new tool for 22 people. Why not a script that installs Node 22 and libvips everywhere?
- Lina · Senior engineer, your mentorA script runs once. Servers keep changing after it.
- Karim · CTOWe have three servers. I can check three servers.
Your job as tech lead
4 minClaude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.
- The exact base image (Node 22, pinned). An agent will happily pick
node:latest. - How images are named:
booking-api:2.14.0or the commit SHA, never onlylatest. - What never goes in an image:
.envfiles, keys, passwords. Secrets arrive at start.
Containerise the booking API so release 2.14 runs identically on app-1, app-2 and app-3. Write a Dockerfile from an official Node 22 image pinned to an exact version, with libvips for sharp. Install from the lockfile only. Add a .dockerignore excluding .env files, node_modules and test data. Config and secrets come from environment variables at runtime. Tag booking-api:2.14.0. Don't change app code or the email worker. Build, run it, call the health endpoint and a photo resize, then show the image size and layers.
- The base names an exact Node 22 version.
docker history: about 380 MB, no.envor secrets in any layer.- You ran the container and resized a photo: the exact thing that crashed app-3.
- It copies
.envinto the image, even with a laterrm. - It fixes one server by installing libraries by hand instead of in the image.
- It says the image works without running a container from it.
Check yourself
2 minNext episode: the search team rushed their own image out. It weighs 1.4 GB, and every one-line fix takes six minutes to build.
Mark it complete to update your skill map.
Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Interactive simulation, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
Technical deep dive
Here's the Lina writes for the booking API. Each instruction becomes an ; the order matters, and the next episode is all about why.
FROM node:22-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends libvips \
&& rm -rf /var/lib/apt/lists/*
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]Lina moves the booking API to images
Release 2.14 crashes on app-3 because it has Node 20 and no libvips. Make all three servers run exactly the same thing, and make rollback fast.
- 1List what the app needs
Write down everything outside our code: Node 22, libvips for
sharp, the npm dependencies, the start command, the port (3000).Why: Whatever isn't in this list is what drifts between servers. It all has to go into the image.
- 2Write the recipe
Turn the list into a Dockerfile:
FROM node:22-slim, install libvips,npm cifrom the lockfile,COPYthe code,USER node,CMD.Why: The Dockerfile lives in git next to the code, so the environment is reviewed and versioned like code.
- 3Build once, tag with a version
CI runs the build on every merge and tags the result.
bashdocker build -t registry.stayhub.dev/booking-api:2.14.0 . docker push registry.stayhub.dev/booking-api:2.14.0Why: Building once means the exact bytes that passed the tests are the ones that ship. A version tag tells you what's running.
- 4Run the same tag everywhere
On app-1, app-2 and app-3: pull
booking-api:2.14.0and start it withdocker run, passing secrets at start time.bashdocker run -d --name api -p 3000:3000 \ --env-file /etc/stayhub/prod.env \ registry.stayhub.dev/booking-api:2.14.0Why: Each server now only needs Docker. Its own Node version no longer matters.
- 5Prove the rollback
Stop the container and run
booking-api:2.13.2instead. Time it: about 20 s per server, because the old image is still on disk.Why: A rollback you haven't tried is a hope. Rehearsing it once makes the next bad release a non-event.
| Container | Virtual machine | |
|---|---|---|
| Isolation | Process-level (namespaces, cgroups), shared kernel | Hardware-level, own kernel via hypervisor |
| Start time | Milliseconds to seconds | Tens of seconds to minutes |
| Size | MBs to hundreds of MBs | GBs (full OS) |
| Density | Dozens to hundreds per host | A handful per host |
| Security boundary | Weaker; kernel exploits affect all containers | Stronger |
| Typical use | Packaging and running apps | Running containers on, isolating tenants, other OSes |
Interactive simulation
Visual diagram
From source code to running containers. Follow one release of the booking app.
Analogy
- Shipping containersThe packing listDockerThe Dockerfile
- Shipping containersA sealed, labelled steel boxDockerAn image
- Shipping containersThe box opened and in useDockerA running container
- Shipping containersThe port warehouseDockerA registry
- Shipping containersCranes don't care what's insideDockerAny server can run any image
Where the analogy breaks: a real container is opened once. One image can be "opened" as many running containers as you like, at the same time, and the image itself never changes.
Check your understanding
Practice ladder
Trace a release to app-3
Scenario Warm-upStayHub now ships images. The facts:
- app-3 has Node 20 installed on its disk and Docker installed.
booking-api:2.15.0was built fromFROM node:22-slimand includes libvips.- app-3 already has
booking-api:2.14.0(380 MB) on disk. 2.15.0 changes only our code (the 10 MB layer). - Omar runs:
docker pull registry.stayhub.dev/booking-api:2.15.0thendocker run … booking-api:2.15.0.
Trace what happens: how much is downloaded, which Node version the app runs with, and what Omar types if 2.15.0 turns out to be broken.
Finish Lina's Dockerfile for the email worker
Code PracticeThe email worker is a Node 22 app started with node worker.js. It renders PDF invoices, which needs the system package fonts-dejavu. Lina's Dockerfile has three gaps:
FROM ___1___ WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends fonts-dejavu \ && rm -rf /var/lib/apt/lists/* COPY package.json package-lock.json ./ RUN npm ci --omit=dev ___2___ USER node ___3___
Fill in the three gaps and say in one line why each matters.
"Still works on my machine"
Debugging PracticeA week after the move to images, three new problems appear. For each, give the cause and the fix.
- You build
booking-api:2.16.0on your Apple Silicon laptop and push it. On app-1 (x86-64) the container exits instantly:exec /usr/local/bin/node: exec format error. - Karim notices app-2 runs
booking-api:latestand app-3 runsbooking-api:latest, but they behave differently. - A teammate fixed a typo by running
docker execinto the container on app-1 and editing a file. After the next deploy, the typo is back.
Containers for the reporting server?
Decision StretchStayHub has one more server: reports-1, a VM that runs a nightly Python script building revenue reports for hotel owners. The facts:
- It changes about twice a year.
- It needs Python 3.12 and a PDF library with system fonts.
- It was set up by an engineer who left; nobody has written down how.
- The VM must be rebuilt next month because its OS reaches end of life.
Karim: "It's one boring server. Do we bother putting it in a container?"
Write a short decision in this form: "We chose X over Y because…, accepting the risk of…".
Explain it yourself
Explain to a teammate who has never used Docker what problem it solves and how an image becomes running containers in production.