Isaac.

Docker Multi-Stage Builds

Optimize Docker images with multi-stage builds.

By EMEPublished: February 20, 2025
dockermulti-stage buildsoptimizationcontainers

A Simple Analogy

Multi-stage Docker builds are like construction cleanup. You keep temporary scaffolding during building, but remove it before the final product ships.


Why Multi-Stage Builds?

  • Smaller images: Remove build artifacts
  • Faster deployment: Less bandwidth
  • Security: Don't ship source code
  • Separation: Build vs runtime dependencies
  • Cost savings: Storage and network

Node.js Example

# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install all dependencies
RUN npm ci

# Copy source
COPY . .

# Build
RUN npm run build

# Stage 2: Runtime
FROM node:18-alpine
WORKDIR /app

# Copy only production dependencies
COPY package*.json ./
RUN npm ci --only=production

# Copy built app from builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ["node", "dist/index.js"]

.NET Example

# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

COPY ["MyApp.csproj", "."]
RUN dotnet restore "MyApp.csproj"

COPY . .
RUN dotnet build "MyApp.csproj" -c Release -o /app/build

# Stage 2: Publish
FROM build AS publish
RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish

# Stage 3: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=publish /app/publish .

EXPOSE 80
ENTRYPOINT ["dotnet", "MyApp.dll"]

Advanced: Conditional Stages

# Stage 1: Development
FROM node:18-alpine AS dev
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]

# Stage 2: Test
FROM dev AS test
RUN npm run test

# Stage 3: Build
FROM test AS builder
RUN npm run build

# Stage 4: Production
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]

Image Size Comparison

Without multi-stage:
- npm install (all deps)        500MB
- node_modules                  400MB
- source code                   50MB
- dist (built files)            20MB
TOTAL: 970MB

With multi-stage:
- dist (from builder)           20MB
- npm ci --only=production      300MB
- node_modules (prod only)      280MB
TOTAL: 300MB (69% reduction!)

Build Arguments

FROM node:18-alpine AS builder
ARG NODE_ENV=production
ARG BUILD_DATE
ARG VCS_REF

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:18-alpine
LABEL org.opencontainers.image.created=$BUILD_DATE
LABEL org.opencontainers.image.revision=$VCS_REF

COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --only=production

EXPOSE 3000
CMD ["node", "dist/index.js"]

Build with:

docker build \
  --build-arg NODE_ENV=production \
  --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
  --build-arg VCS_REF=$(git rev-parse --short HEAD) \
  -t myapp:latest .

Best Practices

  1. Order stages: Build → test → runtime
  2. Copy selectively: Only needed files
  3. Use .dockerignore: Exclude unnecessary files
  4. Cache layers: Stable files first
  5. Minimize runtime: Keep final stage small

Related Concepts

  • Layer caching
  • .dockerignore files
  • Image registry optimization
  • Container security

Summary

Multi-stage builds dramatically reduce image sizes by separating build and runtime environments. Use them to optimize deployment speed and reduce storage costs.