Docker Multi-Stage Builds
Multi-stage builds allow you to create smaller images by separating build and runtime.
Basic Concept
# Stage 1: Build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
Benefits
- Smaller image: Only copy what is needed
- Faster builds: More effective layer caching
- Security: Build tools are not included in production
Multi-Stage Examples
Node.js App
# Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
Go App
# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server
# Production stage
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]
Java App
# 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 package -DskipTests
# Production stage
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
Use Case: Application
# Frontend
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
# Backend
FROM node:18 AS backend-builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=backend-builder /app/dist ./dist
COPY --from=backend-builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
Everyday Use Case
Case: A developer needs to reduce the image size.
# Build multi-stage
docker build -t myapp:latest .
# Compare sizes
docker images myapp
# Single stage: ~900MB
# Multi-stage: ~150MB
Named Stages
# Named stages for clarity
FROM node:18 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:18 AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
Best Practices
- Use named stages: For documentation
- Copy selectively: Only copy what is needed
- Use .dockerignore: To reduce the context
- Cache dependencies: Copy package*.json first
- Use slim images: For the production stage
Conclusion
Multi-stage builds are very important for optimizing Docker images. Use them for all production applications.