Multi-Stage Docker Builds
What are Multi-Stage Builds?
Multi-stage builds use multiple FROM statements in a Dockerfile to create optimized, smaller final images by separating build and runtime environments.
Benefits
- Smaller images: Only runtime dependencies
- Security: No build tools in production
- Faster deployments: Smaller image size
- Clean separation: Build vs runtime
Angular Multi-Stage Build
# Stage 1: Build
FROM node:18-alpine AS build
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci
# Build application
COPY . .
RUN npm run build --configuration production
# Stage 2: Production
FROM nginx:alpine
COPY --from=build /app/dist/my-app /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# Result: ~25MB (vs ~1GB with build tools).NET Multi-Stage Build
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Restore dependencies
COPY ["MyApp.csproj", "./"]
RUN dotnet restore
# Build and publish
COPY . .
RUN dotnet publish -c Release -o /app/publish
# Stage 2: Production
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["dotnet", "MyApp.dll"]
# Result: ~200MB (vs ~700MB with SDK)Node.js Multi-Stage Build
# Stage 1: Dependencies
FROM node:18-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Build
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Production
FROM node:18-alpine
WORKDIR /app
# Copy production dependencies
COPY --from=dependencies /app/node_modules ./node_modules
# Copy built application
COPY --from=build /app/dist ./dist
COPY package*.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Result: ~150MB (vs ~500MB with dev dependencies)Advanced Multi-Stage
# Stage 1: Base
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./
# Stage 2: Development dependencies
FROM base AS dev-dependencies
RUN npm ci
# Stage 3: Production dependencies
FROM base AS prod-dependencies
RUN npm ci --only=production
# Stage 4: Build
FROM dev-dependencies AS build
COPY . .
RUN npm run build
RUN npm run test
# Stage 5: Production
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=prod-dependencies /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package*.json ./
USER node
CMD ["node", "dist/index.js"]Build Arguments
FROM node:18-alpine AS build
ARG NODE_ENV=production
ARG API_URL
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html# Build with arguments
docker build \
--build-arg NODE_ENV=production \
--build-arg API_URL=https://api.example.com \
-t myapp:latest .Target Stages
# Development stage
FROM node:18 AS development
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
# Production stage
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=development /app/dist ./dist
CMD ["node", "dist/index.js"]# Build development image
docker build --target development -t myapp:dev .
# Build production image
docker build --target production -t myapp:prod .Caching Optimization
# Optimize layer caching
FROM node:18-alpine AS build
WORKDIR /app
# Cache dependencies (changes less frequently)
COPY package*.json ./
RUN npm ci
# Copy source code (changes more frequently)
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]Full-Stack Example
# Frontend (Angular)
FROM node:18-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build --prod
# Backend (.NET)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS backend-build
WORKDIR /src
COPY backend/*.csproj ./
RUN dotnet restore
COPY backend/ ./
RUN dotnet publish -c Release -o /app
# Service (Node.js)
FROM node:18-alpine AS service-build
WORKDIR /app/service
COPY service/package*.json ./
RUN npm ci --only=production
COPY service/ ./
RUN npm run build
# Final image with all components
FROM nginx:alpine
COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html
COPY --from=backend-build /app /app/backend
COPY --from=service-build /app/service/dist /app/serviceCI/CD Integration
# GitHub Actions
name: Multi-Stage Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build production image
run: |
docker build \
--target production \
--build-arg NODE_ENV=production \
-t myapp:${{ github.sha }} \
-t myapp:latest \
.
- name: Push to registry
run: |
docker push myapp:${{ github.sha }}
docker push myapp:latestSize Comparison
# Without multi-stage
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/index.js"]
# Size: ~1.2GB
# With multi-stage
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
# Size: ~150MBInterview Tips
- Explain multi-stage: Separate build and runtime
- Show examples: Angular, .NET, Node.js
- Demonstrate benefits: Smaller images, security
- Discuss caching: Layer optimization
- Mention targets: Development vs production
- Show size comparison: Before and after
Summary
Multi-stage Docker builds create optimized images by separating build and runtime stages. Use build stage for compilation and dependencies, production stage for runtime only. Reduces image size by 80-90%. Improves security by excluding build tools. Supports build arguments and target stages. Essential for production Docker images.
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.