--- title: "Dockerfile Best Practices: Instruction-Level Authoring Guide" description: "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)." language: null framework: null category: devops_general tags: - docker - dockerfile - buildkit - containers - best-practices - performance - caching - supply-chain-security - sbom - attestation keywords: - buildkit run mount type=secret exact syntax - docker buildx sbom provenance attestation flags - leaky vessels CVE-2024-21626 CVE-2024-23651 CVE-2024-23652 CVE-2024-23653 - distroless vs alpine CVE count size comparison - docker hub malicious images supply chain incident last_updated: "2026-07-08" difficulty: intermediate version: "Dockerfile syntax 1.7+, BuildKit >= 0.17, Docker Engine >= 25.0.2 (Leaky Vessels fix)" related: - ../../core/devops/docker_best_practices.md - ../../core/principles/solid.md - ../../languages/python/frameworks/fastapi/best_practices.md - ../../core/devops/ci_cd_patterns.md - ../../core/devops/autonomous_least_privilege_environments.md - ../../core/security/secure_coding.md - ../../core/security/owasp_top_10.md - ../git/best_practices.md - ../../core/architecture/microservices.md - ../../core/testing/integration_testing.md search_priority: high status: published --- # Dockerfile Best Practices: Instruction-Level Authoring Guide Companion to [Docker Best Practices](../../core/devops/docker_best_practices.md) (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/` only for the lifetime of that one `RUN` instruction | Use this | ```dockerfile # 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 ``` ```bash 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/`), `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](https://docs.docker.com/build/building/secrets/), [moby/buildkit#2122](https://github.com/moby/buildkit/issues/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](https://www.wiz.io/blog/leaky-vessels-container-escape-vulnerabilities), [Snyk/Qualys advisory summary](https://threatprotect.qualys.com/2024/02/02/docker-patches-multiple-vulnerabilities-impacting-runc-buildkit-and-moby-leaky-vessels/). ## 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). ```bash 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](https://docs.docker.com/build/metadata/attestations/), [Build attestations: SBOM](https://docs.docker.com/build/metadata/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](https://www.chainguard.dev/supply-chain-security-101/best-python-docker-image-top-options-compared), [iximiuz - In Pursuit of Better Container Images](https://iximiuz.com/en/posts/containers-making-images-better/). ## 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](https://jfrog.com/blog/attacks-on-docker-with-millions-of-malicious-repositories-spread-malware-and-phishing-scams/), [The Hacker News coverage](https://thehackernews.com/2024/04/millions-of-malicious-imageless.html). - **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](https://thehackernews.com/2026/04/malicious-kics-docker-images-and-vs.html). **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](../../core/devops/docker_best_practices.md#4-run-as-a-non-root-user). ## 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 | 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](../../core/devops/docker_best_practices.md) - [CI/CD Patterns](../../core/devops/ci_cd_patterns.md) - [Autonomous, Least-Privilege, Portable Execution Environments](../../core/devops/autonomous_least_privilege_environments.md) - [Secure Coding Practices](../../core/security/secure_coding.md) - [OWASP Top 10](../../core/security/owasp_top_10.md) - [Git Best Practices](../git/best_practices.md) ## Sources - [Docker Docs - Build secrets](https://docs.docker.com/build/building/secrets/) - [moby/buildkit#2122 - secret env mount](https://github.com/moby/buildkit/issues/2122) - [Docker Docs - Build attestations](https://docs.docker.com/build/metadata/attestations/) - [Docker Docs - SBOM attestations](https://docs.docker.com/build/metadata/attestations/sbom/) - [Wiz - Leaky Vessels: runC and BuildKit container escape vulnerabilities](https://www.wiz.io/blog/leaky-vessels-container-escape-vulnerabilities) - [Qualys ThreatPROTECT - Leaky Vessels advisory summary](https://threatprotect.qualys.com/2024/02/02/docker-patches-multiple-vulnerabilities-impacting-runc-buildkit-and-moby-leaky-vessels/) - [Chainguard - Best Python Docker image comparison](https://www.chainguard.dev/supply-chain-security-101/best-python-docker-image-top-options-compared) - [iximiuz - In Pursuit of Better Container Images](https://iximiuz.com/en/posts/containers-making-images-better/) - [JFrog - Attacks on Docker with millions of malicious repositories](https://jfrog.com/blog/attacks-on-docker-with-millions-of-malicious-repositories-spread-malware-and-phishing-scams/) - [The Hacker News - Millions of malicious imageless containers on Docker Hub](https://thehackernews.com/2024/04/millions-of-malicious-imageless.html) - [The Hacker News - Malicious KICS Docker images and VS Code extensions](https://thehackernews.com/2026/04/malicious-kics-docker-images-and-vs.html) - [Sysdig - Analysis of supply chain attacks through public Docker images](https://www.sysdig.com/blog/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 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.