Dockerfile best practices

Use multi-stage builds, .dockerignore, pinned base digests, and minimal runtime deps; set HEALTHCHECK and USER. Layer order drives cache hits and scan quality.

Put volatile layers late; split dependency installs from copying source; combine apt/apk installs with cache cleanup in one RUN; never bake secrets—mind build-arg history leakage.

Set WORKDIR, document ports, separate ENTRYPOINT vs CMD; if you need proper signal handling/PID 1, note whether the base image already ships tini.

Match scanning policy: maintained base tags, periodic rebuilds; non-root UID/GID and read-only rootfs (if orchestration requires) belong in the SKILL steps.

  • BuildKit cache mounts and --mount=type=secret for sensitive build inputs.
  • Multi-arch: cross-link buildx/platform matrices from your CI skill.
  • Same docker compose overrides locally and in CI.

Layers & cache

Place .dockerignore and “copy lockfiles/manifests only” COPY steps before frequently changing source; co-locate package installs with cache cleanup inside one RUN.

  • Pin bases: prefer digest (image@sha256:…) and document refresh process.
  • Avoid early COPY . .: huge contexts bust the cache.
  • Document ARG defaults and build-secret boundaries to avoid secrets in layer history.

Complete .dockerignore example (Node.js project):

# .dockerignore — reduce build context size, prevent cache busting
# Version control
.git
.gitignore
.gitattributes

# Dependency directory (reinstalled inside container)
node_modules
npm-debug.log*
yarn-error.log*

# Build artifacts (regenerated in multi-stage build)
dist
build
.next
out

# Tests and coverage
coverage
__tests__
*.test.ts
*.spec.ts
jest.config.*

# Documentation
*.md
docs/
LICENSE

# Local dev config
.env
.env.local
.env.*.local
docker-compose.override.yml

# CI config
.github
.gitlab-ci.yml
Jenkinsfile

# Editor
.vscode
.idea
*.swp
*.swo

Multi-stage builds

Builder stages hold toolchains, test deps, and caches; final stages COPY --from=… only artifacts and runtime libs—smaller surface and image.

  • Name stages (AS builder / AS runtime); avoid long implicit FROM chains.
  • Copy only what production needs; keep debug tools in dev compose overlays.
  • Inside builders, still order deps before source.

Complete Node.js multi-stage Dockerfile (all best practices):

# syntax=docker/dockerfile:1
# Node.js multi-stage build — all best practices

# ---------- Stage 1: deps (production deps only) ----------
FROM node:20-alpine@sha256:a7394f8b2e2a4e4aef9f6082c84f6d7e3b1d2b1 AS deps
WORKDIR /app

# Copy lockfiles first to leverage layer cache
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --prefer-offline \
    && npm cache clean --force

# ---------- Stage 2: builder (build artifacts) ----------
FROM node:20-alpine@sha256:a7394f8b2e2a4e4aef9f6082c84f6d7e3b1d2b1 AS builder
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --prefer-offline         # includes devDependencies for build

COPY . .                             # source last to avoid busting dep cache
RUN npm run build \
    && npm prune --production        # remove devDependencies

# ---------- Stage 3: runtime (minimal image) ----------
FROM node:20-alpine@sha256:a7394f8b2e2a4e4aef9f6082c84f6d7e3b1d2b1 AS runtime

# Security: create non-root user
RUN addgroup --system --gid 1001 nodejs \
    && adduser --system --uid 1001 --ingroup nodejs appuser

WORKDIR /app

# Copy only artifacts and deps from builder stage
COPY --from=builder --chown=appuser:nodejs /app/dist ./dist
COPY --from=builder --chown=appuser:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:nodejs /app/package.json ./

ENV NODE_ENV=production \
    PORT=3000

USER appuser

EXPOSE 3000

# HEALTHCHECK: aligned with business readiness semantics
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"

# ENTRYPOINT handles signals; CMD provides overridable defaults
ENTRYPOINT ["node"]
CMD ["dist/server.js"]

Complete Python multi-stage Dockerfile (FastAPI example):

# syntax=docker/dockerfile:1
# Python multi-stage build — FastAPI / generic Python service

# ---------- Stage 1: builder ----------
FROM python:3.12-slim@sha256:abc123 AS builder

RUN pip install --upgrade pip \
    && pip install build wheel

WORKDIR /app
COPY pyproject.toml requirements.txt ./

# BuildKit secret mount: private registry token never enters layer history
RUN --mount=type=secret,id=pip_token \
    pip install --no-cache-dir \
      --extra-index-url "https://$(cat /run/secrets/pip_token)@private.pypi.example.com/simple/" \
      -r requirements.txt \
      --target /install

# ---------- Stage 2: runtime ----------
FROM python:3.12-slim@sha256:abc123 AS runtime

RUN groupadd --gid 1001 appgroup \
    && useradd --uid 1001 --gid appgroup --no-create-home appuser

WORKDIR /app

# Copy only installed packages and application code
COPY --from=builder /install /usr/local/lib/python3.12/site-packages/
COPY --chown=appuser:appgroup . .

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PORT=8000

USER appuser
EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

ENTRYPOINT ["uvicorn", "app.main:app"]
CMD ["--host", "0.0.0.0", "--port", "8000"]

Runtime, security, signals

Use non-privileged USER and explicit WORKDIR; HEALTHCHECK should reflect real readiness—not merely “process exists.”

  • ENTRYPOINT owns long-running behavior and signals; CMD supplies defaults overridable by compose/k8s.
  • For Java/Node SIGTERM handling, document tini or equivalent.
  • EXPOSE documents intent; real ports come from orchestration.

Image pipeline

  [ Context: .dockerignore + lockfile strategy ]
        │
        ▼
  [ Dockerfile: layered (deps → source) or multi-stage FROM ]
        │
        ▼
  ┌─────────────┐     BuildKit cache / secret mounts; platform matrix (optional)
  │ docker build │──── Tags with digest, SBOM (per policy)
  └─────────────┘
        │
        ▼
  ┌─────────────┐     Image scan, license & baseline policy
  │  Scanning   │──── Block highs or ticket exceptions
  └─────────────┘
        │
        ▼
  [ Push registry → orchestration pins same digest / immutable tag ]

When agents author Dockerfiles, include checklist items for cache-busting COPY order and “final stage only has runtime files” for review and reproducible builds.

Trivy security scan commands and Docker Compose dev environment configuration:

# trivy image scan: check CVE vulnerabilities and secrets leakage
trivy image --exit-code 1 \
  --severity HIGH,CRITICAL \
  --ignore-unfixed \
  myapp:1.2.3-abc1234

# Scan and generate SARIF report (upload to GitHub Security)
trivy image \
  --format sarif \
  --output trivy-results.sarif \
  myapp:1.2.3-abc1234

# GitHub Actions integration example
# - name: Scan image with Trivy
#   uses: aquasecurity/trivy-action@master
#   with:
#     image-ref: myapp:${{ steps.tag.outputs.tag }}
#     format: 'sarif'
#     output: 'trivy-results.sarif'
#     severity: 'HIGH,CRITICAL'
#     exit-code: '1'
#     ignore-unfixed: true

Docker Compose dev environment configuration (compose.yml + compose.override.yml):

# compose.yml — base definition (usable in CI/CD)
services:
  app:
    build:
      context: .
      target: runtime        # specify multi-stage target
    image: myapp:dev
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
      DATABASE_URL: postgres://user:pass@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  db-data:

# compose.override.yml — local dev overrides (not committed to git)
# services:
#   app:
#     build:
#       target: builder     # dev stage includes toolchain
#     volumes:
#       - .:/app            # mount source for hot reload
#       - /app/node_modules # don't override container's node_modules
#     command: npm run dev

Layer order sandbox

Pick a stack and options to emit a suggested instruction order for this page (illustrative—tune per language/base). Use for SKILL self-checks or review prep.

Dependency stack (one)
Options

Suggested layer order (top → bottom)


              

This sandbox does not emit a full Dockerfile; merge with your base image, CI env vars, and HEALTHCHECK. Secrets only via mounts or runtime injection—never default ARG values.

SKILL snippet

---
name: dockerfile-best-practices
description: Multi-stage, non-root, scannable container image best practices
tags: [docker, container, security, devops]
---
# Base image & context
1. Pin base image to full digest (image@sha256:...) and document refresh process
2. .dockerignore excludes .git, node_modules, .env, test files, docs
3. Base image selection: slim/alpine/distroless — choose minimal attack surface per use case

# Layer cache strategy
4. COPY lockfiles first (package-lock.json / go.sum / requirements.txt) then install deps
5. COPY source code after: prevents source changes from busting dep cache layers
6. apt/apk installs in single RUN merged with cache cleanup (&& rm -rf /var/cache/apk/*)
7. BuildKit secret mount (--mount=type=secret): private registry tokens never enter layer history

# Multi-stage builds
8. builder stage contains toolchain & devDependencies; runtime stage only COPYs artifacts
9. Clear stage naming: AS deps / AS builder / AS runtime
10. Intermediate stages can be targeted independently (docker build --target builder)

# Runtime security
11. Final stage creates and switches to non-root user (adduser + USER)
12. HEALTHCHECK aligned with business readiness semantics (not just checking process existence)
13. ENTRYPOINT handles signals; CMD provides overridable default args
14. EXPOSE is documentation only — real ports declared by orchestration (k8s/compose)

# Security scanning
15. trivy image scans HIGH/CRITICAL CVEs, --exit-code 1 blocks CI
16. Rebuild regularly (weekly) to pick up base image security updates
17. Multi-arch: docker buildx --platform linux/amd64,linux/arm64

All skills More skills