The Problem: Build Tools Don't Belong in Production
To compile a Go application, you need the Go toolchain — roughly 600 MB of compilers, standard libraries, and build tools. Your compiled Go binary might be 10 MB. Shipping a 600 MB image to production to run a 10 MB binary is wasteful, slow, and a security liability (every tool in the image is a potential attack vector).
Before multi-stage builds, teams worked around this with two separate Dockerfiles: one for building the artifact, and a shell script to copy the artifact out and feed it to the second Dockerfile. This was fragile and hard to maintain.
Multi-Stage Builds: One Dockerfile, Multiple Phases
Multi-stage builds allow multiple FROM statements in a single Dockerfile. Each FROM starts a completely fresh filesystem — a new "stage." You can copy specific files from one stage to another using COPY --from=<stage_name>. The final image contains only what was defined in the last stage.
In a typical two-stage Go build, the first stage starts from the full Go SDK image, downloads dependencies, and compiles a static binary. The second stage starts completely fresh from a minimal base image, copies only the compiled binary out of the builder stage (nothing else from it — not the SDK, not the source, not the build cache), and sets up a non-root user to run it. The full Go SDK image (~300 MB) is used only during the build. The final image is based on a minimal base (~7 MB) plus the binary — typically under 20 MB total.
Targeting Specific Stages
A build can also target just one specific stage rather than the whole multi-stage pipeline — useful in CI pipelines where you might want to run tests against the builder stage without producing the final runtime image at all.
Key Point: The order of instructions within a stage matters enormously for cache efficiency. Always copy dependency manifests (package.json, go.mod, requirements.txt) and install dependencies before copying your application source code. Source code changes frequently; dependencies change rarely. If you copy source code first, every code change invalidates the dependency installation cache, forcing a full re-download on every build.