sehaj/learn
Docker/02 Images

02.05 · with code · 11 min

Making images small

Where image size actually comes from, the four changes that cut most of it, and how to find the layer that's bloating yours.

The core idea

An image's size is the sum of its layers, and layers only ever add. So making an image smaller comes down to two questions: which base do you start from, and what does each of your steps leave behind?

Smaller images pull faster, start faster on new servers, cost less to store and contain fewer packages that can have vulnerabilities. But size is a means, not a goal. Don't trade away debuggability for the last few megabytes.

Where the bytes are

Every number in this lesson was measured on the notes API (on an arm64 Mac; amd64 sizes differ slightly):

ImageDisk usageContent size (compressed)
node:22 (full Debian base)1.63 GB416 MB
first Dockerfile on node:22-slim425 MB95.5 MB
…with a .dockerignore393 MB90.6 MB
…as a multi-stage build349 MB80.4 MB
multi-stage on node:22-alpine238 MB61.7 MB
multi-stage on distroless212 MB52.2 MB

The biggest single decision is the base image. node:22 ships compilers, git and Python, which are useful for building native modules and useless at runtime. Your own layers are usually small next to it.

The four changes that matter

1. Pick a slimmer base. -slim, -alpine and distroless variants exist for exactly this. The trade-offs between them are the next lesson (02.07 Choosing a base image).

2. Multi-stage builds. Build tools stay in a stage that's thrown away (02.04 Multi-stage builds).

3. A .dockerignore. Stops local node_modules, .git and build output riding along (02.03 Build context and .dockerignore).

4. Clean up in the same step that made the mess. This is the one people get wrong. Watch:

Dockerfile (A, B, C)
# A: install curl
RUN apt-get update && apt-get install -y curl

# B: install, then clean up in a separate step
RUN apt-get update && apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# C: install only what's needed and clean up in the same step
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
❯terminal
$ docker images
IMAGE DISK USAGE CONTENT SIZE
apt:A 404MB 101MB
apt:B 404MB 101MB
apt:C 363MB 84.4MB
node:22-slim 349MB 82.6MB

Output trimmed. B saved nothing: the package lists are still in the earlier layer, and the rm only adds a layer that hides them. C is 41 MB smaller because --no-install-recommends skips optional packages, and the cleanup happens before the layer is saved.

Predict first

Your Dockerfile downloads a 200 MB tarball, extracts a 5 MB binary, and deletes the tarball, each in its own RUN. Roughly how much do these steps add to the image?

Commit to an answer before you read on.

Finding what's big

docker history shows each layer's size, so the culprit usually jumps out:

❯terminal
$ docker history apt:B
CREATED BY SIZE
RUN /bin/sh -c rm -rf /var/lib/apt/lists/* #… 20.5kB
RUN /bin/sh -c apt-get update && apt-get ins… 37.2MB
...

For a file-by-file view of each layer, the open-source tool dive (dive notes-api) lets you browse layers and see exactly which files each one added or changed.

Language-specific habits

  • Node: npm ci --omit=dev in the final stage; don't ship node_modules from your machine.
  • Python: pip install --no-cache-dir, and build wheels in an earlier stage.
  • Go, Rust: compile a static binary in a build stage and copy only that binary into a tiny final image.
  • apt: --no-install-recommends, and rm -rf /var/lib/apt/lists/* in the same RUN.
CheckpointShrink it and prove it

Build the notes API on node:22-slim and on node:22-alpine (change both FROM lines). Compare them with docker images, then run docker history on each and find the largest layer you added yourself.

STUCK? ▸

No change after adding a cleanup line: it's probably in its own RUN. Merge it into the step that created the files.

Recap

Interview questions

MidfreqMust-knowScenario

Your team's image is 1.5 GB. How would you make it smaller?

Report a mistakeEdit this page
☰ Images02.05 · 11/44next →