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
- Order stages: Build → test → runtime
- Copy selectively: Only needed files
- Use .dockerignore: Exclude unnecessary files
- Cache layers: Stable files first
- 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.