BuildKit’s frontend API lets you define entirely new ways to build container images, not just tweak existing ones.

Let’s see how to build a custom frontend for a hypothetical JavaScript project that uses npm for dependency management and esbuild for bundling.

# syntax=docker/dockerfile:1
FROM alpine:3.18 AS builder
RUN apk add --no-cache nodejs npm
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npx esbuild --bundle --outfile=/app/bundle.js --minify src/index.js

FROM alpine:3.18
COPY --from=builder /app/bundle.js /app/bundle.js
CMD ["node", "/app/bundle.js"]

This Dockerfile uses a special syntax directive: syntax=docker/dockerfile:1. This tells Docker to use BuildKit’s docker/dockerfile frontend, which understands this extended syntax.

The builder stage installs Node.js and npm, copies package.json and package-lock.json, runs npm ci to install dependencies, copies the rest of the source code, and then uses esbuild to create a bundled JavaScript file. The final stage copies this bundle.js into a clean Alpine image and sets it as the command to run.

BuildKit’s frontend API is a powerful abstraction. Instead of just parsing Dockerfiles, BuildKit can delegate the entire build process to an external program specified by the syntax directive. This program, the "frontend," receives the build context and instructions, and then communicates back to BuildKit what steps to perform (e.g., downloading base images, copying files, running commands, and ultimately defining the final image layers).

Our example uses the built-in docker/dockerfile frontend, which is familiar. But imagine a frontend written in Go or Rust that understands a completely different build language or integrates directly with a specific framework’s build tools. This frontend would receive the build context and any arguments you pass to docker build (via --build-arg), process them according to its own logic, and then instruct BuildKit on how to assemble the final image. It could, for instance, fetch dependencies from a private repository, perform complex code transformations, or even generate multiple images from a single build invocation.

The magic happens because the syntax directive isn’t just a comment; it’s an instruction to BuildKit to run the specified image (in this case, docker/dockerfile:1) as a specialized build process. This frontend image is responsible for interpreting the subsequent lines of the Dockerfile (or whatever language it’s designed to parse) and generating a series of build operations that BuildKit can execute. These operations are represented internally by BuildKit’s "solver," which optimizes the build graph, caches intermediate results, and executes the necessary steps across potentially multiple build platforms.

When you use syntax=docker/dockerfile:1, you’re essentially telling BuildKit, "Use the official Dockerfile parser and executor for this build." This parser understands multi-stage builds, COPY --from, and all the other advanced features of the Dockerfile syntax. The docker/dockerfile image itself contains the logic to parse these instructions and then generate the low-level build requests that BuildKit’s core engine processes. This decoupling allows the core BuildKit engine to remain lean and efficient, while the frontends handle the complexity of different build languages and workflows.

Many people think BuildKit is just a faster Dockerfile parser. The reality is that the syntax directive allows BuildKit to be a platform for building images, not just an executor of a single format. You can write your own frontend image that understands a different syntax entirely, perhaps one tailored to a specific application framework or a novel way of defining container images. This frontend would then be responsible for translating your custom build instructions into the BuildKit API calls that BuildKit understands.

The next step is understanding how to write your own frontend image that BuildKit can call via the syntax directive.

Want structured learning?

Take the full Buildkit course →