Quick Reference · container image authoring

Dockerfile cheat sheet

A Dockerfile is a sequential recipe. docker build reads it top to bottom and commits a new read-only layer for each instruction. Learn the six instruction families once and the syntax stops being a list to memorize.

base / inherit env / config filesystem execution runtime cmd metadata avoid / caution most common

Distilled & cross-checked across: docs.docker.com/reference/dockerfile · docs.docker.com/build/building/best-practices · devhints.io/dockerfile · Red Hat docker-cheatsheet-r4v2

From source code to running container — the build pipeline
BUILD CONTEXT Dockerfile source files (.dockerignore filters) .dockerignore docker build IMAGE LAYERS FROM ubuntu:24.04 RUN apt-get install … COPY . /app EXPOSE 8080 CMD ["node","app.js"] ← each line = one cached layer cache hit? reuse → faster build docker tag TAGGED IMAGE myapp :1.0 name:tag immutable digest push run REGISTRY Docker Hub / private registry CONTAINER writable layer + image layers (ro) pull another host
01Base Imagealways first
02VariablesENV vs ARG
03WORKDIR & USERcontext & security
04RUNbuild-time execution
05COPY & ADDbring files in
06EXPOSEdocument networking
07CMD & ENTRYPOINTwhat runs at start
08VOLUMEpersist data
09SHELLchange the default shell
10LABEL & HEALTHCHECKmetadata
11ONBUILD & STOPSIGNALadvanced
12Build CLIinvoke docker build

Four concepts worth visualising

Layer cache, multi-stage, CMD vs ENTRYPOINT, and COPY vs ADD — the things that trip people up most.

Layer cache — order matters

Each instruction is fingerprinted. A cache miss invalidates every layer below it. Put things that change least (dependencies) before things that change most (source code).

❌ changes bust cache early FROM node:22-alpine COPY . /app ← changes! RUN npm install ← re-runs! RUN npm run build ← re-runs! every code change = full reinstall ✓ stable layers first FROM node:22-alpine COPY package*.json /app/ RUN npm install ← cached! COPY . /app ← changes RUN npm run build npm install reuses cache ✓ Rule: stable → changing deps before source, seldom before often

Multi-stage build

Use one stage to compile/test and a lean second stage for the final image. Only the last stage ships — build tools and intermediate files stay behind.

STAGE 1: builder FROM golang:1.22 AS builder WORKDIR /src COPY . . RUN go build -o app . ~900 MB (compiler + src) not shipped COPY --from STAGE 2: final FROM alpine:3.19 WORKDIR /app COPY --from=builder /src/app /app/app EXPOSE 8080 ENTRYPOINT ["./app"] ~12 MB final image ✓ discarded at push time

CMD vs ENTRYPOINT

Think of ENTRYPOINT as the "verb" and CMD as its default "arguments". docker run img args replaces CMD, but needs --entrypoint to replace ENTRYPOINT.

Dockerfile docker run img … runs as CMD ["node","app.js"] (no ENTRYPOINT) (no args) node app.js CMD ["node","app.js"] python other.py python other.py ENTRYPOINT ["node"] CMD ["app.js"] (no args) node app.js ENTRYPOINT ["node"] CMD ["app.js"] server.js node server.js Always use exec form ["cmd"] — shell form routes SIGTERM to sh, not the process

COPY vs ADD — the decision

Use COPY for 95 % of cases. ADD's extra powers (auto-extract, remote URLs) are rarely needed and can be surprising.

Adding files to image? local .tar.gz? ADD archive.tar /dst/ auto-extracts — OK here files / dirs? COPY src /dst/ --chown= available remote URL? RUN curl -fsSL URL \ | sha256sum … pinned + verified — not ADD

Multi-stage build template

Two canonical patterns — Node.js with a separate build step, and a Python service with a cache mount.

# ── Node.js: build then ship lean ──
FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:22-alpine AS final
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=deps  /app/node_modules ./node_modules
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
# ── Python: BuildKit cache mount ──
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS base
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

FROM base AS builder
COPY requirements.txt .
# cache pip downloads across builds
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --prefix=/install -r requirements.txt

FROM base AS final
COPY --from=builder /install /usr/local
COPY src/ ./src/
RUN adduser --disabled-password appuser
USER appuser
HEALTHCHECK --interval=30s CMD \
    python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["python", "-m", "uvicorn", "src.main:app"]

Best practices

pin base tagsFROM node:22-alpine not node:latest — reproducibility
one concern per CMDone process per container; supervisor is an anti-pattern
chain RUN with &&merge update + install + clean into a single layer
multi-stage buildskeep compiler/build tools out of the shipped image
COPY deps before srcpackage.json / requirements.txt first → cache npm/pip
non-root USERcreate a dedicated user; drop root before CMD
exec form CMD/EP["cmd"] not cmd — PID 1 gets SIGTERM, graceful shutdown works
.dockerignoreexclude .git, node_modules, __pycache__, .env from context
BUILDKIT secrets--mount=type=secret for tokens — never ENV or ARG for creds
HEALTHCHECK alwaysorchestrators (K8s, Swarm) need it to route traffic correctly
minimal basealpine or distroless — smaller attack surface, faster pulls
OCI labelsorg.opencontainers.image.* — version, revision, source for auditability

.dockerignore quick ref

.git/version history — irrelevant to the build, large
node_modules/rebuilt inside the image via npm ci
__pycache__/Python bytecode — regenerated at runtime
*.env .env*local secrets — must never bake into an image
dist/ build/local build artefacts — replaced by the build stage
*.md docs/documentation — not needed at runtime
tests/ *.test.*test files — run in a stage, don't ship them
**/*.loglog files — runtime writes go to stdout anyway