Multi-Stage Builds: Slash Your Final Image Size
Use multi-stage Dockerfiles to compile in one layer and copy only the runtime artifacts forward — image sizes drop from gigabytes to tens of megabytes.
Netbay Cloud Team
Netbay Engineering
On this page
Every compiler you leave in a final image is dead weight that ships to every node, fills your registry, and extends the window between "vulnerable dependency" and "running patched binary". Multi-stage builds solve this cleanly: one stage compiles and packages, a later stage copies only the artifacts forward, and the build toolchain never survives into the shipped image. Mature applications routinely drop from over a gigabyte to well under 100 MB this way — not by tuning flags, but by deleting an entire category of software from the runtime. This guide shows the pattern in a Go binary and a frontend static site, then explains the flags that keep the trick honest.
How a Multi-Stage Build Is Written
A multi-stage Dockerfile is simply several FROM lines. Everything before the second FROM is stage zero, and COPY --from=0 (or COPY --from=builder when the stage has a name) reaches back into it. Docker executes only what the final stage actually needs, so a stage whose artifacts are never copied can fail and still not break the build — which is why older single-stage files so often carried dead weight nobody noticed.
A Compiled Service on Scratch
# ---- stage 0: build ----
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/server .
# ---- stage 1: run ----
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /out/server /server
USER 10001:10001
ENTRYPOINT ["/server"]
EXPOSE 8080The scratch base is an intentionally empty image, so the final artifact is exactly two files: the static binary and the CA bundle it needs for TLS. CGO_ENABLED=0 produces a pure static binary with no libc dependency, and the -ldflags strip debugging symbols. A Node or Java service cannot use scratch, but the same shape works with alpine:3.21 as a minimal runtime that still carries a package manager and shell for debugging.
Frontend and Compile-and-Run Stacks
Building a Static Site and Serving It Small
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
USER nginxHere the runtime is nginx:alpine, the build output is a directory of static files, and the node toolchain vanishes. The two stages have entirely different personalities: stage zero is fat on purpose so the compiler has everything, stage one is starved on purpose so the deployed surface is tiny. Use --target to build just one stage during development (docker build --target build) and rely on the full target for release.
Name Stages and Copy Selectively
COPY --from can only pull paths, not run commands, so anything that needs transformation happens inside the producing stage. Give every reusable stage an AS name: it makes docker build --target builder, docker build --target test, and --target publish legible and lets one file serve CI, tests, and production.
Flags That Keep Multi-Stage Honest
Three flags matter. --target picks the stage to build. --output (docker build --output type=local,dest=./out .) dumps a stage's filesystem to the host without building a final image, which is how many CI pipelines extract just the compiled artifact. --progress=plain gives the full raw log instead of the interactive spinner, so CI failures show exactly which stage broke. Verify the payoff with docker image inspect on the finished image — compare config.Cmd, config.Healthcheck, and os/layers against the original single-stage build you replaced. The same file can also expose a developer-friendly target: keep a test stage that runs the linter and unit tests in the same Dockerfile, build it in CI with --target test, and ship the slim runtime image only on release.
Takeaway
Multi-stage builds rekey your images from "whole toolchain plus app" to "app plus what it needs at runtime", usually an order-of-magnitude size drop. That is a smaller pull on every node, a smaller scan surface in every registry, and a smaller blast radius if a base-layer library is ever compromised. The pattern is two FROM lines and one COPY --from, and the payoff compounds every time the image is deployed. Deploying on a Netbay VPS with fast, roomy SSD storage makes the pull speed difference obvious in practice — spin up a host at netbayhosts.in and rebuild your worst image today.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance