Using binfmt_misc and docker for multi-platform builds
In this post I’m gonna discuss how to use binfmt_misc
to build and run
non-native docker images that you can build on your PC and deploy to your
target machine like e.g. raspberry pi.
What’s binfmt_misc?
binfmt_misc stands for miscellaneous binary formats and in short, it allows to run non-native binaries (through the help of a format associated interpreter) on the host system just as they were native.
Support
Support in the kernel is available since version 2.1.43 so, literally all
modern systems should support it. On debian, binfmt_misc
is enabled as a module.
|
|
Additionally, you need binfmt_misc
filesystem mounted.
|
|
On debian, this is handled by systemd.
|
|
Examples
Let’s get a better gist of what binfmt_misc
really is, with a couple of examples.
Python bytecode
Let’s start with some basic script compiled to python bytecode.
|
|
This produces:
|
|
Which can be executed using python interpreter
|
|
No surprises at all. Of course, direct execution is impossible as the kernel simply doesn’t know what to do with this format.
|
|
But, python
can be registered as an interpreter for .pyc
files to allow
direct execution. Following the documentation for
binfmt_misc, I’m just
gonna register all files with .pyc
extension to be interpreted with python.
To do this, I need a simple extension matching rule.
|
|
With the above rule, kernel will invoke /usr/bin/python3
when attempting to
directly execute any pyc
file.
|
|
Lua bytecode
Just for fun, I’m gonna register a “magic” matcher for Lua bytecode. Similarly as with python, I’m gonna create a trivial test program:
|
|
With no binfmt_misc
rules, the execution is impossible as expected:
|
|
Magic matching, will attempt to search for a pattern within the binary to choose an interpreter. For that I need a reliable pattern. I’ve found lua52vm bytecode description document, which mentions “Lua signature” to be “1b 4c 75 61”. This seems to match the contents of the binary as inspected by hexdump so I’ll use that. Here’s the rule.
|
|
Non-native binaries
binfmt_misc
really shines when combined with qemu and docker to allow
execution of non-native code from other platforms like e.g. ARM.
First, let’s start with the hello-world
docker image we all know.
|
|
No problems at all. Let’s try to run the same image for linux/arm/v7
(which
is raspberry pi 3).
|
|
No surprises at all. Let’s install qemu and some setup tools first.
|
|
binfmt-support provides some tools to make format registration and management easier.
Installation involves systemd setup. The services are enabled by default and on start-up by default they should register qemu for all supported platform (including ARM - which is of interest here).
|
|
binfmt-support
adds a systemd unit that makes the configuration persistent
across reboots. The config files are stored under /usr/lib/binfmt.d/
and
read on boot.
With the above, it’s now possible to run armv7 binaries directly on our system (through qemu emulation).
|
|
How is that useful?
In some situations, it might unblock the development completely as the target platform may simply be inadequate to build its own code (i.e. not enough RAM, too slow, etc).
Here’s a real life example.
Pydantic project
I’m currently working on a project that uses
pydantic. This package requires
pydantic-core - which requires some
native libraries to be built. My destination platform is raspberry pi 3
(armv7l) - it takes ages to build pydantic-core
on rpi3 - and often it will
just fail due to lack of memory. I’ve been mounting swap files to make the build
work but this is really clunky. Docker multi platform setup solves that
problem completely.
Why not use pre-built binaries?
I’ve run into a situation when some of my dependencies simply don’t provide binaries at all. One of them is markupsafe. As you can see here there’s no pre-built armv7 package.
This is a problem. When using pip
with a custom platform, you have to
explicitly define --only-binary=:all:
or --only-binary=:none
. Here’s a
simple requirements file to prove that.
|
|
Using pip
with a custom platform is therefore not an option as sooner or
later you’ll run into a dependency problem like demonstrated above
(unless you want to run your own python pip repository and host
pre-built packages for all your dependencies).
How to use docker multi platform build?
Let’s start with an example Dockerfile
|
|
I’m using the requirements.txt
which I already discussed, containing pydantic
and markupsafe as dependencies. The hello.py
script is trivial as it’s not
really important here.
|
|
After installing qemu
and setting up binfmt_misc
, the default docker
builder should support emulated platforms as well. This can be checked with
the following command.
|
|
In case your platform is missing, there’s a convenient setup image that will
configure all binfmt_misc
rules:
|
|
I can now build an image for rpi3 on my development machine.
|
|
I can dump this image and transfer to my rpi like so:
|
|
It runs without problems:
|
|
Platform specific stages in Dockerfile
Additionally, stages within Dockerfile can be configured to run on a specific
platform. Here’s a slightly modified Dockerfile
|
|
builder
will run on linux/amd64
. Commands in runner
will execute via
qemu emulation on linux/arm/v7
.
Platforms don’t have to be hard-coded like that. There are two special
variables available $BUILDPLATFORM
and $TARGETPLATFORM
. These will be
populated accordingly when invoking docker build
with a custom platform.
The above example Dockerfile can be modified to take advantage of that.
|
|
… and build:
|
|
More information is available in docker documentation for multi-platform builds.
Docker + Qemu + binfmt_misc disadvantages
There’s mainly one. It’s slow - as it’s effectively like a virtual machine. Each command runs within its own emulated context.