Small images pull faster, deploy faster, cost less to store, and expose fewer packages to attackers. Yet naive Dockerfiles routinely produce images ten times larger than necessary. This lesson covers the techniques that shrink images - above all, multi-stage builds.
Three usual suspects:
node:20 is roughly 400 MB before your code arrives; node:20-alpine is about a tenth of that.That third point surprises people. This pattern does not save space:
RUN wget https://example.com/big.tar.gz
RUN tar xzf big.tar.gz && ./install.sh
RUN rm big.tar.gz # too late - it lives in layer 1 foreverDownload, use, and delete within a single RUN so the layer never contains the file:
RUN wget https://example.com/big.tar.gz \
&& tar xzf big.tar.gz && ./install.sh \
&& rm big.tar.gzThe biggest wins come from multi-stage builds: use one stage with all the build tooling, then copy only the finished artifacts into a clean final stage. Everything not copied is discarded.
A TypeScript Node API:
# ---- Stage 1: build ----
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci # includes dev dependencies
COPY . .
RUN npm run build # emits dist/
# ---- Stage 2: runtime ----
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]The --from=build flag copies files out of the earlier stage. TypeScript, dev dependencies, and source files never reach the final image.
The effect is even more dramatic for compiled languages. A Go binary needs nothing but itself:
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/app
FROM scratch
COPY --from=build /bin/app /app
ENTRYPOINT ["/app"]scratch is the empty image - the result is often under 15 MB, down from a gigabyte-plus build environment.
A good default: slim for compatibility, alpine when you have verified your dependencies work on it.
docker images myapp # overall size
docker history myapp:latest # size contributed by each layerThe excellent open source tool dive lets you browse each layer file-by-file and spot waste (dive myapp:latest).
.dockerignore (git history and node_modules are classic stowaways).npm ci --omit=dev, pip install --no-cache-dir, or apt-get install --no-install-recommends plus rm -rf /var/lib/apt/lists/*.COPY --from=stage moves only the artifacts you need into the final image.docker history and the dive tool.Next up: Docker Best Practices and Security, the final lesson, covering non-root users, image scanning, and production hardening.