Implementing Multi-Stage Builds
1. Creating Multi-Stage Dockerfile
| Stage | Description | Use Case |
|---|---|---|
| FROM node:18-alpine AS builder | Initial stage for building application dependencies | Optimizing image size by separating build-time and runtime environments |
| WORKDIR /app | Sets working directory in the container | Organizing files and commands within a specific path |
| COPY package*.json . | Copies package files to the container | Installing dependencies in the builder stage |
| RUN npm ci --only=production | Installs production-only dependencies | Reduces image size by excluding devDependencies |
| FROM node:18-alpine | Final stage using minimal runtime image | Ensures lightweight, secure runtime environment |
| WORKDIR /app | Sets working directory in the final container | Organizes files and commands within a specific path |
| COPY --from=builder /app/node_modules ./node_modules | Copies installed dependencies from builder stage | Transfers necessary modules without build tools |
| COPY . . | Copies application code to the container | Includes source files in the final image |
| EXPOSE 3000 | Documents the port the application listens on | Helps with container orchestration and networking |
| CMD ["npm", "start"] | Defines the default command to run when starting the container | Starts the application in the final stage |
Example: Multi-Stage Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci --only=production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Note: Multi-stage builds reduce image size significantly by using separate stages for building and running the application.
2. Using Named Build Stages
| Stage Name | Usage | Description |
|---|---|---|
| FROM --platform=... AS builder | FROM --platform=linux/amd64 AS builder |
Sets a named build stage for building artifacts |
| FROM --platform=... AS runtime | FROM --platform=linux/amd64 AS runtime |
Sets a named build stage for the final runtime environment |
Example: Multi-stage Dockerfile with Named Stages
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:18 AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
Note: Named stages improve readability and allow reusing intermediate build artifacts.
3. Copying Artifacts Between Stages
| Source Stage | Destination Stage | Copy Command | Notes |
|---|---|---|---|
| builder | production | COPY --from=builder /app/dist /app/dist |
Copy built artifacts from builder stage to production stage |
| builder | testing | COPY --from=builder /app/test-results /app/test-results |
Copy test results from builder to testing stage |
Example: Copying Build Artifacts
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
EXPOSE 3000
CMD ["node", "dist/index.js"]
Note: The
--from flag specifies the source stage name or index, and the paths are relative to the build context.
4. Targeting Specific Build Stage
| Option | Values | Description |
|---|---|---|
| --target | stage-name |
Specifies the build stage to stop at |
Note: The
--target flag allows you to build only a specific stage in a multi-stage Dockerfile, skipping all previous stages.
5. Optimizing Build Cache Layers
| Layer | Description | Optimization Tip |
|---|---|---|
| FROM | Base image layer | Use multi-stage builds to minimize base image size and avoid unnecessary dependencies. |
| RUN | Command execution layer | Combine multiple RUN commands with && to reduce the number of layers. |
| COPY / ADD | File copy layer | Use .dockerignore to exclude unnecessary files and directories from the build context. |
| ENV | Environment variable layer | Set environment variables early in the Dockerfile to allow for better caching. |
Example: Optimizing RUN Commands
# Bad - Multiple RUN commands
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
# Good - Combined RUN commands
RUN apt-get update && \
apt-get install -y curl wget
Example: Using .dockerignore
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
6. Implementing Build-Time Dependencies
| Dependency Type | Description | Usage Example | Notes |
|---|---|---|---|
| build-arg | Defines build-time variables available only during the build process. | |
Useful for versioning or customizing builds without changing the image. |
| COPY --from | Copies files from another stage in the same Dockerfile. | |
Enables multi-stage builds to reuse artifacts from previous stages. |
| RUN with --mount | Mounts temporary filesystems or caches during build time. | |
Improves build performance by caching dependencies. |
Example: Using Build-Time Args
FROM node:18
ARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENV
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Note: Build-time args are not available at runtime and do not persist in the final image.
7. Reducing Final Image Size
| Strategy | Description | Example |
|---|---|---|
| Multi-stage builds | Use separate build stages to compile and test, then copy only necessary artifacts to the final stage. | |
| Use minimal base images | Choose lightweight base images like Alpine or Slim variants to reduce image size. | FROM node:18-alpine |
| Clean package manager cache | Remove package manager caches after installing dependencies to reduce image size. | |
Example: Multi-stage build with size optimization
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
CMD ["./main"]
Note: Multi-stage builds allow you to use a full-featured image for building and testing, while shipping only the minimal runtime environment in the final image.
8. Sharing Layers Between Stages
| Layer | Description | Usage |
|---|---|---|
| FROM | Base image for the stage | FROM node:18-alpine AS builder |
| COPY --from | Copy files from another stage | COPY --from=builder /app/dist ./dist |
Example: Sharing Dependencies
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Note: Layers are shared using the
--from flag in COPY and ADD instructions.9. Using External Images in Stages
| Stage | Base Image | Description |
|---|---|---|
| FROM | node:18-alpine |
Starts with a Node.js 18 Alpine image as the base |
| FROM | python:3.10-slim |
Uses Python 3.10 slim image for a lightweight environment |
| FROM | nginx:alpine |
Uses the official Nginx Alpine image for serving static content |
Example: Using External Images in Multi-Stage Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
Note: The
AS builder alias allows referencing this stage in later stages using --from=builder.Warning: Ensure that the external image's architecture matches your target environment (e.g.,
linux/amd64 vs linux/arm64).Best Practices for External Images
- Use official images from Docker Hub when available.
- Pin to specific tags (e.g.,
node:18-alpine) rather than usinglatest. - Consider using multi-arch images for cross-platform compatibility.
10. Building Platform-Specific Images
| Platform | Architecture | Dockerfile Instruction | Notes |
|---|---|---|---|
| Linux/amd64 | amd64 |
--platform linux/amd64 |
Default for most Docker builds |
| Linux/arm64 | arm64 |
--platform linux/arm64 |
Used for ARM-based systems like Apple Silicon |
| Windows/amd64 | amd64 |
--platform windows/amd64 |
For Windows containers |
Note: Platform-specific builds require
--platform flag to ensure correct architecture and dependencies.Warning: Cross-platform builds may not work if base images or dependencies are not available for the target platform.
11. Creating Development vs Production Stages
| Stage | Description | Base Image | Use Case |
|---|---|---|---|
| development | Includes all dependencies for building and running the application in a development environment. | node:18-alpine |
Local development, testing, debugging |
| production | Minimal image optimized for running the application in production. | node:18-alpine |
Deployment, reduced attack surface, faster startup |
Example: Multi-stage Dockerfile
FROM node:18-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=development
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
FROM node:18-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=development /app/src ./src
EXPOSE 3000
CMD ["npm", "start"]
Note: Use
--only=production to avoid installing devDependencies in the production stage.
Key Benefits
- Smaller production image size
- Reduced security risks from dev tools
- Optimized build and runtime environments
12. Using Scratch Base Image
| Property | Value | Description |
|---|---|---|
| FROM | scratch |
Minimal base image with no OS or packages |
| Size | ~2.5MB |
Smallest possible image size |
| Use Case | Static binaries |
Best for compiled Go, Rust apps |
Example: Go App with Scratch
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
FROM scratch
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]
Note: No shell, package manager, or filesystem tools in scratch image.
Warning: Requires all dependencies to be statically linked or copied into image.