I recently switched from an Apple Intel machine to an Apple Silicon machine which caused some complications with Docker. 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. 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 can be helpful.
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:latest .
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 $USER/alpine-base:latest .
Then, when ready to release for public consumption, I’ll use the following:
docker build --platform linux/arm64,linux/amd64 --tag $USER/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 $USER/alpine-base:latest .
noti --title "Alchemists Docker Built: alpine-base:latest"
# Test
docker run --disable-content-trust --pull never --interactive --tty --rm $USER/alpine-base:latest bash
# Release
docker build --platform linux/amd64,linux/arm64 --tag $USER/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:
-
Docker Alpine Base - This is a base image for all Alpine Linux based images from which to build from.
-
Docker Alpine Ruby - This is a Ruby development environment which is currently used by Circle CI for all projects found on this site.
For the resulting images, you can visit Docker Hub and study the multiple platforms supported per image:
Resources
The following are additional resources that might be of help to you when building for multiple platforms.
-
Faster Multi-Platform Builds: Dockerfile Cross-Compilation Guide - Provides tips, techniques, and speed improvements for compiled languages that might need to be built for multiple platforms.
Conclusion
I’m quite happy I can now build all of my images for multiple platforms with minimal effort and hope this is of help to you too. 🎉