Lessons 1/4 · 15 min
A reproducible, versioned image
Why "works on my machine" is not enough
Amina has a test node running on her laptop. Her colleague Brice launches it too: it crashes, because his machine has a different version of a library. Nobody worked badly: the program depended on things nobody had written down.
Three words first.
- A container is a program started as an isolated process: it sees its own files, its own libraries and its own view of the network. Unlike a virtual machine, it shares the Linux kernel of the host (on Windows or macOS, Docker Desktop runs a small Linux virtual machine in between, but the logic is the same).
- An image is the read-only package from which containers are started. It relates to a container like a program file relates to a running program.
- A Dockerfile is the written recipe that builds an image.
Reading a Dockerfile
Example for a small Node.js service (the workshop indexer). Values in angle brackets are for you to fill in.
FROM node:<tag>@sha256:<digest-you-looked-up>
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
USER node
CMD ["node", "index.js"]Each instruction is a step. Those that change files (COPY, RUN) produce a cached layer: if nothing changed since the last build, Docker reuses it ("CACHED" in the output). As soon as one step changes, every step after it is redone. Note: here COPY . . runs before USER node, so the files in /app belong to root and are only readable by node; if the application must write there, use COPY --chown=node:node . ..
Reproducible means pinned
Three levers, from the most important to the least visible.
- The base image.
FROM ubuntu:latestpromises nothing:latestis just the default tag name, the one the publisher moved last. A tag can be moved; a digest (@sha256:…) cannot, because it is the SHA-256 hash of the image manifest: different content gives a different digest, as in the hash-functions workshop. You read it withdocker images --digestsfor an image pulled from a registry (a locally built image has none until it is pushed); otherwise on the registry's page. For a multi-architecture image, the digest you pin is usually that of the index, which points to the per-architecture variants. - The dependencies. A lock file (
package-lock.json) freezes exact versions;npm ciinstalls them as they are and fails if the file andpackage.jsondisagree.npm installmay resolve things differently. - Your own version. Tag what you build with an explicit number, for example
bac-testnode:lab-1, never onlylatest.
This course deliberately states no "current" version number: versions change. You learn to pin them and to check them (the project's release page, docker images --digests, npm ls), then to write down what you chose.
Instruction order matters
In the example we copy the two dependency files first, then run npm ci, then copy the rest of the code. If you edit index.js, only the last copy is redone: the slow installation stays cached. Had you written COPY . . before RUN npm ci, every one-line code change would rerun the full installation. Also add a .dockerignore file to keep .git, node_modules and above all .env out of the context sent to the build.
What this does NOT guarantee
- Not identical bytes: a build may embed dates, or an
apt-get updatethat fetches whatever the mirror holds today. "Reproducible" here means: same behaviour from the same pinned inputs. - Pinned does not mean safe. A digest also freezes a known flaw: you must update on purpose, by changing the digest and retesting.
- A tag that works today may point elsewhere tomorrow.
Recap: base image by digest, locked dependencies, explicit version, well-ordered cache.
