Multi-Stage Build Patterns
Advanced multi-stage Dockerfile patterns beyond the basics in SKILL.md.
BuildKit Cache Mounts
BuildKit (Docker 18.09+) supports cache mounts that persist between builds — dramatically faster for package managers.
# syntax=docker/dockerfile:1
# Node.js with persistent npm cache
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build
# Python with persistent pip cache
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .
# Go with module cache
FROM golang:1.22-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -o /server ./cmd/server
Anti-Pattern: Not using BuildKit cache mounts. Without them, every npm ci downloads all packages from scratch. With them, only changed packages are re-downloaded.
Cross-Compilation
Go Cross-Compile in Docker
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build
ARG TARGETOS TARGETARCH
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -ldflags="-s -w" -o /server ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=build /server /server
ENTRYPOINT ["/server"]
Build for multiple platforms:
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .
Rust Cross-Compile
FROM --platform=$BUILDPLATFORM rust:1.76 AS build
ARG TARGETPLATFORM
RUN case "$TARGETPLATFORM" in \
"linux/amd64") echo "x86_64-unknown-linux-musl" > /target ;; \
"linux/arm64") echo "aarch64-unknown-linux-musl" > /target ;; \
esac
RUN rustup target add $(cat /target)
WORKDIR /app
COPY . .
RUN cargo build --release --target $(cat /target)
RUN cp target/$(cat /target)/release/myapp /myapp
FROM scratch
COPY --from=build /myapp /myapp
ENTRYPOINT ["/myapp"]
Monorepo Patterns
Shared Dependencies, Per-Service Builds
# Stage 1: Workspace root dependencies
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/shared/package.json ./packages/shared/
COPY services/api/package.json ./services/api/
COPY services/worker/package.json ./services/worker/
RUN corepack enable && pnpm install --frozen-lockfile
# Stage 2: Build shared library
FROM base AS shared-build
COPY packages/shared/ ./packages/shared/
RUN pnpm --filter shared build
# Stage 3: Build API service
FROM shared-build AS api-build
COPY services/api/ ./services/api/
RUN pnpm --filter api build
# Stage 4: Production API image
FROM node:22-alpine AS api
WORKDIR /app
ENV NODE_ENV=production
COPY --from=api-build /app/services/api/dist ./dist
COPY --from=api-build /app/node_modules ./node_modules
RUN adduser -S appuser && chown -R appuser /app
USER appuser
CMD ["node", "dist/index.js"]
Turborepo Docker Integration
FROM node:22-alpine AS base
RUN corepack enable
# Prune monorepo to only include the target package and its dependencies
FROM base AS pruner
WORKDIR /app
COPY . .
RUN npx turbo prune api --docker
# Install dependencies for pruned subset
FROM base AS installer
WORKDIR /app
COPY --from=pruner /app/out/json/ .
RUN pnpm install --frozen-lockfile
# Build with full source
COPY --from=pruner /app/out/full/ .
RUN npx turbo build --filter=api
# Production
FROM node:22-alpine
WORKDIR /app
COPY --from=installer /app/services/api/dist ./dist
COPY --from=installer /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
Bun and Deno Patterns
Bun
FROM oven/bun:1.1 AS build
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
COPY . .
RUN bun build ./src/index.ts --target=bun --outdir=./dist
FROM oven/bun:1.1-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER bun
EXPOSE 3000
CMD ["bun", "run", "dist/index.js"]
Deno
FROM denoland/deno:2.0 AS build
WORKDIR /app
COPY . .
RUN deno compile --allow-net --allow-read --output=server src/main.ts
FROM gcr.io/distroless/cc-debian12
COPY --from=build /app/server /server
EXPOSE 8000
ENTRYPOINT ["/server"]
Development Stage Pattern
Include a development target in your multi-stage build:
# Shared base
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
# Development: all deps + dev tools
FROM base AS development
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]
# Production deps only
FROM base AS deps
RUN npm ci --only=production
# Build
FROM base AS build
RUN npm ci
COPY . .
RUN npm run build
# Production
FROM node:22-alpine AS production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]
Use target in docker-compose to select the stage:
services:
app:
build:
context: .
target: development # Use dev stage
volumes:
- .:/app # Hot reload
Secret Handling in Builds
# syntax=docker/dockerfile:1
# Mount secrets during build (never stored in image layers)
FROM node:22-alpine AS build
WORKDIR /app
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm install --registry https://npm.company.com/
# Build with secret:
# docker build --secret id=npm_token,src=.npmrc .
Anti-Pattern: COPY .npmrc . or ENV NPM_TOKEN=xxx — these bake secrets into image layers that can be extracted with docker history.
Static Analysis Images
Hadolint (Dockerfile Linting)
# Lint your Dockerfile
docker run --rm -i hadolint/hadolint < Dockerfile
# With custom config
docker run --rm -i -v $(pwd)/.hadolint.yaml:/.config/hadolint.yaml \
hadolint/hadolint < Dockerfile
Dive (Image Size Analysis)
# Analyze layers and wasted space
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive myimage:latest