← Catalog

No. 007 · devops

Docker Best Practices

Containers that build fast and run small

Version 1.0.0 License MIT Format SKILL.md

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 alpine or slim variants as base images
  • Combine RUN commands with && to reduce layers
  • Clean up package manager caches in the same RUN layer:
    RUN apk add --no-cache python3 build-base \
        && pip install -r requirements.txt \
        && apk del build-base
  • Use COPY --link for 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