Most Dockerfiles are bloated because they include build tools, dependencies, and source files that the running container doesn’t need. A multi-stage build can cut image size by 80% or more.
Multi-stage builds
Separate the build stage from the runtime stage:
# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Stage 2: Runtime
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
The builder stage has devDependencies, compilers, and source code. The runner stage has only what’s needed to execute.
Layer caching
Order COPY instructions from least to most frequently changing:
# Good — dependencies layer is cached until package.json changes
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
COPY . .
# Bad — any source change invalidates the dependency cache
COPY . .
RUN pnpm install
Use .dockerignore to exclude node_modules, .git, dist, and
other files that shouldn’t be in the build context:
node_modules
.git
dist
*.md
.env*
Security
Don’t run as root:
RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser
USER appuser
Pin base image versions:
# Bad — can change without notice
FROM node:latest
# Good — deterministic, auditable
FROM node:22.12-alpine
Scan for vulnerabilities:
docker scout cves my-image:latest
Size optimization
- Use
alpineorslimvariants as base images - Combine
RUNcommands with&&to reduce layers - Clean up package manager caches in the same
RUNlayer:RUN apk add --no-cache python3 build-base \ && pip install -r requirements.txt \ && apk del build-base - Use
COPY --linkfor better cache behavior in Docker 23+
Health checks
Always include a health check:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
See references/multi-stage-examples.md for language-specific examples.
When it triggers
- writing a Dockerfile
- Docker image is too large
- Docker build is slow
- container security scanning fails