The guide
A Dockerfile Is Read Twice: Once by a Parser, Then by Everyone Who Pulls the Image
How Docker reads the file line by line, why layer order decides build speed, and how pinning, users and secrets quietly become part of what you ship.
- latest the tag the builder assumes when a FROM line names none
- uid 0 the default user inside a container: root
- .wh. OCI prefix that hides a deleted file in a later layer
- ~/.cache/pip where pip keeps downloaded packages on Linux
Read the full guide Fold the guide
A Dockerfile looks like a shell script with capital letters. It is not one. It is a small language with its own parser, its own rules about comments and line endings, and its own idea of when a variable gets its value. Most surprises in container builds come from treating it as a script, and most of the rest come from forgetting that every line ends up inside a file that other people will download.
Docker has used the format since its first public release on March 20, 2013. The rules have grown since then, and the current reference is long. Yet the problems people actually ship fall into a short list: images that change under them, builds that are slower than they need to be, containers that run as root without anyone deciding so, and passwords that were “only used during the build”.
How the builder reads the file
The builder works line by line, but a line is not always a line. A backslash at the very end continues the instruction onto the next physical line, and the backslash itself disappears. Nothing is inserted in its place, so the space you leave before it is the only space you get. Inside such a continued instruction, a line that starts with a hash is dropped as a comment, and an empty line is dropped too, with a warning that it will become an error in a future release.
A hash anywhere else is plain text. RUN echo 'we are running some # of cool things' passes the hash straight to the shell. This trips people who paste an inline comment after an instruction: the builder does not see a comment there, it sees more arguments.
Before the first instruction, a few comments are not comments at all. Lines such as # syntax=docker/dockerfile:1 or # escape= followed by a backtick are parser directives. They count only at the very top of the file. The first ordinary comment, empty line or instruction ends the directive section, and anything that looks like a directive after that point is just a comment that does nothing. A directive may appear once.
The escape directive exists mostly for Windows, where the backslash is a path separator. The reference shows the classic failure: a line ending in c:\\ and the next line folded together into one broken COPY. Current BuildKit reads a doubled escape character at the end of a line as the end of the instruction, while the reference’s printed output came from an older builder that joined the lines. When the outcome depends on the builder, the honest advice is the reference’s own: switch the escape character to a backtick, or avoid ending a line with a backslash at all.
Heredocs add a third kind of line. In RUN, COPY and ADD, a word such as <<EOF tells the builder that the following lines, up to one that holds only EOF, belong to the instruction. The body is raw text: no continuation, no comment stripping. A minus sign, as in <<-EOF, removes leading tabs, the same convention shells use for indented here documents. Quoting the word turns off variable expansion inside the body. Forget the closing line and the builder stops with an unterminated heredoc.
Then there are two ways to write a command. Shell form is a plain string that the builder runs through /bin/sh -c. Exec form is a JSON array, so the strings need double quotes and every backslash must be doubled. Write single quotes and the builder does not complain; it quietly treats the text as shell form and runs something you did not intend.
Layers, order and the cache
Each instruction produces a layer, and the layers stack. When a layer changes, every layer after it is rebuilt, even if those later steps would produce the same bytes. That one rule explains most advice about ordering. Put steps that rarely change near the top and steps that change often near the bottom.
The common mistake is a COPY of the whole project before installing dependencies. Then any edit to any source file invalidates the install step, and the build downloads everything again. Docker’s own guidance is to copy the manifest and lock file first, install, and only then copy the rest of the code.
The cache can also preserve something stale. If apt-get update sits in its own RUN, that layer stays cached for months. Later, someone adds a package to the separate install line, the builder reuses the old update layer, and the install reads package lists that no longer match the mirrors. The fix is to run update and install in the same RUN, add --no-install-recommends so optional extras stay out, and delete /var/lib/apt/lists before the step ends.
Package caches are the other source of bulk. pip keeps downloads in ~/.cache/pip on Linux, and its documentation says to disable that cache only when caching happens at a higher level, such as layered caches in container builds. Inside an image the cache is dead weight. You can switch it off with --no-cache-dir, or give BuildKit a cache mount so the downloads persist between builds without ever landing in a layer. The same mount pattern works for npm, whose default cache lives in /root/.npm.
Multi-stage builds cut size further. A build stage holds compilers and headers, and the final stage copies only the result with COPY --from. BuildKit builds only the stages the target depends on, so a stage that nothing reaches is simply skipped. That is useful when deliberate and confusing when it comes from a typo in a stage name.
Pinning, users and what the image remembers
A FROM line without a tag means latest. Tags are pointers that a publisher can move, so the same Dockerfile can produce a different image next month. A version tag narrows the drift; a digest removes it, because a digest always names the same image. Docker’s guidance is candid about the cost: pinning by digest also means updating it yourself, and opting out of automatic security fixes until you do.
Users are the next quiet default. The default user inside a container is root, uid 0. Docker drops many kernel capabilities by default, so root in a container is weaker than root on the host, and user namespaces can map it to an unprivileged account. That mapping is available but not enabled by default. A service that needs no privileges should create its own user with a fixed UID and switch to it with USER. Docker also advises against installing sudo, whose terminal and signal behavior is unpredictable in containers.
Signals matter for the same process. Shell form CMD and ENTRYPOINT start your program under a shell that does not pass signals, so docker stop never reaches it as SIGTERM. Exec form makes the program the main process instead, which is why Docker recommends it.
Secrets are where the layer model bites hardest. An image is a stack of changesets, and the OCI specification records a deletion as a whiteout file named with the .wh. prefix in a later layer. The earlier layer still holds the original bytes. Deleting a key in a later step hides it from the running container, not from anyone who unpacks the image. NIST’s container security guide makes the same point about secrets embedded in an image file system: anyone with access to the image can parse it to learn them.
Build arguments are not a safe channel either. Docker’s reference warns that they show up in docker history and in some provenance attestations, and ENV values persist in the image outright. The supported route is a secret mount, which exposes the value to a single RUN instruction and leaves nothing behind in the image or its metadata.
A worked example, and the limits of a linter
The Dockerfile Linter on this page opens with a four-stage sample. It parses 29 lines into 21 instructions and reports 17 findings: 1 error, 8 warnings and 8 info notes, with one more hidden by a # lint-ignore DFL011 comment placed above the final FROM. The error is an ARG NPM_TOKEN, a name that looks like a credential.
The most instructive warning is a single letter. The final stage copies --from=biuld, a misspelling of the build stage. No stage has that name, so the builder would treat it as an image to pull. Because nothing now reaches the build stage, and the build stage was the only thing reaching the dependency stage, all three earlier stages show up as skipped. Correct the typo in the editor and two of those notes disappear at once; the third belongs to a stage that really is unused.
Every rule shows its reason and a fix, and any rule can be switched off, which is the right response when a team has decided differently on purpose. The findings download as JSON, as plain text, or as a SARIF 2.1.0 log that code review systems can read, checked against the published OASIS schema.
What a linter of this kind cannot do is just as important. It reads only the text. It cannot see inside the base image, so it cannot know which user that image already sets or what packages it holds. It cannot see the build context, values passed at build time, or anything a script downloads. Its secret checks match names, file names and private key headers from a short stated list, and a secret with an innocent name walks straight past them.
A clean report therefore means that none of the stated rules fired, not that the image is safe. Treat it the way you would treat a spelling checker: fast, consistent, and good at catching the mistakes people make when they are tired. The decisions about trust, pinning and privilege still belong to the people who will run the container.
Sources
Every source consulted for this guide. Links checked October 20, 2026.
- Dockerfile reference Docker Docs (Apache-2.0 documentation)
- Building best practices Docker Docs (Apache-2.0 documentation)
- Multi-stage builds Docker Docs (Apache-2.0 documentation)
- Docker build cache Docker Docs (Apache-2.0 documentation)
- Optimize cache usage in builds Docker Docs (Apache-2.0 documentation)
- Build secrets Docker Docs (Apache-2.0 documentation)
- SecretsUsedInArgOrEnv Docker Docs, build checks (Apache-2.0)
- JSONArgsRecommended Docker Docs, build checks (Apache-2.0)
- Running containers Docker Docs (Apache-2.0 documentation)
- Docker Engine security Docker Docs (Apache-2.0 documentation)
- Image Layer Filesystem Changeset Open Container Initiative image-spec, GitHub (Apache-2.0)
- NIST SP 800-190 Application Container Security Guide NIST (M. Souppaya, J. Morello, K. Scarfone), September 2017
- Caching - pip documentation The pip developers (MIT)
- Static Analysis Results Interchange Format (SARIF) Version 2.1.0 Plus Errata 01 OASIS Open
- Here document Wikipedia (CC BY-SA)
- Docker (software) Wikipedia (CC BY-SA)