An application is more than its code. It runs on top of a stack of things the code does not control:
Change any layer and the same code can behave differently.
Drift between machines
Machines that were set up by hand, or set up at different times, drift apart:
| Layer | Developer laptop | Production server |
|---|---|---|
| OS | macOS 14.6 | Ubuntu 22.04 |
| Node.js | v20.17.0 | v18.19.1 |
| Python | 3.12.7 | 3.10.12 |
| OpenSSL | 3.3 | 3.0 |
A release that calls Array.prototype.toSorted() (added in Node.js 20) passes every test on the laptop and crashes on the server:
The code is identical. The environment is not.
What teams tried before containers
| Approach | Problem |
|---|---|
| Setup wiki pages | Steps get skipped and go out of date |
| Config management (Ansible, Puppet) | Converges hosts, but every host still holds its own packages, and two apps on one host can need different versions |
| One VM per app | Consistent, but each VM carries a full OS: slow to start, gigabytes per copy |
What a container image changes
A packages the application with its runtime, libraries and OS files. A is a process started from that image.
The host only provides the kernel and Docker. Everything else comes from the image, so every machine that runs the same image gets the same runtime.
The image name decides the runtime, not the host:
The part after the colon is the tag. It selects a version of the image: node:20-alpine is Node.js 20 on Alpine Linux, node:18-alpine is Node.js 18. --rm deletes the container when it exits; the image stays.
What an image does not fix
- The kernel. Containers share the host kernel. A Linux image needs a Linux kernel.
- Configuration you pass in. Environment variables and mounted files still differ per environment (Chapter 3).
- External services. The database and the network are outside the image.