A Dockerfile is a plain text file containing ordered instructions that define how a container image is built: which base image to start from, which files to copy, which commands to run and how the resulting container should start. In containers and cloud-native delivery, the Dockerfile is the reproducible recipe behind an image, and it is usually stored in version control next to the application code.
The build engine, today usually BuildKit, reads the Dockerfile and produces an image made of stacked file system layers plus metadata. The same format is accepted by other OCI-compatible builders such as Buildah and Kaniko, so a Dockerfile is not tied to a single tool.
How a Dockerfile builds an image
The builder processes instructions from top to bottom. Instructions that change the file system, mainly RUN, COPY and ADD, each create a new layer that records only the differences from the layer below. Other instructions, such as ENV, EXPOSE and CMD, change only the image configuration.
Every layer is identified by a content hash and cached. On a rebuild, the builder reuses a cached layer as long as the instruction and its inputs are unchanged; the first changed instruction invalidates the cache for itself and every instruction after it. This rule is the basis of almost all Dockerfile optimization.
The build context is the set of files sent to the builder, usually the project directory. A .dockerignore file excludes items such as .git, local dependencies and secrets, which keeps the context small and prevents accidental inclusion of sensitive files in the image.
Core Dockerfile instructions
A small set of instructions covers most real Dockerfiles. Knowing exactly what each one does avoids common confusion, for example between CMD and ENTRYPOINT or between COPY and ADD.
| Instruction | Purpose | Creates a layer |
|---|---|---|
| FROM | Sets the base image and starts a build stage | Starts from base layers |
| RUN | Executes a command during the build | Yes |
| COPY | Copies files from the build context or another stage | Yes |
| ADD | Like COPY, also extracts local archives and fetches URLs | Yes |
| WORKDIR | Sets the working directory | Metadata, creates the directory if needed |
| ENV / ARG | Runtime variables / build-time variables | Metadata only |
| USER | Sets the user for later instructions and runtime | Metadata only |
| CMD / ENTRYPOINT | Default command / fixed executable at start | Metadata only |
The exec form of CMD and ENTRYPOINT, written as a JSON array, runs the program directly as PID 1 so it receives stop signals. The shell form wraps the command in /bin/sh -c, which can prevent graceful shutdown because the shell does not forward signals.
Multi-stage builds and image size
A multi-stage build uses several FROM instructions in one Dockerfile. Early stages contain compilers, package managers and test tools; the final stage copies only the built artifacts into a minimal runtime base. This separates build-time dependencies from what ships to production.
The choice of final base image has a large effect on size and attack surface. The table compares common options; sizes are approximate and change between releases.
| Base image type | Approximate size | Trade-off |
|---|---|---|
| Full distribution (Debian, Ubuntu) | Roughly 70-130 MB | Familiar tools, larger attack surface |
| Slim variants | Roughly 30-80 MB | Fewer packages, still glibc-based |
| Alpine | About 5-10 MB | Very small, uses musl libc, compatibility caveats |
| Distroless | A few MB to tens of MB | No shell or package manager, harder debugging |
| scratch | Empty | Only for static binaries |
Dockerfile compared with related container concepts
A Dockerfile is one artifact in a chain that ends with running containers. Confusing the links in that chain leads to misplaced configuration, such as putting runtime secrets into the build.
| Term | What it is | Relation to the Dockerfile |
|---|---|---|
| Dockerfile | Build recipe in text form | Source of truth for the image |
| Container image | Immutable layers plus configuration | The output of building a Dockerfile |
| Container registry | Storage and distribution service for images | Where built images are pushed |
| Container | Running instance of an image | Starts with the Dockerfile’s CMD and ENTRYPOINT |
| Docker Compose | File describing multi-container applications | References images or Dockerfiles to build |
Writing an efficient and secure Dockerfile
Good Dockerfiles share a few habits. The most important is ordering instructions by change frequency: copy dependency manifests and install dependencies first, then copy the application source, so routine code changes do not invalidate the dependency layers.
| Mistake | Consequence | Fix |
|---|---|---|
| Copying all source before installing dependencies | Slow rebuilds after every code change | Copy manifests first, then install, then copy source |
| Using the latest tag for the base | Non-reproducible builds | Pin a version tag or digest |
| Secrets in ENV, ARG or copied files | Credentials stored in image layers | Use build secret mounts and runtime injection |
| Running as root | Higher impact if the container is compromised | Add a USER instruction with an unprivileged account |
| Package caches left in layers | Larger images | Clean caches in the same RUN instruction |
Deleting a file in a later instruction does not remove it from earlier layers; it only hides it. A secret copied in one step and deleted in the next remains extractable from the image, which is why sensitive data must never enter a regular layer.
Explained without jargon
A Dockerfile is a recipe for packaging an application. It lists, step by step, which starting ingredients to use, what to add and how to prepare everything, so that anyone following it gets exactly the same result.
The finished dish is the container image, which can be shipped and served anywhere. Because the recipe is written down, the package can be rebuilt identically at any time, on any machine, by any team member.
A typical use case
A development team ships a Node.js web service. Its original Dockerfile started from a full distribution image, copied the entire project first and ran the install step afterward, producing an image of about 1 GB that took several minutes to rebuild after every code change.
The team rewrites it as a two-stage build: the first stage installs dependencies and compiles the code, and the final stage copies only production files onto a slim runtime image, runs as an unprivileged user and uses the exec form of CMD. The image shrinks to roughly 150 MB, rebuilds after code changes take seconds thanks to layer caching, and vulnerability scans report far fewer findings.