DevOps Foundations · Module 5: Container basics
Images, Layers and Builds
Every container image is a stack of filesystem diffs with a build cache on top. This lesson makes layers legible so builds stay fast and images stay small.
10 min reading
Objectives
- Explain how Dockerfile instructions become cached layers
- Order instructions so rebuilds reuse the cache instead of redoing work
- Read an image history and name what each layer added
- Use multi-stage builds to keep runtimes small without losing the toolchain
Why this matters
A team rebuilds its image on every commit and each build takes eleven minutes, because a dependency install sits above the application copy and re-runs on every code change. Moving two lines cuts the build to forty seconds. Layer order is the cheapest performance work in the whole container stack, and it is decided once in the Dockerfile, then paid on every build forever.
Concepts
Each Dockerfile instruction creates one layer: a diff against the previous filesystem state. The build cache reuses a layer when its instruction and inputs are unchanged; one changed line invalidates its own layer and every layer below it. Order follows change frequency: stable foundations (base image, system packages, language dependencies) at the top, changing application code at the bottom. COPY package.json plus install before COPY source is the canonical example: dependency layers rebuild only when dependencies change.
Image history makes this visible: each layer shows its size and command, so an oversized image always has a named culprit, usually a package cache, a build toolchain left in the runtime, or a COPY of the whole context including git history and fixtures. .dockerignore excludes the context the build never needs; multi-stage builds separate the build environment from the runtime by COPY --from the compiled artifact into a slim final image. The build stage can be heavy; the shipped stage carries only what runs.
Reproducibility means the same inputs produce the same image. Pin base image tags to digests or immutable versions instead of latest, pin dependency versions in lockfiles, and avoid timestamps and network fetches inside the build. A Dockerfile that builds differently each week is a lottery ticket; pinning turns it into a record.
Worked example
A Node service builds slowly and ships at 1.1 GB. The slow Dockerfile:
FROM node:20 WORKDIR /app COPY . . RUN npm install CMD ["node", "server.js"]
Two problems: COPY . . before install invalidates the dependency layer on every edit, and the final image carries devDependencies, npm cache and source maps. The repaired version:
FROM node:20-slim AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build FROM node:20-slim WORKDIR /app COPY --from=build /app/dist ./dist COPY package.json package-lock.json ./ RUN npm ci --omit=dev CMD ["node", "dist/server.js"]
Expected reading: dependency layers cache across code edits, the runtime stage drops the toolchain and dev dependencies, and the final image is a fraction of the original. Verify with image history before and after: fewer heavy layers, no cache directories. Lab L13 replays this on a small service with a non-root user added.
The common wrong move
FROM latest plus unpinned installs. Six months later the rebuild pulls a new major base, a dependency resolves differently, and the image that used to work fails in ways nobody can reproduce from the Dockerfile. latest is a moving target wearing a stable name. Pin everything the build needs to be the same twice.
Lab and next step
Lab L13 turns a small service into a repeatable non-root image with this layering, plus a test. Next, lesson 2 covers the user the container runs as and the runtime around it.
Quick check
An optional 4-question self-check. Answers never leave your device, are not stored, and never count toward any assessment.
Lesson feedback
No published feedback yet.
Log in and complete the lesson to leave feedback.
Exercise
Take any Dockerfile you own (or the slow one above). Show its image history sizes, reorder for cache, add .dockerignore, and report before/after build times on a no-change rebuild plus final image size.
Pass criteria
History shown with the heavy layer named; reorder puts stable steps first; no-change rebuild time and final size both reported with improvement.