Reducing Docker Image Size with Multi-Stage Builds and Distroless Bases

A Node or Go image built naively from a full Debian base routinely ships 800MB to 1GB for an application whose actual runtime footprint is a few dozen megabytes. The gap isn't the application — it's the compiler toolchain, package manager caches, shared libraries, and shell utilities that were only ever needed to build the thing, not run it, and that never get removed because a single-stage Dockerfile has no mechanism to leave them behind. This tutorial rebuilds that image twice: once to separate build-time weight from runtime weight with multi-stage builds, and once more to strip the runtime environment down to a distroless base with no shell, no package manager, and a meaningfully smaller attack surface.

Prerequisites

  • Docker Engine 23.0+ with BuildKit enabled (docker buildx version to confirm)
  • A working single-stage Dockerfile for a compiled or transpiled application (examples below use Go, since its static-binary output makes the distroless step unambiguous)
  • Familiarity with FROM, COPY, and RUN — this tutorial assumes you can already write a basic Dockerfile
  • Optional but useful: docker history and docker images for measuring the before/after difference yourself