BuildKit, Docker’s next-gen builder, lets you create images for multiple architectures simultaneously, a game-changer for distributing your applications across x86, ARM, and other platforms.

Let’s see it in action. Imagine we have a simple Go application that prints its architecture.

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Printf("Hello from %s/%s!\n", runtime.GOOS, runtime.GOARCH)
}

We’ll build a Dockerfile for this:

# syntax=docker/dockerfile:1
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY main.go .
RUN --mount=type=cache,target=/go/pkg/mod \
    go build -o app .

FROM alpine:latest
COPY --from=builder /app/app /app/app
CMD ["/app/app"]

Now, to build this for multiple architectures, we’ll use docker buildx. First, ensure you have buildx installed and configured. If not, run docker buildx install and then docker buildx create --use.

To build for linux/amd64 and linux/arm64 (common for desktops and Raspberry Pis/cloud instances respectively), we’d run:

docker buildx build --platform linux/amd64,linux/arm64 -t yourusername/multiarch-app:latest . --push

The --platform flag is key here. You can specify comma-separated architectures. The --push flag automatically pushes the resulting multi-arch image to a registry. If you omit --push, buildx will create a local manifest list, which you can then inspect or push later.

This command triggers BuildKit to:

  1. Spin up a builder instance: This might be a local Docker daemon or a remote builder.
  2. Execute the Dockerfile for each specified platform: It essentially runs the build process in isolated environments for linux/amd64 and linux/arm64.
  3. Create separate images: For each platform, a distinct image is built.
  4. Assemble a manifest list: This is the magic. BuildKit doesn’t just build two separate images; it creates a manifest list. This list is a pointer that, when pulled by a Docker client, automatically selects and downloads the correct image for the client’s architecture.

The problem this solves is the complexity of managing separate build pipelines and registries for different CPU architectures. Before buildx, you’d typically build an image on an amd64 machine, then potentially cross-compile or use a different build host for arm64, leading to duplicated effort and potential inconsistencies.

buildx leverages BuildKit’s advanced caching and parallel execution capabilities. It can cache layers independently for each architecture, making subsequent builds much faster. The --mount=type=cache,target=/go/pkg/mod in our Dockerfile is an example of how BuildKit handles build cache more effectively than the traditional builder.

The exact levers you control are primarily within the docker buildx build command:

  • --platform: Defines target architectures. You can find supported platforms with docker buildx supported.
  • --output: Controls where the build artifacts go. type=docker outputs a local image, type=oci an OCI image, and type=tar a tarball. type=inline outputs the image directly to the daemon.
  • --push: Pushes the resulting manifest list and associated images to a registry.
  • --tag: Tags the manifest list.
  • --builder: Specifies which builder instance to use.

A crucial detail often overlooked is how the Docker daemon interacts with manifest lists. When you run docker pull yourusername/multiarch-app:latest, your Docker client queries the registry for the latest tag. It receives the manifest list and then checks its own system’s architecture (uname -m). Based on this, it downloads the specific image digest that matches. This is why simply building locally without --push and then trying to docker run might not behave as expected if your local daemon isn’t configured to handle multi-arch manifests correctly or if the manifest list isn’t properly registered.

The next concept you’ll likely encounter is optimizing these multi-platform builds further using advanced caching strategies and potentially distributing the build load across multiple builder nodes.

Want structured learning?

Take the full Buildkit course →