Docker and Kubernetes course menu
Docker Multi-Stage Builds Explained with Examples
Shrink Docker images with multi-stage builds: compile in one stage and ship only the result, with C, React and Node examples, and distroless and scratch images.
On this page
Building software needs compilers and tools, and running it does not. A multi-stage build uses one stage to build, and copies only the result into a small final image.
The problem#
Here is a small C program, compiled in a gcc image:
mkdir hello
cd hello
cat > hello.c <<'EOF'
#include <stdio.h>
int main(void) {
printf("Hello from a tiny image\n");
return 0;
}
EOF
cat > Dockerfile.single <<'EOF'
FROM gcc:15
WORKDIR /src
COPY hello.c .
RUN gcc -static -o hello hello.c
CMD ["./hello"]
EOF
docker build -q -f Dockerfile.single -t hello:single . > /dev/null
docker run --rm hello:single
docker images hello:single --format '{{.Size}}'Hello from a tiny image
1.54GBMore than a gigabyte, for a tiny program.
Two stages#
AS names a stage, and COPY --from takes files from it. Only the last stage becomes the image:
cat > Dockerfile <<'EOF'
FROM gcc:15 AS build
WORKDIR /src
COPY hello.c .
RUN gcc -static -o hello hello.c
FROM alpine:3.24
COPY --from=build /src/hello /usr/local/bin/hello
CMD ["hello"]
EOF
docker build -q -t hello:multi . > /dev/null
docker run --rm hello:multi
docker images hello --format '{{.Tag}} {{.Size}}'Hello from a tiny image
multi 9.36MB
single 1.54GBThe compiler is not in the final image, so it is smaller, and it contains less that can be attacked.
FROM scratch#
scratch is a completely empty image. A statically linked program needs nothing else:
cat > Dockerfile.scratch <<'EOF'
FROM gcc:15 AS build
WORKDIR /src
COPY hello.c .
RUN gcc -static -o hello hello.c
FROM scratch
COPY --from=build /src/hello /hello
CMD ["/hello"]
EOF
docker build -q -f Dockerfile.scratch -t hello:scratch . > /dev/null
docker run --rm hello:scratch
docker images hello:scratch --format '{{.Size}}'Hello from a tiny image
933kBSuch an image has no shell, so there is nothing to exec into. Go and Rust programs often ship this way. Distroless images are similar, with a language runtime and little else.
Frontend apps#
This is the classic use. Build a React app with Node.js, then serve the static files with Nginx:
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.30-alpine
COPY --from=build /app/dist /usr/share/nginx/htmlThe final image contains no Node.js and no node_modules.
Node.js with a build step#
For TypeScript, compile with dev dependencies, then ship only production ones:
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]Build a single stage#
--target stops at a named stage. That is useful for a stage that runs tests, or for development:
docker build -q --target build -t hello:build . > /dev/null
docker run --rm hello:build ls /srchello
hello.cCommon mistakes#
Practice#
Practice exercise
Compare the sizes of the three hello images, sorted by tag.
Show solution
docker images hello --format '{{.Tag}} {{.Size}}' | sortbuild 1.54GB
multi 9.36MB
scratch 933kB
single 1.54GBKey takeaway#
Next: Tags and Docker Hub.
Finished this lesson?
Related lessons
Recommended Docker and Kubernetes resources
- Docker Deep Dive by Nigel Poulton
A practical guide to how Docker works, for readers who have finished the basics.
Some links are affiliate links. If you buy through them, Laviyx Learn may earn a commission at no extra cost to you. As an Amazon Associate, we earn from qualifying purchases.