Skip to content
Laviyx Learn

Try

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.

Intermediate7 min readUpdated
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:

Terminal
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}}'
Output
Hello from a tiny image
1.54GB

More 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:

Terminal
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}}'
Output
Hello from a tiny image
multi 9.36MB
single 1.54GB

The 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:

Terminal
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}}'
Output
Hello from a tiny image
933kB

Such 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:

DOCKERFILE
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/html

The 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:

DOCKERFILE
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:

Terminal
docker build -q --target build -t hello:build . > /dev/null
docker run --rm hello:build ls /src
Output
hello
hello.c

Common mistakes#

Practice#

Practice exercise

Compare the sizes of the three hello images, sorted by tag.

Show solution
Terminal
docker images hello --format '{{.Tag}} {{.Size}}' | sort
Output
build 1.54GB
multi 9.36MB
scratch 933kB
single 1.54GB

Key takeaway#

Next: Tags and Docker Hub.

Finished this lesson?

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.