With the rise of ARM-based cloud computing resources and Apple’s switch from Intel-based CPUs to Apple Silicon, building Docker Images for multiple architectures has become increasingly important. We, as developers, must ensure that our applications can be executed on every potential architecture. We can easily achieve that by using Docker CLI and the buildx plugin. This article will look at building Docker Images for multiple architectures by containerizing a simple Go application. Before we dive into building the multi-arch Docker Images, let’s quickly revisit what buildx is.



What is buildx

buildx is an open-source Docker CLI plugin that brings enhanced build capabilities based Moby BuildKit to the Docker CLI. It’s worth mentioning that the x in buildx stands for experimental, meaning that features and capabilities provided by buildx are not yet stable. Keep in mind that actual features and command line interface may change. Once a feature provided by buildx has matured and become stable, it will be integrated into regular Docker CLI commands such as docker build and others.

buildx requires at least Docker 19.03 or later. buildx is already installed on your system, If you’re using Docker for Desktop on Windows or macOS. Also, on Linux, it’s part of your Docker installation if you’ve installed Docker using the DEP or RPM packages. Otherwise, you have to install buildx manually (following the instructions provided in the GitHub repository).

The Go sample application

As mentioned earlier, we will use a simple HTTP API written in Go for demonstration purposes. You can find the sample application in this GitHub repository. All application code resists in main.go:

package main

import "github.com/gin-gonic/gin"

type Response struct {
    Message string `json:"message"`
}

func main() {
    r := gin.Default()

    r.GET("/ping", func(ctx *gin.Context) {
        ctx.JSON(200, Response{
            Message: "pong",
        })
    })
    r.Run(":8080")
}

If we take a look at the Dockerfile, we will see that there is nothing special. A regular multi-stage Dockerfile for building Go applications and running them on alpine:latest:

#build stage
FROM golang:alpine AS builder
RUN apk add --no-cache git
WORKDIR /go/src/app
COPY . .
RUN go get -d -v ./...
RUN go build -o /go/bin/app -v ./...

#final stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /go/bin/app /app
ENTRYPOINT /app
LABEL Name=multiarchdocker Version=0.0.1
EXPOSE 8080

Upon building multi-arch Docker Images, Docker sets a bunch of build arguments (ARG) that we could use in our Dockerfile when you need to. Those arguments include things like TARGETPLATFORM, TARGETOS, and TARGETARCH. Check out the documentation here to see the entire list of predefined arguments. For the scope of this article, we will ignore that our application is running with root-privileges. However, for production usage and real-world scenarios, you should run your applications using a dedicated, non-privileged user.

Build for multiple architectures

To build multi-arch Docker Images with docker buildx, we need a builder. The builder is responsible for building our Docker Images for the desired architectures. We can easily create and bootstrap a new builder, as shown here:

# Create a new builder
docker buildx create --name mybuilder --bootstrap --use

Optionally, we can specify which platforms we want our builder to support using the --platform flag. If we omit the platform flag - as we did in the example above - we will get a builder to build Docker Images for all supported platforms. We can inspect existing builders to get a list of the supported platforms:

# Inspect our custom builder
docker buildx inspect mybuilder

Name:  mybuilder
Driver: docker-container

Nodes:
Name:   mybuilder0
Endpoint: unix:///var/run/docker.sock
Status:  running
Platforms: linux/amd64, linux/amd64/v2, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6

Having the builder in place and knowing all supported platforms, we can use docker buildx build to build our Docker Image for the architectures we’re interested in. For now, let’s build our Docker Images for linux/arm64/v8 and linux/amd64:

# build the Docker Image for arm64 and amd64
docker buildx build --push \
  --platform linux/arm64/v8,linux/amd64 \
  --tag thorstenhans/multi-arch-go:latest \
  .

Because we provide the --push flag, Docker will automatically push our Docker Images to Docker Hub.

Verify multiple architectures in Docker Hub

We can verify if our Docker Image has been pushed correctly to Docker Hub by simply browsing Docker Hub and looking for our new Docker Image (thorstenhans/multi-arch-go). If we look at Tags, we’ll see the latest tag persisted for both architectures.

multi-arch Docker Image in Docker Hub

Alternatively, we can use docker buildx imagetools to inspect the Docker Image (still being persisted only in Docker Hub) directly from the terminal:

# Inspect our multi-arch Image directly from the terminal
docker buildx imagetools inspect thorstenhans/multi-arch-go:latest

Name:   docker.io/thorstenhans/multi-arch-go:latest
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest:  sha256:d1bf1e0354a2f3a3087032e23a7194731f9b10d432a088235bf129987f277c6d

Manifests:
 Name:   docker.io/thorstenhans/multi-arch-go:latest@sha256:7a075078ea365e0d30ccea21bbab5995925ea3f7cd85113195d07821ab4ef5c2
 MediaType: application/vnd.docker.distribution.manifest.v2+json
 Platform: linux/arm64

 Name:   docker.io/thorstenhans/multi-arch-go:latest@sha256:d176bc796c9823e2778f7df16a0586be1ef009c4d5a4e4ebae60882640585aa6
 MediaType: application/vnd.docker.distribution.manifest.v2+json
 Platform: linux/amd64

When we pull the image, Docker will automatically pull the Docker Image matching the host platform from which the docker pull command was issued.

# Pull the Docker Image from an intel-based mac
docker pull thorstenhans/multi-arch-go:latest

latest: Pulling from thorstenhans/multi-arch-go
213ec9aee27d: Already exists
e10dc8b62d66: Pull complete
776133c6b0e8: Pull complete
Digest: sha256:d1bf1e0354a2f3a3087032e23a7194731f9b10d432a088235bf129987f277c6d
Status: Downloaded newer image for thorstenhans/multi-arch-go:latest
docker.io/thorstenhans/multi-arch-go:latest

To verify which platform was pulled, use docker image inspect as shown in the following snippet:

# Inspect the pulled Docker Image
docker image inspect thorstenhans/multi-arch-go:latest -f "{{ .Architecture }}"

amd64

Overwrite platform when pulling Docker Images

Although Docker automatically pulls the correct architecture for the host, there are use cases in which we want to pull a Docker Image for a particular platform. We can do so by adding the --platform flag with the corresponding platform identifier to docker pull:

# Explicitly pull the linux/arm64 image
docker pull thorstenhans/multi-arch-go:latest --platform linux/arm64

latest: Pulling from thorstenhans/multi-arch-go
9b18e9b68314: Pull complete
3de262401255: Pull complete
e89d527d0602: Pull complete
Digest: sha256:d1bf1e0354a2f3a3087032e23a7194731f9b10d432a088235bf129987f277c6d
Status: Downloaded newer image for thorstenhans/multi-arch-go:latest
docker.io/thorstenhans/multi-arch-go:latest

Again, let’s verify that the image was pulled on our machine with the desired platform:

# Inspect the pulled Docker Image
docker image inspect thorstenhans/multi-arch-go:latest -f "{{ .Architecture }}"

arm64 

What we’ve covered in this article

Throughout the article, we covered quite some topics:

  • 💡Understood why we need multi-arch Docker Images
  • 👋🏻Learned what buildx is and how we can get access to it
  • ⚙️Created a custom builder to create Docker Images for multiple architectures
  • 📦Containerized a sample application for arm64 and amd64
  • ✅Verified that a multi-arch Docker Image is correctly persisted in Docker Hub
  • ⬇️ Verified that Docker pulled the Docker Image for the correct platform
  • ❗️Overwrite the platform when pulling a Docker Image

Conclusion

Providing Docker Images for various architectures and platforms has become more and more important in recent years. By leveraging docker buildx build, we can easily build multi-arch Docker Images and distribute them using either Docker Hub or any other Docker Registry v2 compliant artifact registry.