Docker Buildx is a powerful tool for building container images, and when combined with BuildKit, it unlocks a whole new level of performance and flexibility.

Let’s see BuildKit in action. Imagine you have a simple Dockerfile:

FROM alpine:latest
RUN echo "Hello from Docker Buildx and BuildKit!" > /app/hello.txt
COPY hello.txt /app/hello.txt
CMD ["cat", "/app/hello.txt"]

Normally, building this would be a standard docker build operation. But with BuildKit enabled via docker buildx, we can achieve faster builds, better caching, and more advanced features.

First, you need to ensure you have docker buildx installed. It’s usually included with recent Docker Desktop versions. To check, run:

docker buildx version

If it’s not there, you might need to install it separately depending on your Docker installation.

Now, to enable BuildKit, you don’t explicitly "enable" it in the Docker daemon’s configuration file for buildx in the same way you might for older Docker versions. Instead, buildx defaults to using BuildKit. When you create a builder instance with buildx, it automatically sets up a BuildKit daemon.

Let’s create a new builder instance:

docker buildx create --use --name mybuilder

The --use flag makes this new builder the default for subsequent buildx commands. The --name mybuilder gives it a recognizable name.

Now, let’s build our simple Dockerfile using this builder:

docker buildx build --platform linux/amd64 --tag my-alpine-app:latest .

Here’s what’s happening under the hood:

  • docker buildx build: This invokes the buildx command.
  • --platform linux/amd64: This specifies the target build platform. BuildKit excels at cross-platform builds. You can specify multiple platforms like linux/amd64,linux/arm64.
  • --tag my-alpine-app:latest: This tags the resulting image.
  • .: This indicates the build context (the current directory).

When this command runs, buildx communicates with the BuildKit daemon associated with mybuilder. BuildKit parses your Dockerfile, analyzes dependencies, and executes the build steps. The key advantage is its parallel execution of build steps and sophisticated caching mechanisms. For instance, if you change only a later layer in your Dockerfile, BuildKit can often reuse intermediate build results from earlier layers, drastically speeding up rebuilds.

The mental model for BuildKit is that it treats your Dockerfile as a directed acyclic graph (DAG) of build steps. Each instruction (FROM, RUN, COPY, etc.) is a node, and dependencies between them form the edges. BuildKit’s solver optimizes this graph, executing independent nodes in parallel and intelligently caching the results of each node. This is a fundamental departure from the sequential, layer-by-layer execution of the older Docker builder.

Consider the caching. If you have a RUN apt-get update && apt-get install -y some-package followed by a COPY . /app, and you only change files copied into /app, BuildKit will not re-run the apt-get install command. It recognizes that the inputs to that step (the package list) haven’t changed, and it can reuse the cached image layer from that step. This is far more granular than the older builder’s cache, which often invalidated the entire cache from a RUN command onwards if the command itself changed, or if a preceding COPY instruction changed.

One of the most significant benefits of BuildKit, especially with buildx, is its output flexibility. You can build images and push them directly to a registry without needing to docker login first if you’re pushing to a public registry like Docker Hub, or if you’ve already authenticated. You can also export the build artifacts in various formats, like a tarball or even a local OCI image layout.

For example, to push directly to Docker Hub (assuming you’re logged in via docker login):

docker buildx build --platform linux/amd64 --tag your-dockerhub-username/my-alpine-app:latest --push .

The --push flag tells BuildKit to build the image and then immediately push it to the specified registry. This is incredibly useful in CI/CD pipelines.

If you’re building for multiple architectures, BuildKit handles the complexity of cross-compilation and manifest list creation seamlessly. For instance, building for linux/amd64 and linux/arm64 simultaneously:

docker buildx build --platform linux/amd64,linux/arm64 --tag your-dockerhub-username/my-multiarch-app:latest --push .

BuildKit will build the image for each specified platform and then create a manifest list that points to the correct image for the architecture it’s being pulled on.

The next step in mastering buildx and BuildKit is exploring its advanced features like multi-stage builds, secret management, and custom build arguments.

Want structured learning?

Take the full Buildkit course →