Docker on Apple M1 Max tips

28. Dezember 2021

 

Docker performance on Apple MacBook Pro with M1 Max processor – status and tips

Architecture Switch

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 :)

Two problems

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.

Docker Preferences

VirtioFS to the rescue?

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.

You can read about VirtioFS support in this very, very long thread and download the experimental Docker build here.

Docker experimental settings

„write through“, Scotty!

Another recent „game-changer setting“ in addition to using VirtioFS on recent MacOS versions (currently 12.2 or 12.3 beta), especially for file / streaming operations like importing database dumps  that popped up in that Docker issue thread is to enable „write through“ (instead of the default „write back“) in the default MacOS Docker Desktop Moby image, see https://github.com/docker/roadmap/issues/7#issuecomment-1045214972. A one-liner to change the setting is:

docker run -it –rm –privileged –pid=host alpine:edge nsenter -t 1 -m -u -n -i bash -c ‚echo „write through“ > /sys/class/block/vda/queue/write_cache‘

But be aware that this setting is currently not persistent, you have to re-enable it every time Docker Desktop is restarted!

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:

Docker htop qemu

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:

Docker htop native

 

Docker BuildKit – building multi-platform Docker images

With Docker BuildKit you can build multi-platform images, e.g. for AMD and ARM64, see https://docs.docker.com/desktop/multi-arch/.

For PHP images, we are mostly relying on the Webdevops images. There is currently a pending merge request to support multi-platform images which will hopefully be merged soon.

To force Docker to pull and run a multi-platform built image for the correct platform of your machine, you can add e.g. „–platform linux/arm64/v8“ to the run command:

docker run -it –platform linux/arm64/v8 –entrypoint /bin/sh formatdgmbh/webdevops-php-apache-dev:7.4

Some sidenotes

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:

Docker Compose Arm64

In a Makefile, that looks something like this:

up: checktraefik
ifeq ($(shell uname -m),arm64)
docker-compose -f docker-compose.yml -f docker-compose-arm64.yml up -d
else
docker-compose -f docker-compose.yml up -d
endif