What does the `# syntax=docker/dockerfile:1` line at the top of a Dockerfile do?
answer
- Not really a comment
- Must be the very first line
- Names an image, not a version number
- BuildKit runs it to parse the file
- Decouples syntax from the engine
basics
~20 sIt is a BuildKit parser directive naming the frontend image that parses the Dockerfile. BuildKit pulls docker/dockerfile:1 and uses it instead of its built-in parser, so newer Dockerfile syntax works without upgrading the Docker Engine.
solid answer
~40 sIt is a **parser directive**, not a comment, and it must be the first line — before any instruction and before any ordinary comment, or BuildKit treats it as plain text. Its value is an image reference: BuildKit pulls `docker/dockerfile:1`, runs it as the build's frontend, and that image — rather than the parser compiled into the local BuildKit — translates the Dockerfile into LLB. The point is decoupling: Dockerfile syntax such as heredoc `RUN <<EOF` blocks, `COPY --link` and `ADD --checksum` ships in frontend releases, so pinning the frontend lets an older engine build a Dockerfile that uses recent syntax. The `1` tag floats across the backwards-compatible 1.x line; `1.7` or a digest pins harder. The classic builder ignores the directive completely.
code
dockerfile · 9 lines# syntax=docker/dockerfile:1
FROM alpine:3.20
RUN <<EOF
set -eux
apk add --no-cache curl ca-certificates
adduser -D -u 10001 app
EOF
USER app
CMD ["/usr/bin/curl", "--version"]go deeper
Recognise the line and be able to say it selects the Dockerfile parser BuildKit uses, and that it belongs on the very first line. Knowing it is what makes heredoc RUN blocks work is a solid bonus.
Explain the frontend concept: BuildKit understands LLB, and the frontend image converts Dockerfile syntax into it. Cover why the tag choice matters and why the directive is ignored by the classic builder.
Bring in the operational angle: an unmirrored frontend image is a build-time network dependency and an executed third-party artefact, so mirroring and a deliberate refresh policy belong in your build platform.
Own the policy question — whether the organisation floats on :1 for feature velocity or pins to a digest for reproducibility, who bumps it, and how a fleet of repositories avoids drifting into two different Dockerfile dialects.
### What that line actually is `# syntax=docker/dockerfile:1` looks like a comment, and to anything that is not BuildKit it *is* a comment. To BuildKit it is a **parser directive**: a small, fixed set of `# key=value` lines that must appear at the very top of a Dockerfile, before any build instruction and before any ordinary comment. Once a normal comment or an instruction has been seen, the parser stops looking for directives and every later `# syntax=` line is treated as plain text — which is the single most common reason the pin appears to do nothing. The directive's value is an **image reference**: a container image that BuildKit will pull and run as the build's *frontend*. ### What a frontend is BuildKit itself does not understand Dockerfiles. It understands LLB, a graph of build operations. A frontend is the component that converts some build definition into LLB, and the Dockerfile frontend — published as `docker/dockerfile` — is the one that converts Dockerfile syntax. BuildKit ships with a copy of that frontend compiled in, which is what you get when the directive is absent. When the directive is present, BuildKit fetches the named image from a registry, runs it as a short-lived container, hands it the Dockerfile and the build's parameters, and takes back the LLB it produces. The image is cached after the first pull, so the cost is paid once per builder, not once per build. ### Why anyone bothers Decoupling. The Dockerfile language grows independently of the Docker Engine. Heredoc bodies on `RUN` and `COPY`, `COPY --link`, `ADD --checksum`, additional `RUN --mount` types and other syntax all arrived in frontend releases. With the pin, a machine running an older engine can still build a Dockerfile that uses recent syntax, because the parser doing the work comes from the registry rather than from the installed binary. Without the pin you are limited to whatever the local BuildKit was compiled with, and a Dockerfile using newer syntax fails with a parse error on some machines and succeeds on others — the classic "works on my laptop" build failure. ### Choosing the tag `docker/dockerfile:1` is the tag almost everyone should use. It is a moving major-version tag: it resolves to the newest 1.x.y release, which is maintained as backwards compatible, so you get new syntax and fixes without editing thousands of Dockerfiles. `docker/dockerfile:1.7` narrows to a minor line, and a full `1.7.0` or a digest pins exactly — the right choice when a regulated pipeline needs bit-identical build inputs, at the cost of manual bumps. There is also a `-labs` channel (`docker/dockerfile:1-labs`) carrying experimental features that are not in the stable frontend; treat anything you use from it as subject to change. ### What it unlocks, concretely The most visible feature is heredoc support, which turns a wall of backslash continuations into an ordinary shell script inside the Dockerfile: ```dockerfile # syntax=docker/dockerfile:1 FROM alpine:3.20 RUN <<EOF set -eux apk add --no-cache curl ca-certificates adduser -D -u 10001 app EOF USER app ``` The body runs in a single `RUN`, so it is still one layer, and `set -eux` gives you failure-on-error and a trace without repeating `&& \` on every line. `COPY <<EOF /etc/app/config.ini` writes an inline file the same way. ### The operational edges **The frontend is code you execute.** BuildKit pulls that image and runs it as part of every build. It comes from Docker Hub by default. In an environment that cares about supply chain, mirror it into your own registry, reference it there, and decide deliberately how often the mirror is refreshed — a floating `:1` tag means a build today can use a frontend that did not exist last week. **It needs to be reachable.** On an air-gapped or heavily firewalled builder, an unmirrored `# syntax=` line turns every build into a failed image pull. Either mirror the frontend or drop the directive and accept the built-in parser's feature set. **The classic builder ignores it entirely.** Build the same file with `DOCKER_BUILDKIT=0` and the line is just a comment, so any syntax it was enabling fails to parse. That is a useful tell when one CI job fails and another succeeds on the same Dockerfile. **It is per-file, not per-project.** The directive lives in each Dockerfile. Adding it to one file changes nothing about the others, which is why partial adoption across a repository produces builds that behave differently file by file.
- Why pin to `docker/dockerfile:1` rather than a full version like `1.7.0`?`1` floats across the 1.x line, which upstream maintains as backwards compatible, so every build picks up new syntax and fixes without editing hundreds of Dockerfiles. A full version or a digest is the right call when the pipeline must have identical build inputs over time or when you mirror the frontend and control its refresh, but then someone has to own the bumps.
- What breaks if that line is present on a builder with no route to Docker Hub?Every build fails at the frontend pull, before a single instruction runs. The fix is to mirror the frontend image into a registry the builder can reach and reference it there, or to drop the directive and stay within the parser compiled into the local BuildKit. It is also worth remembering that the frontend is an image you execute, so mirroring gives you supply-chain control as well as availability.
- Someone adds the directive halfway down the file and reports it has no effect. Why?Parser directives are only recognised at the very top of the Dockerfile. As soon as a build instruction or an ordinary comment appears, the parser stops looking for them and any later `# syntax=` line is read as a normal comment. Move it to line one — above every comment, not just above the first FROM.
It is a shebang for the Dockerfile: instead of hoping the local tool understands the dialect, the file names the interpreter it wants and BuildKit fetches it.
saying these in an interview costs you the question
- Calls it a comment with no effect on the build
- Thinks it pins the base image version
- Says it can appear anywhere before FROM
- Assumes the classic builder honours it
- Never considers that the frontend is pulled and executed