Multistage Docker Builds

Understanding, implementing, and optimizing multistage builds for production applications

Introduction to Multistage Builds

Docker multistage builds are a powerful feature that allows you to use multiple temporary build stages in a single Dockerfile, ultimately producing a single, optimized final image. This approach helps solve one of the most challenging aspects of Docker: creating efficient, lightweight images that don't include unnecessary build tools or dependencies.

Real-world Analogy: Constructing a House

Think of building a Docker image like constructing a house:

  • Traditional single-stage build: After building your house, all construction equipment, materials, blueprints, and worker supplies remain on your property permanently. Your "house image" includes everything used to build it.
  • Multistage build: After construction, you keep only the finished house. All scaffolding, construction equipment, and temporary work materials are removed. Your "house image" contains only what's needed for living in it.

Why Use Multistage Builds?

The Problems with Single-Stage Builds

Before multistage builds, developers faced a difficult choice:

Key Benefits of Multistage Builds

Traditional vs. Multistage Build Comparison

flowchart TB subgraph "Traditional Single-Stage Build" A1[Base Image] --> B1[Install Build Tools] B1 --> C1[Copy Source Code] C1 --> D1[Build Application] D1 --> E1[Configure Runtime] E1 --> F1["Final Image
(Contains Everything)"] end subgraph "Multistage Build" A2[Build Stage: Base Image] --> B2[Install Build Tools] B2 --> C2[Copy Source Code] C2 --> D2[Build Application] E2[Runtime Stage: Base Image] --> F2[Configure Runtime] D2 -- "Copy only built artifacts" --> F2 F2 --> G2["Final Image
(Runtime Only)"] end

Multistage Build Syntax

Basic Structure

The key to multistage builds is using multiple FROM statements in your Dockerfile. Each FROM statement begins a new build stage.

# Build stage
FROM node:18 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Runtime stage
FROM node:18-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm install --production
EXPOSE 3000
CMD ["node", "dist/server.js"]

Key Syntax Elements

Practical Multistage Build Examples

Example 1: Node.js Application

This example shows a typical React frontend with Node.js backend:

# Build stage for frontend
FROM node:18 AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build

# Build stage for backend
FROM node:18 AS backend-build
WORKDIR /app/backend
COPY backend/package*.json ./
RUN npm install
COPY backend/ ./
RUN npm run build

# Final stage
FROM node:18-alpine
WORKDIR /app
COPY --from=backend-build /app/backend/dist ./
COPY --from=frontend-build /app/frontend/build ./public
COPY --from=backend-build /app/backend/package*.json ./
RUN npm install --production
EXPOSE 3000
CMD ["node", "server.js"]

Example 2: TypeScript Application

Here's an example for a TypeScript Node.js application:

# Build stage
FROM node:18 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build

# Production stage
FROM node:18-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm install --production
EXPOSE 3000
CMD ["node", "dist/index.js"]

Advanced Multistage Build Techniques

Using Different Base Images

One powerful technique is using different base images for build and runtime stages:

# Build stage - uses a larger image with build tools
FROM node:18 AS build
WORKDIR /app
COPY . .
RUN npm install && npm run build

# Runtime stage - uses a minimal image
FROM alpine:3.18
RUN apk add --no-cache nodejs
WORKDIR /app
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]

Builder Pattern with Multiple Specialized Stages

For complex applications, you can use multiple specialized build stages:

flowchart TB base[Base Builder] --> deps[Dependencies Builder] base --> assets[Assets Builder] deps --> test[Test Stage] assets --> test test --> production[Production Image]
# Base builder with common tools
FROM node:18 AS base
WORKDIR /app
COPY package*.json ./

# Dependencies builder
FROM base AS dependencies
RUN npm install

# Asset builder for frontend
FROM base AS builder
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Test stage
FROM dependencies AS test
COPY . .
RUN npm test

# Production image
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=dependencies /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]

Using Buildkit Features

Docker BuildKit offers additional features for multistage builds:

# Enable BuildKit features with this syntax
# Example using build secrets
FROM node:18 AS build
WORKDIR /app
COPY . .
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm install

# Example using cache mounts for faster builds
FROM node:18 AS deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm install

Best Practices for Multistage Builds

Optimizing Layer Caching Example

This example demonstrates proper ordering of operations for efficient caching:

# Good caching - Dependencies installed first, separated from code
FROM node:18 AS build
WORKDIR /app
# Copy only package files first (changes less frequently)
COPY package*.json ./
RUN npm install
# Then copy code (changes more frequently)
COPY . .
RUN npm run build

# Bad caching - Everything copied at once, breaking caching for dependencies
FROM node:18 AS build-inefficient
WORKDIR /app
# Copying everything at once forces npm install to run on every code change
COPY . .
RUN npm install
RUN npm run build

Real-world Applications

Case Study: Full Stack JavaScript Application

Consider a typical full-stack JavaScript application with:

Frontend Build Node.js + Build Tools (300+ MB) Backend Build Node.js + TypeScript (300+ MB) Test Stage Testing Framework (350+ MB) Static Assets HTML/CSS/JS API Server Files Compiled JS Production Image Node.js Alpine (~120 MB)

Complete Multistage Dockerfile Example

# Base development dependencies stage
FROM node:18 AS base
WORKDIR /app
COPY package*.json ./
RUN npm install

# Frontend build stage
FROM base AS frontend-build
WORKDIR /app/client
COPY client/package*.json ./
RUN npm install
COPY client/ ./
RUN npm run build

# Backend build stage
FROM base AS backend-build
WORKDIR /app/server
COPY server/package*.json ./
RUN npm install
COPY server/ ./
RUN npm run build

# Test stage
FROM base AS test
WORKDIR /app
COPY --from=frontend-build /app/client ./client
COPY --from=backend-build /app/server ./server
RUN npm test

# Production stage
FROM node:18-alpine
WORKDIR /app
# Copy backend build artifacts
COPY --from=backend-build /app/server/dist ./
# Copy frontend static assets
COPY --from=frontend-build /app/client/build ./public
# Copy package files and install only production dependencies
COPY --from=backend-build /app/server/package*.json ./
RUN npm install --production
EXPOSE 3000
CMD ["node", "index.js"]

Cost Savings and Performance Improvements

Real-world benefits from this approach:

Hands-on Exercises

Exercise 1: Convert a Single-stage Dockerfile

Take the following single-stage Dockerfile and convert it to a multistage build:

# Single-stage Dockerfile
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/server.js"]

Exercise 2: Optimize a React Application

Create a multistage Dockerfile for a React application that:

  1. Uses Node.js to build the application
  2. Uses Nginx to serve the static files
  3. Includes a testing stage
  4. Optimizes for caching and minimal final size

Exercise 3: Implement BuildKit Features

Enhance a multistage Dockerfile to use BuildKit features:

  1. Add a build cache for npm packages
  2. Use a build secret for accessing a private npm registry
  3. Implement parallel build stages where appropriate

Summary and Next Steps

Key Takeaways

Further Learning

Additional Practice Activities

Activity 1: Image Size Comparison

Create both a single-stage and multistage Dockerfile for the same application, then compare:

Activity 2: Real-world Application Conversion

Take an existing application from your projects and convert its Dockerfile to a multistage build. Document the before and after metrics to demonstrate the improvements.

Activity 3: Advanced Multistage Pipeline

Create a sophisticated multistage Dockerfile that includes:

Ensure the stages are ordered to maximize caching efficiency.