Dockerfile
Dockerfile: The Definitive Guide to Container Image Construction
A Dockerfile is a plain-text configuration script that encodes every instruction required to build a reproducible container image. It is the single most important artifact in a Docker-based workflow: one file separates a fragile, environment-dependent application from a portable, deterministic deployment unit. This guide examines the Dockerfile at depth — instruction semantics, optimization heuristics, multi-stage architecture, and comparative baseline analysis — so that engineers at every level can write Dockerfiles that are lean, secure, and production-grade.
Key Takeaways
- A Dockerfile is declarative build documentation. Each instruction becomes a read-only layer in the final image; order and granularity determine cache behaviour, image size, and rebuild speed.
- Multi-stage builds are essential for production. They separate compilation artefacts from runtime artefacts, routinely cutting final image sizes by 60–90 % compared with single-stage approaches.
- Layer caching is the primary optimisation lever. Instructions that change infrequently (
COPYdependency manifests,RUN apt-get) should precede those that change often (COPY .application source) to maximise build-cache hits. - Base-image choice dictates the attack surface. Alpine Linux images average 5 MB vs. Debian's 120 MB, but the trade-off is musl vs. glibc compatibility — not all packages compile cleanly against musl.
- Non-root execution and .dockerignore are non-negotiable for security. Running as UID 1000+ and excluding node_modules, .git, and secrets from the build context are baseline requirements in any hardened pipeline.
What Is a Dockerfile and Why Does It Matter?
At its core, a Dockerfile is a step-by-step recipe that the docker build engine translates into a series of overlay filesystem layers. Each line starting with a known instruction keyword (FROM, RUN, COPY, CMD, etc.) produces exactly one layer — or, in the case of RUN and ADD, can be chained to accumulate changes before committing a new diff on top of the previous image.
The importance of the Dockerfile cannot be overstated for three reasons:
- Reproducibility. Two teams building from the same Dockerfile on different machines produce byte-identical images (assuming identical base-tag versions), eliminating the classic "it works on my machine" failure mode.
- Immutability. Once built, the image is a static artifact. The Dockerfile is the only source of truth; there is no mutable runtime state to drift.
- Automation. CI/CD systems parse, validate, and trigger builds from a single file. The Dockerfile is the contract between development and deployment.
From Source Text to Layers: The Build Pipeline
The Docker build engine follows a deterministic sequence when it encounters a Dockerfile:
- Parsing — the engine tokenises each line, resolving continuation characters (
\), environment variable substitutions (${VAR}), and instruction arity. - Cache resolution — for each instruction, the engine computes a cache key from (a) the instruction text, (b) the checksum of any files referenced by
COPYorADD, and (c) the ID of the parent layer. If a matching layer exists, the build is skipped and the cached layer is reused. - Layer execution — uncached instructions run in an ephemeral container spun from the parent layer. The resulting filesystem delta is committed as a new read-only layer.
- Final commit — after the last instruction, the engine writes the complete layer stack, along with metadata (entrypoint, exposed ports, labels), to the target image registry.
Understanding this pipeline is critical: a single misplaced COPY . . instruction near the top of the file can invalidate every subsequent cache entry, turning a 10-second build into a 10-minute full rebuild.
Dockerfile Instruction Taxonomy
Essential Instructions
| Instruction | Purpose | Cache Behaviour | Best-Practice Note |
|---|---|---|---|
FROM |
Sets the base image for subsequent instructions. | Always a cache miss when the tag or digest changes. | Pin to a digest (@sha256:…) in production for immutability. |
RUN |
Executes a command and commits the resulting filesystem change. | Invalidated when prior-layer content or the command text changes. | Chain related commands with && and clean up in the same RUN to limit layer count. |
COPY |
Copies files from the build context into the image. | Invalidated by any change to source file contents or path. | Prefer COPY over ADD; ADD has implicit archive-extract and URL-fetch behaviour that is hard to reason about. |
ENTRYPOINT |
Sets the executable that runs when the container starts. | Treated as immutable metadata; does not affect layer cache. | Use JSON array form (["/sbin/init"]) to avoid shell-wrapping surprises. |
CMD |
Provides default arguments to ENTRYPOINT or runs a command if no entrypoint is set. |
Metadata only; no cache impact. | Only one CMD is honoured; later declarations silently overwrite earlier ones. |