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=secretfor 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
ARGdefaults 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 implicitFROMchains. - 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.”
ENTRYPOINTowns long-running behavior and signals;CMDsupplies defaults overridable by compose/k8s.- For Java/Node SIGTERM handling, document
tinior equivalent. EXPOSEdocuments 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.
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