Syndication Icon
Published April 1, 2021 Updated April 1, 2021
Cover
Docker Multi-Platform Images

I recently switched from an Apple Intel machine to an Apple Silicon machine which caused some complications with Docker. In fact, at the time of this writing, Apple Silicon machines have to use the Docker Preview version of the Desktop app in order to use Docker at all. Therfore, the latest Release Candidate 2 (RC2) is significant because it’s the first time I’ve been able to launch Docker on my Apple Silicon machine.

During my upgrade to RC2, however, I discovered a new and interesting situation: multi-platform images. Due to the Apple Silicon machine (ARM 64), built images were not compatible with existing architectures like Apple Intel, Circle CI, Heroku, etc. In fact, I had to learn how multi-platform builds work, which might be of interest to others facing similar problems.

Getting Started

Luckily, building for multiple platforms is supported via Docker’s buildx command, which I was oblivious to until now but now rely upon. You can read up on buildx by following the link or printing help information from the command line:

docker buildx --help

This will yield the following output:

Usage:  docker buildx [OPTIONS] COMMAND

Build with BuildKit

Options:
      --builder string   Override the configured builder instance

Management Commands:
  imagetools  Commands to work on images in registry

Commands:
  bake        Build from a file
  build       Start a build
  create      Create a new builder instance
  du          Disk usage
  inspect     Inspect current builder instance
  ls          List builder instances
  prune       Remove build cache
  rm          Remove a builder instance
  stop        Stop builder instance
  use         Set the current builder instance
  version     Show buildx version information

Run 'docker buildx COMMAND --help' for more information on a command.

Builder Instance Creation

By default, Docker images will use your current system’s architecture. For an Apple Silicon machine, this means ARM 64. In order to remain compatible for local use while also working on other platforms you’ll need to build for multiple platforms at once. This is where knowing how to configure and use builder instances comes into play.

To create a builder instance, you’ll want to run the following command:

docker buildx create --name multiarch --platform linux/arm64,linux/amd64

I opted to name my builder instance, multiarch, but you can use whatever you like. For platforms (i.e. --platform), I only needed to support Apple Silicon (ARM 64) and AMD 64 machines.

Once your builder instance is created, you can view your listing by running the following command:

docker buildx ls

In my case, this will output the following:

NAME/NODE    DRIVER/ENDPOINT             STATUS  PLATFORMS
multiarch *  docker-container
  multiarch0 unix:///var/run/docker.sock running linux/amd64*, linux/arm64*, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
default      docker
  default    default                     running linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

Notice multiarch has an asterisk next to it. This is because I switched from default — what you initially start with — to multiarch by running the following command:

docker buildx use multiarch

The above is critical to registering the desired platforms you want to build with Docker.

Builder Instance Deletion

At risk of being Captain Obvious, you can remove an existing builder instance by running the following:

docker buildx rm multiarch

Doing so will remove the multiarch builder instance and immediately default you back to the default builder instance.

BuildX Alias

Before we continue, though, I want to pause and point out that you can alias buildx as build which you are probably more familiar with if you’ve spent any time building Docker images in the past. To do this, run the following:

docker buildx install

If, at any time, you are not happy with this setup, you can uninstall the alias by running:

docker buildx uninstall

Using the alias avoids having to constantly type: docker build buildx. For the rest of this article, I’ll assume you are using this alias which will be important when discussing further command line usage in this article.

Building Multiple Platforms

Now that we’ve discussed buildx and builder instances, we can focus on using our Dockerfile to build images for multiple platforms via a single command. That command is:

docker build --platform linux/arm64,linux/amd64 --tag bkuhlmann/alpine-base .

In my case, I’m building an Alpine Linux base image which will produce the following output as it builds for both platforms (truncated for brevity):

[+] Building 15.8s (11/17)
 => [internal] booting buildkit                                                                 2.0s
 => => pulling image moby/buildkit:buildx-stable-1                                              1.6s
 => => creating container buildx_buildkit_multiarch0                                            0.4s
 => [internal] load build definition from Dockerfile                                            0.0s
 => => transferring dockerfile: 1.68kB                                                          0.0s
 => [internal] load .dockerignore                                                               0.0s
 => => transferring context: 152B                                                               0.0s
 => [linux/amd64 internal] load metadata for docker.io/library/alpine:3.13.3                    2.9s
 => [linux/arm64 internal] load metadata for docker.io/library/alpine:3.13.3                    3.0s
 => [auth] library/alpine:pull token for registry-1.docker.io                                   0.0s
 => [internal] load build context                                                               0.0s
 => => transferring context: 412B                                                               0.0s
 => [linux/amd64 2/5] WORKDIR /usr/src                                                          0.0s
 => [linux/arm64 2/5] WORKDIR /usr/src                                                          0.0s
 => [linux/amd64 3/5] RUN set -o nounset                                                        9.8s
 => => # (30/43) Installing gdbm (1.19-r0)
 => => # (31/43) Installing libsasl (2.1.27-r10)
 => => # (32/43) Installing libldap (2.4.57-r1)
 => => # (33/43) Installing npth (1.6-r0)
 => => # (34/43) Installing sqlite-libs (3.34.1-r0)
 => => # (35/43) Installing gnupg (2.2.27-r0)
 => [linux/arm64 3/5] RUN set -o nounset                                                        9.8s
 => => # (7/23) Installing libatomic (10.2.1_pre1-r3)
 => => # (8/23) Installing libgphobos (10.2.1_pre1-r3)
 => => # (9/23) Installing isl22 (0.22-r0)
 => => # (10/23) Installing mpfr4 (4.1.0-r0)
 => => # (11/23) Installing mpc1 (1.2.0-r0)
 => => # (12/23) Installing gcc (10.2.1_pre1-r3)

There are a couple aspects of the above output, I’d like to highlight for you. The first is the first line where you see: Building 15.8s (11/17). This output gives you total time, in seconds, of the build and will keep updating in real time until the build is complete. The last number, 11/17, lets you know how many steps (11) of the entire process (17) are complete.

The last portion of the above output focuses on real time build output for all architectures:

[linux/amd64 3/5]
[linux/arm64 3/5]

These platforms are built in parallel but, since I’m on an Apple Silicon machine, the ARM 64 build will finish first while the AMD 64 build will take longer. Once the build finishes, you might be eager to use your newly built image but find they’re not listed via the following command:

docker images

This lack of image information means you have to inform Docker to either load the image for local use or push to the Docker Registry, which is definitely different behavior from what you might be used to when building for a single platform only. I first build for my local platform for exploration and testing purposes:

docker build --load --tag bkuhlmann/alpine-base:latest .

Then, when ready to release for public consumption, I’ll use the following:

docker build --platform linux/arm64,linux/amd64 --tag bkuhlmann/alpine-base:latest --push .

With the first example, the trick is to use --load to immediately load your newly built image for local use. However, when deploying for public use, you’ll want to specify all platforms your image supports (i.e. --platform) and use --push to push to the Docker Registry.

I’m unaware of a way to build once for local use and multiple platforms via a single command. You have to build for local use and then build again for release/deployment purposes. Luckily, if you haven’t made any further changes to your Dockerfile, releasing will only consist of building the corresponding images for platforms which are not your current platform.

Workflow

Putting this all together, here is the workflow I’ve settled on so far:

# Build
docker build --load --tag bkuhlmann/alpine-base:latest .
noti --title "Alchemists Docker Built: alpine-base:latest"

# Test
docker run --disable-content-trust --pull never --interactive --tty --rm bkuhlmann/alpine-base:latest bash

# Release
docker build --platform linux/amd64,linux/arm64 --tag bkuhlmann/alpine-base:latest --push .
noti --title "Alchemists Docker Released: alpine-base:latest"

💡 Noti is one of my recommended Homebrew Formulas which is handy for being notified of long running build/release processes when they are finished.

Examples

Should you need further working examples of everything I’ve discussed thus far, I’d recommend checking out these projects:

For the resulting images, you can visit Docker Hub and study the multiple platforms supported per image:

  • Docker Alpine Base - Currently at 0.1.1.

  • Docker Alpine Ruby - Currently at 0.2.1 but you can see where 0.1.0 only supported a single platform prior to the new multi-platform support added in 0.2.1.

Conclusion

The release of RC2 is a great leap forward for Docker engineers who also use Apple Silicon machines. I’m quite happy I can now build all of my images for multiple platforms with minimal effort. 🎉