Dockerfile#
A Dockerfile is a small declarative DSL describing how to build a container image. Each instruction creates a layer; the final result is an OCI-compliant image runnable by Docker, Podman, containerd, Kubernetes, or any OCI runtime.
The same file format is consumed by docker build, podman build,
Buildah, BuildKit,
Kaniko, Buildpacks, and most CI build steps.
The Anatomy of a Dockerfile#
# Comment
FROM <base-image>[:tag][@digest]
ARG <name>[=default] # build-time variable
ENV <name>=<value> # runtime environment variable
WORKDIR <path> # set working directory
COPY <src> <dst> # copy files into the image
ADD <src> <dst> # like COPY plus URL/tar handling
RUN <shell command> # run during build, layer the result
USER <user> # set the user for following steps
EXPOSE <port> # documentation only
HEALTHCHECK CMD <cmd> # health probe definition
VOLUME ["/data"] # declare a volume mount-point
ENTRYPOINT ["/app", "--flag"] # the program
CMD ["arg1"] # default arguments
Every instruction (mostly) creates a new image layer.
Layers are content-addressable and cached.
Order matters: things that change rarely should come first to maximize cache reuse.
A Real Multi-Stage Build#
# syntax=docker/dockerfile:1.7
ARG GO_VERSION=1.22
# ----- build stage -----
FROM golang:${GO_VERSION} AS build
WORKDIR /src
# Cache go modules
COPY go.mod go.sum ./
RUN go mod download
# Copy source and build
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' \
-o /out/app ./cmd/app
# ----- runtime stage -----
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]
The pattern: build with a fat image (Go toolchain, build tools); copy only the artifact into a minimal runtime image.
Best-Practice Cheat Sheet#
A condensed list of the habits that separate a workable Dockerfile from a production-ready one. Pinning, minimal bases, multi-stage builds, non-root users, layer-aware ordering, and BuildKit secrets all push the image toward smaller, safer, and reproducible.
Pin base images by digest:
FROM gcr.io/distroless/static-debian12:nonroot@sha256:abc123...
Use a minimal base,
distroless,alpine,scratch.Multi-stage so build tools don’t end up in the runtime image.
Run as non-root,
USER nonroot:nonrootor a numeric UID.One process per container; init systems are usually unnecessary.
Cache friendly, copy lockfiles first, run install, then copy source.
No secrets at build time in regular layers; use BuildKit
--mount=type=secretinstead.``.dockerignore`` to keep
node_modules,.git, build output out of the build context.
.dockerignore#
A glob-pattern file that excludes paths from the build context.
.git
node_modules
__pycache__
*.log
.env
.env.*
build
dist
target
coverage
Without it, large directories balloon build times and image sizes.
Build-Time Secrets and Caches#
BuildKit (the default builder) adds two features that change a lot of real-world Dockerfiles.
Build secrets, secrets available during a RUN step but not in the layer.
# syntax=docker/dockerfile:1.7 RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm ci # Build with the secret # docker buildx build --secret id=npmrc,src=$HOME/.npmrc .
Cache mounts, speed up package managers by persisting their cache across builds.
RUN --mount=type=cache,target=/root/.cache/pip \ pip install -r requirements.txt
ENTRYPOINT vs. CMD#
ENTRYPOINT, the program.
docker run image foo barcalls it withfoo bar.CMD, default arguments. Overridden by anything after the image name.
Idiomatic combinations.
ENTRYPOINT ["/app"]plusCMD ["--config", "/etc/app.toml"]run with default args; users can override.ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]plusCMD ["app"]a shell wrapper handles signals / setup.
Use the exec form (JSON array) so PID 1 is your binary, not a shell that doesn’t forward signals.
Common Mistakes#
The traps that bloat images, defeat caching, or create
runtime surprises. Most are one-line fixes once recognized.
combine related RUN steps, use --no-install-recommends
and clean apt caches in the same layer, prefer COPY over
ADD, and pin tags instead of relying on latest.
``apt-get install`` without ``–no-install-recommends``, bloats the image.
No ``apt-get clean`` in the same RUN, the cache becomes part of the layer.
Multiple ``RUN`` for related setup, each is a separate layer; combine where appropriate.
Using ``ADD`` for plain files,
COPYis preferred;ADDhas tar/URL magic that surprises people.Running as root, the default; almost always wrong in production.
``apt-get update`` in a separate layer, the package list gets cached; subsequent installs use stale data.
``RUN cd /tmp && …``,
cddoesn’t persist across RUN steps; useWORKDIR.``EXPOSE`` is documentation, it does not open ports;
-p/ Compose / Kubernetes does.Relying on ``latest`` in CI, non-reproducible builds.
Image Hygiene#
The supply-chain practices that turn a Dockerfile into a shippable artifact in 2026. Cosign signing with admission verification, syft- or BuildKit-generated SBOMs, Trivy / Grype scanning in CI, and SLSA-style provenance attestations all stack into something auditors and incident responders can use.
Sign images,
cosign; verify in admission controllers.Generate SBOMs,
syftor BuildKit’s--sbom.Scan,
trivy,grype,snyk; in CI on every build.Provenance attestations, BuildKit emits SLSA-style provenance.
Variants and Cousins#
The other formats and tools that build OCI images. Some are syntactic clones (Containerfile); others skip Dockerfiles entirely (Buildpacks, ko, Jib, Nix); programmable build languages (Earthly, Dagger) emit images and CI pipelines together. Dockerfiles remain the lowest common denominator.
Containerfile, same syntax; the OCI-blessed name. Podman uses it.
Buildpacks, “no Dockerfile”; detect language, build, produce image. Useful when you want consistency across many services.
Nix flakes / nix2container, reproducible images without a Dockerfile.
ko / Jib, language-native image builders for Go and Java that skip Dockerfiles entirely for typical service builds.
Earthly / Dagger, programmable build languages that emit OCI images and CI pipelines together.
Dockerfiles remain the lowest common denominator and the default for most teams.
When (Not) to Write a Dockerfile#
Use for.
Shipping internal services as containers.
Standardizing dev environments (
Dev Containers).Building consistent CI / build images.
Distributing a tool with all its dependencies.
Skip when.
A buildpack covers the use case (most web services).
Your language ecosystem has a better-than-Docker tool (ko for Go, Jib for Java).
You’re shipping a single static binary, maybe just package the binary.
See Also#
Containers, the broader runtime story.
Security, supply-chain hygiene.