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
| Mechanism | Persists in image/history? | Verdict |
|---|---|---|
ARG / ENV with the secret value | Yes - 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 it | Yes - the file exists in an earlier layer regardless of later deletion | Never |
RUN --mount=type=secret | No - tmpfs-mounted at /run/secrets/<id> only for the lifetime of that one RUN instruction | Use 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:
| CVE | Component | Effect | Fixed in |
|---|---|---|---|
| CVE-2024-21626 | runc | File-descriptor leak → host filesystem access | runc 1.1.12 |
| CVE-2024-23651 | BuildKit | Race condition in cache-mount handling → container breakout | BuildKit 0.12.5 |
| CVE-2024-23652 | BuildKit | Arbitrary file deletion on host during build | BuildKit 0.12.5 |
| CVE-2024-23653 | BuildKit | Breakout via the BuildKit GRPC SecurityMode check | BuildKit 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 }}'
| Flag | Values | Effect | ||
|---|---|---|---|---|
--provenance | false \ | true/mode=min (default when pushing) \ | mode=max | max records full build definition (Dockerfile contents, build args, source), min records only builder identity + timestamp |
--sbom | false \ | true | Runs 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
| Base | Reported size (comparable helloworld/app image) | Typical CVE count | Patch 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 MB | Few to none (musl/apk has a much smaller package surface); some tags still carry a handful of low-severity CVEs | apk security cycle |
gcr.io/distroless/* | ~19.9 MB comparable build | Near-zero (no shell, no package manager, no libc extras beyond runtime deps) | Tied to upstream rebuild cadence |
| Chainguard / Wolfi-based images | ~15-20 MB | At or near zero CVEs, rebuilt nightly | Hours 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
ubuntuor 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:
.dockerignorebefore writing the Dockerfile; exclude.git,node_modules,.env*, test/doc trees.- Order instructions least-to-most frequently changing: OS packages → dependency manifests → application source.
ARGfor build-time-only values (recorded in build history - never put secrets here);ENVonly for values the running process actually needs.- Prefer
COPYoverADD(the latter's implicit remote-fetch/auto-extract behavior is a surprise vector); useCOPY --linkfor 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--targetto build a debug/test variant from the same Dockerfile without shipping its extra dependencies. WORKDIR, neverRUN cd(the latter does not persist across instructions).- Explicit non-root
USER- see Docker Best Practices §4.
Testing & CI Gates
hadolintin CI on every Dockerfile change (catches missing pins,ADDmisuse, 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-testagainst the final stage to assert expected files/users/permissions.- Gate merges on
docker scout cves --exit-codeortrivy 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
| Dimension | Hand-Written Dockerfile | Cloud-Native Buildpacks (Paketo) | Nix-based Image Builds |
|---|---|---|---|
| Layer control | Full | Buildpack-managed | Full, declarative |
| Reproducibility | Requires discipline (digest pinning) | Good | Excellent (content-addressed) |
| Best fit | Full control needed | Standard language stacks, zero Dockerfile maintenance | Teams already on Nix, maximal reproducibility requirement |
Related Documentation
- Docker Best Practices
- CI/CD Patterns
- Autonomous, Least-Privilege, Portable Execution Environments
- Secure Coding Practices
- OWASP Top 10
- Git Best Practices
Sources
- Docker Docs - Build secrets
- moby/buildkit#2122 - secret env mount
- Docker Docs - Build attestations
- Docker Docs - SBOM attestations
- Wiz - Leaky Vessels: runC and BuildKit container escape vulnerabilities
- Qualys ThreatPROTECT - Leaky Vessels advisory summary
- Chainguard - Best Python Docker image comparison
- iximiuz - In Pursuit of Better Container Images
- JFrog - Attacks on Docker with millions of malicious repositories
- The Hacker News - Millions of malicious imageless containers on Docker Hub
- The Hacker News - Malicious KICS Docker images and VS Code extensions
- Sysdig - Analysis of supply chain attacks through public Docker images
Glossary
- Secret mount:
RUN --mount=type=secret- a BuildKit tmpfs mount available only for the duration of oneRUNinstruction, 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.