Docker Builds are slow on M1

This is a neat Docker trick for those who have an ARM development machine (Apple M1), but sometimes need to build x86/amd64 images locally to push up to a registry.

Sure, having a CI/CD platform to do this is probably ideal, but for little programs or just sometimes in general, it may be handy to know this.

If you have a spare Intel machine laying around, you can turn it into a build server. Get something like Ubuntu installed on there and install the latest Docker. On your development machine, copy your publish SSH key over to this build server.

So now you you have your server set up and you want to build your Docker image for an x86/amd64 platform. Typically, you would run a command like this below to target the platform.

docker buildx build -f Dockerfile --platform linux/amd64 .

And this will work, but what you'll notice is that this is an extremely slow process. The Apple Silicon chips are amazing and are the fastest machines I've ever used. However, when emulating the x86 instructions to build a docker image, it takes such a long time. I've seen this take over an hour on larger and more complex projects.

Just as an example. Here we have a very simple Ruby on Rails application that has little moving parts. I'm using things like esbuild and css-bundling, but nothing fancy. On the M1 chip, it took over 250 seconds to build the image.

# Apple Silicon
[+] Building 316.4s (23/23) FINISHED

However, on a AMD 5900X server, I have a Virtual Machine running on there which has Docker installed. Running the exact same project on there took much less time.

# AMD 5900X
[+] Building 62.3s (24/24) FINISHED

So, the main concern here is that I do not want to interrupt my normal process on how I build images or handle things. It would be a pain to push up my code, ssh into the build server, pull it down, and then build the image. This is a lot of steps, but luckily there is a MUCH easier way to do this.

Docker's buildx build command has a flag that we can specify a specific builder.

So, we can create this builder on our local machine. The nice part about this creation is that it is idempotent, so you can run this command many times without changing the result. All we have to do is to create a builder profile and in this case I have named it amd64_builder.  Since this builder is a pool of resources, I give a name to for the VM. I'm also specifying the platform that I'm building against and then pass in the SSH url for my builder machine.

docker buildx create \
  --name amd64_builder \
  --node linux_amd64_builder \
  --platform linux/amd64 \
  ssh://USERNAME@IP_ADDRESS_OF_BUILDER

Now, I can build and push the image to the registry. The magical flag that we'll use is --builder as we can now specify the builder VM. The rest of the buildx command is the same as we would expect.

docker buildx build \
  --builder amd64_builder \
  --tag USERNAME/REPONAME:TAG \
  --push .

In some cases, I've seen this go almost 10x faster than the amd64 emulation on the M1 chip. If you have a spare Intel/AMD machine laying around, this may be a worthwhile adventure.