tools/docker/dockerfile_best_practices.md

Dockerfile Best Practices: Instruction-Level Authoring Guide

Non-default BuildKit build-security flags: secret mounts, SBOM/provenance attestations, distroless size/CVE numbers, and named Docker Hub supply-chain incidents (Leaky Vessels CVEs, JFrog imageless-repo campaign).

Companion to Docker Best Practices (registry/CI-level policy). This document covers the non-default, easy-to-miss flags and documented incidents that determine whether a build leaks secrets, ships an unverifiable artifact, or is itself an attack surface. Assumes # syntax=docker/dockerfile:1.7 and BuildKit (default since Docker Engine 23.0).

Secret Handling: Exact Flag Reference

MechanismPersists in image/history?Verdict
ARG / ENV with the secret valueYes - recoverable forever via docker history / docker image inspect, even after a later RUN rm (each instruction is a separate immutable layer diff)Never
COPY a secret file then RUN rm itYes - the file exists in an earlier layer regardless of later deletionNever
RUN --mount=type=secretNo - tmpfs-mounted at /run/secrets/<id> only for the lifetime of that one RUN instructionUse this
# syntax=docker/dockerfile:1.7
RUN --mount=type=secret,id=pip_index_token,target=/run/secrets/token,uid=1000 \
    PIP_INDEX_URL="https://user:$(cat /run/secrets/token)@pypi.internal/simple" \
    pip install --no-cache-dir internal-package
docker build --secret id=pip_index_token,src=./token.txt .
docker build --secret id=pip_index_token,env=PIP_INDEX_TOKEN .

Mount params: id (required), target/dst (default /run/secrets/<id>), env (populate from a build-time env var instead of a file), uid/gid (ownership on the mounted file). Source: Docker Build docs - Build secrets, moby/buildkit#2122.

2017 baseline for why this matters: 17 backdoored Docker Hub images shipped a cryptominer that generated roughly $90,000 before removal - a reminder that build-time credential hygiene and base-image provenance are the same threat model, not two separate concerns.

Build-Time Container Breakout: "Leaky Vessels" (Jan 2024)

Four CVEs in runc/BuildKit/Moby, CVSS 8.6-10, that allowed a malicious Dockerfile/base image to escape the build container and touch the host during a normal docker build, not just at runtime:

CVEComponentEffectFixed in
CVE-2024-21626runcFile-descriptor leak → host filesystem accessrunc 1.1.12
CVE-2024-23651BuildKitRace condition in cache-mount handling → container breakoutBuildKit 0.12.5
CVE-2024-23652BuildKitArbitrary file deletion on host during buildBuildKit 0.12.5
CVE-2024-23653BuildKitBreakout via the BuildKit GRPC SecurityMode checkBuildKit 0.12.5

Docker Engine 25.0.2 / Docker Desktop 4.27.1 bundle all four fixes. Decision rule: pin your CI build-agent's Docker/BuildKit version and check it against this table before trusting RUN --mount=type=cache or RUN --mount=type=secret with untrusted base images or Dockerfiles from forks/PRs. Sources: Wiz - Leaky Vessels, Snyk/Qualys advisory summary.

SBOM and Provenance Attestation (non-default)

Neither is generated by a plain docker build; both require buildx and are only attached to registry-pushed images by default (a minimal provenance attestation is auto-added on push; SBOM is not).

docker buildx build --sbom=true --provenance=mode=max -t myregistry/myapp:1.4.0 --push .
docker buildx imagetools inspect myregistry/myapp:1.4.0 --format '{{ json .SBOM }}'
FlagValuesEffect
--provenancefalse \true/mode=min (default when pushing) \mode=maxmax records full build definition (Dockerfile contents, build args, source), min records only builder identity + timestamp
--sbomfalse \trueRuns a package-scan of the final layer and attaches a SPDX-format SBOM as a separate attestation manifest

Attestations are stored as extra manifests in the image index, not inside the image layers - inspect them with docker buildx imagetools inspect, gate on them with a policy engine (Docker Scout, in-toto/cosign verify) before deploy. Source: Docker Docs - Build attestations, Build attestations: SBOM.

Distroless / Minimal Base Images: Real Numbers

BaseReported size (comparable helloworld/app image)Typical CVE countPatch cadence
Debian-based official image (e.g. python:3.13)Several hundred MB~280 known CVEs on average (Docker Hub base-image survey)Debian release cycle
*-alpine~25.7 MBFew to none (musl/apk has a much smaller package surface); some tags still carry a handful of low-severity CVEsapk security cycle
gcr.io/distroless/*~19.9 MB comparable buildNear-zero (no shell, no package manager, no libc extras beyond runtime deps)Tied to upstream rebuild cadence
Chainguard / Wolfi-based images~15-20 MBAt or near zero CVEs, rebuilt nightlyHours after upstream CVE disclosure, not a distro release cycle

Decision rule: distroless/Chainguard for anything internet-facing or in the default deploy path; keep an explicit :debug variant (Chainguard ships one per image) or a sidecar/ephemeral-debug-container for kubectl debug-style troubleshooting, since distroless has no shell by construction. Do not choose distroless for images that need runtime package installation or shell-based entrypoint scripting - that forces a shell back in, defeating the point. Sources: Chainguard - best base image comparisons, iximiuz - In Pursuit of Better Container Images.

Docker Hub Supply-Chain Incidents (know these before trusting an unpinned public image)

  • JFrog, 2024: 4.6M "imageless" Docker Hub repositories identified (30% of all public repos analyzed); 2.81M of those tied to three large-scale malware/phishing landing-page campaigns abusing Docker Hub's namespace as free web hosting. JFrog blog, The Hacker News coverage.
  • Cryptomining campaigns: images disguised as ubuntu or MySQL utility images, secretly running Monero miners, pulled millions of times over multi-year campaigns before takedown (Sysdig research).
  • KICS (2026): a legitimate, 5M+-pull security-scanning image had credential-stealing malware injected into a compromised release - a reminder that even a well-known, actively maintained security tool's image is a viable supply-chain target, not just abandoned/typosquatted ones. The Hacker News.

Decision rule: pin digests (FROM image@sha256:...) for any image not published by Docker Official Images/Verified Publishers; run docker scout cves or trivy image in CI as a merge gate, not an advisory-only step; never resolve an unpinned :latest tag from an unofficial namespace in a production build.

Instruction-Ordering and Layer-Cache Fundamentals

These are default-knowledge for any working engineer and are covered exhaustively in general Docker documentation - kept here only as a terse checklist, not re-derived:

  • .dockerignore before writing the Dockerfile; exclude .git, node_modules, .env*, test/doc trees.
  • Order instructions least-to-most frequently changing: OS packages → dependency manifests → application source.
  • ARG for build-time-only values (recorded in build history - never put secrets here); ENV only for values the running process actually needs.
  • Prefer COPY over ADD (the latter's implicit remote-fetch/auto-extract behavior is a surprise vector); use COPY --link for layer-cache independence in multi-stage builds.
  • RUN --mount=type=cache,target=... for package-manager caches (pip/npm/go-build) - persists across builds without touching the image layer, commonly 50-80% faster incremental installs.
  • Name every stage (AS builder, AS test, AS runtime); use --target to build a debug/test variant from the same Dockerfile without shipping its extra dependencies.
  • WORKDIR, never RUN cd (the latter does not persist across instructions).
  • Explicit non-root USER - see Docker Best Practices §4.

Testing & CI Gates

  • hadolint in CI on every Dockerfile change (catches missing pins, ADD misuse, missing --no-install-recommends).
  • Build the image in CI on every PR; track duration over time - a sudden slowdown usually means a cache-invalidation regression from reordered instructions.
  • container-structure-test against the final stage to assert expected files/users/permissions.
  • Gate merges on docker scout cves --exit-code or trivy image --exit-code 1 --severity CRITICAL,HIGH, not a report-only scan.
  • Verify multi-arch builds (--platform linux/amd64,linux/arm64) explicitly rather than assuming parity.

When Not to Hand-Write a Dockerfile

DimensionHand-Written DockerfileCloud-Native Buildpacks (Paketo)Nix-based Image Builds
Layer controlFullBuildpack-managedFull, declarative
ReproducibilityRequires discipline (digest pinning)GoodExcellent (content-addressed)
Best fitFull control neededStandard language stacks, zero Dockerfile maintenanceTeams already on Nix, maximal reproducibility requirement

Sources

Glossary

  • Secret mount: RUN --mount=type=secret - a BuildKit tmpfs mount available only for the duration of one RUN instruction, never persisted to a layer.
  • Provenance attestation: A signed record of how an image was built (source, build args, builder identity), attached as a separate manifest, inspectable via docker buildx imagetools inspect.
  • SBOM: Software Bill of Materials - an enumerated list of packages/versions in the final image, attached as an attestation manifest.
  • Distroless: A base image containing only the application and its runtime dependencies - no shell, no package manager.