Docker Multi-Stage Builds — Reduce Image Size
Advertisement
Docker Multi-Stage Builds — Reduce Image Size
Multi-stage builds separate the build process from runtime, dramatically reducing final image sizes. Learn to architect efficient production images.
Introduction
Multi-stage builds use multiple FROM statements in a single Dockerfile. Build artifacts stay in builder images while only necessary runtime files go into the final image.
- Docker Multi-Stage Builds — Reduce Image Size
- Basic Multi-Stage Pattern
- Traditional Single-Stage
- Multi-Stage Build
- Advanced Multi-Stage Architecture
- Three-Stage Build
- Language-Specific Examples
- Go Application
- Python Application
- Java Application
- Frontend Application Example
- Conditional Stages
- Development vs Production
- Optimized Build Example
- Build Optimization with BuildKit
- Analyzing Image Layers
- Performance Impact
- FAQ
Basic Multi-Stage Pattern
Traditional Single-Stage
# Problem: 1.2 GB image includes build tools
FROM node:18
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]
Size breakdown:
- Base Node image: 1000 MB
- Dependencies: 150 MB
- Build artifacts: 50 MB
- Total: 1.2 GB
Multi-Stage Build
# Stage 1: Builder (discarded after build)
FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
# Stage 2: Runtime (final image)
FROM node:18-alpine
WORKDIR /app
COPY /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]
Size breakdown:
- Runtime image: 173 MB
- Built artifacts: 50 MB
- Total: 223 MB (81% reduction)
Advanced Multi-Stage Architecture
Three-Stage Build
# Stage 1: Dependencies
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Builder
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # Includes dev dependencies
COPY . .
RUN npm run build
# Stage 3: Runtime
FROM node:18-alpine
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY /app/dist ./dist
COPY /app/package.json ./
USER 1001
EXPOSE 3000
CMD ["node", "dist/server.js"]
Language-Specific Examples
Go Application
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
# Runtime stage
FROM gcr.io/distroless/base-debian11
COPY /app/app /
EXPOSE 8080
CMD ["/app"]
Results:
- With go:1.21: 800 MB
- With distroless: 20 MB
Python Application
# Build stage
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
# Runtime stage
FROM python:3.11-slim
WORKDIR /app
COPY /root/.local /root/.local
COPY app.py .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
Results:
- Traditional: 900 MB
- Multi-stage: 250 MB
Java Application
# Build stage
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package
# Runtime stage
FROM eclipse-temurin:17-jre-alpine
COPY /app/target/app.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
Frontend Application Example
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage: serve with nginx
FROM nginx:alpine
# Copy built assets
COPY /app/dist /usr/share/nginx/html
# Copy nginx config
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Create nginx.conf:
events {
worker_connections 1024;
}
http {
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:3000;
}
}
}
Size comparison:
- With Node runtime: 1.2 GB
- With Nginx: 150 MB
Conditional Stages
Development vs Production
# Stage 1: Base
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./
# Stage 2: Development (includes devDeps)
FROM base AS dev
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Stage 3: Production (only production deps)
FROM base AS prod-deps
RUN npm ci --only=production
# Stage 4: Production (final)
FROM base AS prod
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
USER 1001
EXPOSE 3000
CMD ["node", "dist/server.js"]
Build specific stage:
# Build dev image
docker build --target dev -t myapp:dev .
# Build prod image
docker build --target prod -t myapp:prod .
Optimized Build Example
# syntax=docker/dockerfile:1.4
# Stage 1: Builder
FROM node:18-alpine AS builder
WORKDIR /build
# Cache dependencies layer
COPY package*.json ./
RUN npm ci
# Copy source and build
COPY . .
RUN npm run build
# Prune dev dependencies
RUN npm prune --only=production
# Stage 2: Runtime
FROM node:18-alpine AS runtime
# Security: non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Copy artifacts
COPY /build/dist ./dist
COPY /build/node_modules ./node_modules
COPY /build/package*.json ./
USER nodejs
EXPOSE 3000
HEALTHCHECK \
CMD node healthcheck.js
CMD ["node", "dist/server.js"]
Build Optimization with BuildKit
Enable experimental BuildKit features:
export DOCKER_BUILDKIT=1
docker build -t myapp:1.0 .
Advanced Dockerfile syntax:
# syntax=docker/dockerfile:1.4
FROM node:18-alpine AS builder
WORKDIR /app
# Cache mount: shared between builds
RUN \
npm ci
# Secrets during build
RUN \
npm ci --registry="$(cat /run/secrets/github_token)"
COPY . .
RUN npm run build
FROM node:18-alpine
COPY /app/dist ./dist
CMD ["node", "dist/server.js"]
Build with secrets:
DOCKER_BUILDKIT=1 docker build \
--secret github_token=/path/to/token \
-t myapp:1.0 .
Analyzing Image Layers
# View all layers and their sizes
docker history myapp:1.0
# Output:
# IMAGE CREATED CREATED BY SIZE
# a1b2c3d4 2 minutes ago /bin/sh -c npm run build 45MB
# b2c3d4e5 2 minutes ago COPY --from=builder /app ... 30MB
# c3d4e5f6 3 minutes ago FROM node:18-alpine 173MB
# Inspect detailed image structure
docker inspect myapp:1.0 | jq '.RootFS.Layers'
# Compare image sizes
docker images myapp
# REPOSITORY TAG SIZE
# myapp prod 95MB
# myapp dev 800MB
Performance Impact
| Metric | Traditional | Multi-Stage | Savings |
|---|---|---|---|
| Image Size | 1.2 GB | 150 MB | 87% smaller |
| Build Time | 2m 30s | 1m 45s | 30% faster |
| Pull Time | 90s | 10s | 89% faster |
| Push Time | 60s | 5s | 92% faster |
FAQ
Q: Why does multi-stage build reduce image size? A: Build tools (compilers, dev dependencies) are not copied to the final image. Only runtime dependencies and built artifacts are included.
Q: Can I use multiple FROM statements without multi-stage? A: Multiple FROM statements create multiple images. Multi-stage references previous stages with COPY --from=builder.
Q: Does multi-stage build take longer? A: Usually faster overall because smaller images push/pull quickly, offsetting slightly longer build time.
Advertisement
← Previous
Docker vs Podman — Which to Use?