A Dockerfile that builds small and fast
🎯You'll be able to
Write a Dockerfile that uses a pinned, slim base image, orders layers so rebuilds are quick, leaves junk out of the build and uses stages to keep build tools out of the final image.
🏠The everyday picture
When you pack for a trip, you put the things that never change (the toiletry bag) in first and the day's clothes on top, so repacking each morning only touches the top. And you leave the iron at home: you needed it before leaving, not on the trip.
📘What it really is
-
Base image:
FROM node:22follows whatever the latest Node.js 22 build is and includes a full Debian system with compilers.FROM node:22.23.3-alpinepins an exact version on Alpine Linux, which is much smaller. Pinning means the same Dockerfile builds the same thing next month. For the strongest pin, add the digest:node:22.23.3-alpine@sha256:.... -
Layer order for the cache: copy the dependency list first, install, then copy the code.
dockerfile COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY server.js ./Changingserver.jsnow reuses the cachednpm cilayer, so rebuilds take seconds instead of a minute. -
npm ciinstead ofnpm install: installs exactly whatpackage-lock.jsonlists and fails if the two disagree.--omit=devleaves out test tools. -
.dockerignore: lists what the build should never see, such asnode_modules,test,.envand*.log. Without it,COPY . .copies your localnode_modules(built for your computer, maybe a different system) and any secrets lying in the folder. -
Multi-stage builds: one stage installs or compiles, the final stage copies in only the results. ```dockerfile FROM node:22.23.3-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev
FROM node:22.23.3-alpine WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY package.json server.js ./ ``` For apps that compile (TypeScript, Go, Java), the first stage holds the compiler and the final image holds only the output.
- Smaller is better for three reasons: faster to pull when a new copy starts, less to scan, and fewer programs an attacker could use.
📝A worked example
Input: replace the starter's Dockerfile with the two-stage version above (plus the lines from Lessons 3 and 4), add app/.dockerignore,
and run docker build -t routerani:1.0.0 app twice, changing one line of server.js between builds.
Expected output: docker images routerani shows about 240 MB instead of about 1.6 GB (measured on 1 November 2026). The second build shows
CACHED for the npm ci step and finishes in a few seconds. docker run --rm --entrypoint ls routerani:1.0.0 /app lists
node_modules, package.json and server.js, and no test folder.
🌍Where it's used
- Busy CI systems: a team building 200 images a day saves hours when dependency layers come from the cache.
- Scaling events: during a sale, new copies start on fresh servers, which must pull the image first; small images start sooner.
- Security reviews: scanners report fewer issues on slim images because there are fewer packages.
📝Case scenario
An exam-registration site's image was 2.1 GB because it copied the whole project folder, including test videos. On the last day for
registration, traffic doubled and new copies took four minutes each to pull the image, so users saw errors while capacity caught up. After
a .dockerignore, a slim base and two stages, the image was 180 MB (illustrative figures) and new copies served traffic in under 30 seconds.
🖱️Try it
Build the starter image, then your improved one, and compare docker images sizes and docker history for each. Change one line of
server.js and rebuild both. Success: you can point to the line in each Dockerfile that decides whether npm runs again.
⚠️Common mistakes
COPY . .before installing. Sign: every code change reinstalls all packages. Fix: copy the lock file and install first, then copy code.- Floating tags like
node:latestornode:22. Sign: a build that worked yesterday breaks today. Fix: pin the exact version, and update it on purpose. - No
.dockerignore. Sign: the image contains.env, tests or a localnode_modules. Fix: add one that lists them.
🛠️For your project
Project 1 test 3 checks that app/.dockerignore leaves out node_modules and that there's no test folder in the image.
✅Quick check
Why copy package-lock.json and run npm ci before copying the code?
Show the answer
So code changes reuse the cached dependency layer and rebuilds are fast.
What does a multi-stage build leave out of the final image?
Show the answer
Whatever only the build needed: compilers, build tools, development packages.
