Use when writing, reviewing, or optimizing Dockerfiles, docker-compose files, or container images. Trigger on mentions of Docker layer caching, multi-stage builds, image size reduction, container security hardening, BuildKit, .dockerignore, HEALTHCHECK, distroless, alpine, scratch base images, non-root containers, Docker build secrets, or CI/CD container pipelines. Do NOT use for Kubernetes manifests, Helm charts, Docker Swarm orchestration, container runtime configuration (containerd/CRI-O), or general Linux system administration.
Use multi-stage builds to separate build dependencies from the runtime image. Never ship compilers, SDKs, or package managers in production.
# syntax=docker/dockerfile:1
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app ./cmd/server
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
# syntax=docker/dockerfile:1
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
FROM deps AS build
COPY . .
RUN npm run build
FROM node:22-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S app && adduser -S app -G app -u 1001
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
# syntax=docker/dockerfile:1
FROM python:3.13-slim AS builder
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-compile -r requirements.txt
FROM python:3.13-slim
WORKDIR /app
RUN groupadd -r app && useradd -r -g app -u 1001 app
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY --chown=app:app . .
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]
Place least-changing instructions first. Docker invalidates all layers after the first changed layer.
# GOOD: dependencies cached separately from source
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# BAD: any source change invalidates npm ci cache
COPY . .
RUN npm ci
Merge related operations into one RUN to reduce layers and prevent leftover artifacts.
# GOOD: single layer, cache cleaned
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
ca-certificates && \
rm -rf /var/lib/apt/lists/*
# BAD: cleanup in separate layer doesn't reduce image size
RUN apt-get update
RUN apt-get install -y curl ca-certificates
RUN rm -rf /var/lib/apt/lists/*
Pin base images and packages for reproducible builds.
# GOOD: pinned digest
FROM node:22.12.0-alpine3.21@sha256:abc123...
# GOOD: pinned package versions
RUN apk add --no-cache curl=8.12.1-r0
# BAD: floating tags
FROM node:latest
Never run containers as root. Create a dedicated user with a fixed UID.
# Alpine
RUN addgroup -S app && adduser -S app -G app -u 1001
USER app
# Debian/Ubuntu
RUN groupadd -r app && useradd -r -g app -u 1001 -s /bin/false app
USER app
# Distroless (already non-root)
FROM gcr.io/distroless/static:nonroot
USER nonroot:nonroot
Choose the smallest base that supports your runtime:
| Base Image | Size | Shell | Package Manager | Use Case |
|---|---|---|---|---|
scratch | 0 MB | No | No | Static binaries (Go, Rust) |
distroless | ~2 MB | No | No | Compiled apps needing libc |
alpine | ~7 MB | Yes | apk | Apps needing shell/tools |
*-slim | ~80 MB | Yes | apt | Apps needing glibc |
Never use ARG or ENV for secrets. Use BuildKit secret mounts.
# syntax=docker/dockerfile:1
# Mount secret at build time — never stored in image layers
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
Build command:
docker build --secret id=npmrc,src=$HOME/.npmrc .
Apply at runtime, not in the Dockerfile:
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--read-only \
--tmpfs /tmp \
--security-opt=no-new-privileges \
myimage
Always create a .dockerignore to exclude unnecessary files from the build context.
.git
.github
.vscode
node_modules
dist
*.md
!README.md
Dockerfile*
docker-compose*
.env*
__pycache__
*.pyc
.pytest_cache
coverage
.nyc_output
tests
Omitting .dockerignore sends the entire directory (including .git) as build context, slowing builds and risking secret leakage.
Define HEALTHCHECK in every production Dockerfile. Orchestrators use it to determine container readiness.
# HTTP check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# TCP check (no curl available)
HEALTHCHECK --interval=30s --timeout=3s \
CMD ["sh", "-c", "exec 3<>/dev/tcp/localhost/8080 || exit 1"]
# For distroless (no shell) — use the app itself
HEALTHCHECK --interval=30s --timeout=3s \
CMD ["/app", "--healthcheck"]
| Anti-Pattern | Fix |
|---|---|
FROM ubuntu:latest | Pin version + digest: FROM ubuntu:24.04@sha256:abc... |
Running as root (no USER) | Create non-root user, add USER app |
COPY . . before dep install | Copy lockfile first, install deps, then COPY . . |
Secrets in ARG/ENV | Use --mount=type=secret (BuildKit) |
Separate RUN apt-get update and install | Combine in one RUN with rm -rf /var/lib/apt/lists/* |
| Installing vim, wget, net-tools in prod | Use --no-install-recommends, only install what's needed |
Missing .dockerignore | Always create one (see section above) |
ENTRYPOINT with shell form | Use exec form: ENTRYPOINT ["/app"] |
Enable BuildKit: export DOCKER_BUILDKIT=1 (default in Docker 23.0+).
Persist package manager caches across builds. Reduces rebuild time by 50–70%.
# pip
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# apt
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt/lists \
apt-get update && apt-get install -y curl
# npm
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Go modules
RUN --mount=type=cache,target=/go/pkg/mod \
go build -o /app .
# Maven
RUN --mount=type=cache,target=/root/.m2 \
mvn package -DskipTests
Inject secrets that never persist in image layers.
# Single secret
RUN --mount=type=secret,id=aws_creds,target=/root/.aws/credentials \
aws s3 cp s3://bucket/data /data
# Multiple secrets
RUN --mount=type=secret,id=gh_token \
--mount=type=secret,id=npm_token \
GH_TOKEN=$(cat /run/secrets/gh_token) \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm ci
Build with:
docker build \
--secret id=gh_token,src=./gh_token.txt \
--secret id=npm_token,src=./npm_token.txt \
.
Clone private repos without copying keys into the image.
RUN --mount=type=ssh \
git clone [email protected]:org/private-repo.git /src
Build with:
eval $(ssh-agent) && ssh-add ~/.ssh/id_ed25519
docker build --ssh default .
Inline multi-line scripts without shell gymnastics (BuildKit + dockerfile:1.4+).
# syntax=docker/dockerfile:1
RUN <<EOF
set -e
apt-get update
apt-get install -y --no-install-recommends curl
rm -rf /var/lib/apt/lists/*
EOF
COPY <<EOF /etc/app/config.yaml