Skip to content

How to Use Docker Multi-Stage Builds to Reduce Image Size

DodaTech 2 min read

In this tutorial, you'll learn about How to Use Docker Multi. We cover key concepts, practical examples, and best practices.

The Problem

Your Docker image is 1.5 GB because it includes the entire SDK, build tools, and intermediate artifacts. Production containers only need the compiled binary or runtime files, not the entire toolchain used to create them. Multi-stage builds let you use separate FROM statements in a single Dockerfile, copying only the build output into a minimal final image.

Quick Fix

1. Basic multi-stage build for a Go app

# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .

# Stage 2: Runtime
FROM alpine:3.20
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]

The final image contains only the binary and Alpine — no Go compiler, no source code, no intermediate object files.

2. Multi-stage build for a Node.js app

# Stage 1: Build frontend
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Serve with nginx
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

The final nginx image is ~25 MB instead of the ~400 MB Node.js base image.

3. Build and compare image sizes

docker build -t myapp:latest .
docker images myapp

Expected output:

REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
myapp        latest    a1b2c3d4e5f6   10 seconds ago   23.4MB

Without multi-stage, the same Go app would be ~1.1 GB. Use docker images to compare before and after.

4. Use scratch for static binaries

FROM golang:1.22 AS builder
RUN CGO_ENABLED=0 go build -o /server .

FROM scratch
COPY --from=builder /server /server
CMD ["/server"]

The scratch base image is completely empty — the image size equals the binary size plus metadata.

5. Name build stages for clarity

FROM python:3.12-slim AS base
FROM base AS dependencies
RUN pip install --no-cache-dir -r requirements.txt
FROM base AS production
COPY --from=dependencies /usr/local /usr/local
COPY . /app

Stage names make multi-stage builds self-documenting.

6. Copy specific files between stages

COPY --from=builder --chown=app:app /app/dist /app
COPY --from=builder --chmod=755 /app/binary /app/

Inspect Container Configuration

docker inspect <container-id> --format '{{json .Config}}' | python3 -m json.tool
# {
#   "Hostname": "abc123",
#   "Env": ["PATH=/usr/local/bin:..."],
#   "Cmd": ["node", "app.js"]
# }

Use docker inspect to examine the full configuration of a container. This reveals misconfigurations in environment variables, command arguments, and network settings that may not appear in logs.

Prevention

  • Always use a minimal base image for the final stage (alpine, scratch, distroless)
  • Copy only artifacts needed at runtime — no source code, no build tools
  • Use .dockerignore to exclude node_modules, .git, and build caches from the build context
  • Tag intermediate stages with meaningful names (AS <a href="/design-patterns/builder/">builder</a>, AS dependencies)
  • Build with docker build --target production to stop at a specific stage for debugging

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro