Docker performance on Apple MacBook Pro with M1 Max processor – status and tips
No doubt about it, the 2021 Macbook (Pro) with the M1 (Max) processor is a powerful, fast, silent and „cool“ workhorse – and although it has lots of power I don’t think you will hear or feel it cool down very often, as opposed to its predecessors. And you will be really happy about its battery endurance, too.
The power and also the low engery consumption is mostly a result of the new „M1“ processor architecture, which is based on the ARM platform. Previous MacBooks were based on Intel / AMD architectures.
But – no light without shadows … of course, that architecture switch is a huge step and so there are some things to consider before jumping on the train. While most of your daily usage software should run fine on the M1, there are some apps that may give you headaches. Since we are mostly using our MacBooks to develop software and we are utilizing Docker a lot for our local development, let’s have a look at the current state of Docker on M1 MacBooks, shall we?
Docker on MAC – history and status
Granted, MacBooks and Docker have never been best friends in the past, since the performance was always behind Docker on Linux or even Docker on Windows (now that they have their Linux subsystem), but the current state of Docker on M1 is currently worse than on „older“ MacBooks running on Intel.
Sure, Docker has been ported and officially released for the M1 some time ago and many of your Docker projects even may be running without any changes … but, depending on the setup and scale of the project, the performance on your new, shiny, fast, powerful and expensive MacBook may be horrible – even compared to your older MacBook, which may have an older processor, less RAM etc.! So, why is that and, more importantly, (how) can we fix that?!
I am not 100% sure about the „why“, but I will give you some hope for the future of Docker on M1 and also show you some ways to improve the performance to get at least the performance you were used to with Docker on Intel machines – or even better :)
We have identified two main problems with performance (which weren’t surprising at all when you think about it) when trying out „old“ Docker project setups on the new M1 machines in Docker – first, file I/O – which has always been a big problem with Docker on MacOS and second, poor performance with Docker images based on AMD / Intel architecture.
An old friend – File I/O
The first problem really is hard, since it has been a big headache right from the beginning with running Docker on MacOS, but currently it seems to be even worse than before on M1 systems. As soon as you mount directories with more than a few files as volumes into your Docker containers, performance really suffers.
There have been different workarounds and tools to ease the pain, e.g. we are using Docker Sync since years for PHP projects, especially Shopware projects. We also tried complicated setups where only multiple small folders where mounted into the container. Other solutions utilize nfs mounts, mutagen or even running Docker on linux VMs on the MacBook and SFTP’ing files into and out of the VM from the IDE.
Docker has also tried different optimizations for MacOS, e.g. there is a switch to „use gRPC FUSE for file sharing“ in the general preferences, and lately there is another „experimental“ switch to „use the new Virtualization framework“ on MacOS. Those may help a bit, but the difference hasn’t really been significant, at least to us and when used on their own, withour other optimizations.
But, fear not – there is a real game changer around the corner: MacOS has added support for VirtioVS, a file system for virtualization, also based on FUSE. And Docker already has an experimental build in which you can activate VirtioVS support which will hopefully land in official builds in early 2022!
We tried it on our M1 MacBooks and this time it really makes a difference – volume mounts and file based operations are much faster than before, roughly around factors 2 to 10, depending on the usecase and project. It helped a lot with overall Apache / PHP performance and also for importing large MySQL dumps.
It should also be a big help for Intel based Macbooks, but we haven’t really tried there yet. But for M1, you get a real performance boost and a solution for problem 1.
Imaging the obvious
The second problem is easier to solve – you just have to make sure that all your image layers are specifically built for the ARM architecture. It is not enough to just tag your top layer as „platform: linux/arm64“ – you really have to make sure that all the base images you are using are built for ARM, too! Otherwise, the Docker Dashboard may display your image as ARM and tell you that everything is okay, but it’s not! :)
You can see that, if you are connecting to your container e.g. via bash and run e.g. „htop“ in it. You will see that all the processes in the container will be emulated via „qemu“ and therefore be a lot slower than any native process:
If you use an ARM64 base image, e.g. „arm64v8/php:7.4-apache“ for PHP and build on that, there shouldn’t be „qemu“ anywhere in „htop“ and everything should run native in the container. This makes a huge difference in CPU intensive tasks, e.g. when generating images or compiling files.
For comparison: it took 35 seconds to „compile themes“ in a Shopware 6 project when running on a „hybrid“ AMD / ARM image with „qemu“ and only 3 seconds with the „real“ native ARM64 image! This is how it should look in „htop“ in your „native“ container:
You should add a „platform“ tag to your images in docker-compose.yml, e.g. „platform: ‚linux/arm64′“ for your web container. Interestingly, there seems to be no official MySQL image for ARM64 yet, so we are using MariaDB ARM64 images for now. You can use the AMD based MySQL image for M1, too, but you have to tag it as a certain platform to get it running: „platform: ‚linux/x86_64′“. Then you can use „docker-compose build …“ to build the image and use the MySQL container on M1 – it will be marked as „potentially slow“ in your Docker Dashboard, but it should run.
For Docker Compose, we are simply overwriting the relevant image variables with an additional docker-compose-arm64.yml file, which we are adding with the „-f“ parameter when executed on M1/ARM64:
In a Makefile, that looks something like this:
up: checktraefikifeq ($(shell uname -m),arm64)docker-compose -f docker-compose.yml -f docker-compose-arm64.yml up -delsedocker-compose -f docker-compose.yml up -dendif