Adventures in Building Multi-Arch Docker Images with Chrome/Chromium
Docker is great – run the same code on all machines, no setup needed. So they say. But once you go beyond simple Dockerfiles, things start to change.
We recently had the, uhm, pleasure of trying to dockerize Chrome, ideally with support for DRM content. We thought it would be easy — grab the latest version from Google, unpack it, and done. But it turns out there’s a little more to it than that. Especially when you build for multiple architectures. Because now, there’s not just amd64, there is also arm64 (for Apple Silicon Macs) and armv7l, which Raspberry Pi uses (sometimes called armhf).
In this post, we are going to explore what is needed to create a multi-arch Docker image with Chrome (or Chromium). If you want the TL;DR version: Use Chromium and Debian, which makes it possible to build multiarch images.
Why can’t you just run one image across multiple architectures?
One image to run on every platform – that would be lovely. But things don’t work that way. Each architecture has different instruction sets, so the code has to be compiled specifically for that architecture. In other words, different builds for different architectures. But even that is not enough, because each Linux distribution has different (often incompatible) libraries. So you need different builds for different distributions as well.
You can run an amd64 image on a Mac with Apple Silicon, but it will be using QEMU to emulate the other architecture. And this is slow. Also, some things simply don’t work on QEMU, as documented here and here. In particular. Google Chrome for Linux amd64 does not run with macOS’ QEMU.
Docker luckily offers a way to build for multiple architectures at the same time. Using the
buildx feature, you can specify multiple platforms in your build command, and Docker will create separate images for each, and even combine them into one parent image, so that whever you pull the image, the correct architecture is chones.
To find out more about how to build multiarch images, you should check out the buildx documentation. Our basic setup for building a multiarch image from the same Dockerfile looks like this — simply run this in the shell:
docker buildx create --name mybuilder --driver docker-container --bootstrap --use docker buildx build \ --platform "linux/amd64,linux/arm64,linux/arm/v7" \ -t myimage:latest \ .
The above builds three images and merges them into one. To actually use these images, you would need to “load” them from the build container into your local system, but this does not work when building multiple architectures at the same time. So, for development purposes, we’ve decided to not use the buildx feature at all and simply build the Dockerfile directly, as that will be faster.
If you want to actually deploy the multiarch containers, you need to use the
--push flag to push them to a remote registry. Examples of this are shown in this blog article.
The problem with Google Chrome
Now, let’s make a Dockerfile that works on all above architectures and runs Chrome. Well, ideally, we’d install Google Chrome directly using a
.deb file. This contains all the components we need, including DRM support for video playback on various sites (like Netflix).
However, Google, for whatever reason, has not yet published a Linux Chrome build for architectures other than amd64. There is one for macOS, but that does not help when running Docker. This means we cannot use the same Docker image for arm64 and armhf. We’ve spent a lot of time looking around the Internet, but we couldn’t find anything. Ultimately, it’s up to Google to release this eventually.
Chromium to the rescue
So, no Chrome for non-arm64 architectures. Instead, we have to use Chromium, the open-source build that does not include all the Google features (which we don’t need anyway). Luckily, Debian has Chromium builds for all supported architectures.
So, if you are using Debian, the basic installation would work like this:
FROM debian:bullseye ENV DEBIAN_FRONTEND noninteractive RUN apt-get update -qq \ && apt-get install -y --no-install-recommends \ chromium \ chromium-driver \ && apt-get clean -y && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/*
Note that we’ve added ChromeDriver support, as we need this internally. Also, we’re properly cleaning up the image after the installation to reduce the space needed — which is a lot anyway, because Chromium pulls in a few dependent packages.
This gets the job done, and now we need some kind of display for Chromium. We can do this two ways: 1) run Chromium in headless mode, or 2) run a virtual display like Xvfb. The latter will simulate a display for Chrome to use, which is often better than running in headless mode, since headless mode does not support certain features. We’ll save this for a later post maybe, but simply speaking, you can run:
apt-get install -y xvfb xvfb-run chromium --no-sandbox --disable-dev-shm-usage ...
We’ve not gone further into what Chromium options should be used here — this is a bit complex, and we’ll cover this in a future post.
In any case, this build unfortunately does not include DRM support either. This is because the DRM module which we need (Widevine CDM) is proprietary and not availble for all architectures.
Widevine is what most popular services use to protect content that they stream (Netflix, Spotify, …). It is proprietary; all you can do is download a precompiled library. There is no source code available. This also means that nobody can even provide builds for other architectures.
Google has decided not to publish Widevine for non-amd64 machines. This has also been noted here. There is one workaround that people have exploited to get Widevine running on armv7l devices (e.g., Raspberry Pis). ChromeOS, which is a 32-bit system, ships a precompiled Widevine library for armv7l, and when you extract this library from the image, you can use it on a Raspberry Pi.
So, courtesy of one hacker, we at least have a way to install Widevine on a Raspberry Pi. If you are using Raspbian — a Debian derivative —, you can simply run:
apt install libwidevinecdm0
If you are using Debian, you don’t have this package in your package repositories. But, no problem, you can download the latest
.deb file from this link. Then, you can simply run dpkg -i libwidevinecdm0_4.10.2252.0-1_armhf.deb, and you’re done. This also works from within a Dockerfile.
If you want a ready-to-use image that works for most of your daily needs, e.g. when automating Chrome, you can check out the alpine-chrome images. These images come in multiple variants that can be used for various purposes, like extension testing, screenshotting, running automation tests with Puppeteer, etc. But: these images only work on amd64 architectures. If you use arm64 or armhf, you’re out of luck.
If you are building your own image to do other things, and you don’t need the alpine-chrome images, you can of course just install the Google Chrome build from the
.deb file that Google offers. This, again, will only work for amd64. Check out this Stack Overflow post for more info on how to do that.
At the moment, there is no solution if you want a multiarch image for arm64, amd64, and armv7l, if you want to use Chrome. You can use Debian and Chromium if you do not need DRM. And if you are on a Raspberry Pi, you can use the Widevine package from its repositories.
Let’s hope that eventually Google will provide Linux arm64 builds for Chrome and Widevine, which should resolve this issues. I wouldn’t bet on it though. Until then, you are stuck with running on good old amd64 hardware.