Cross-Compiling Haskell under NixOS with Docker


I learned how to cross-compile Haskell projects under NixOS using Docker images for ARM architectures, and how to run them under emulation on x86_64
hosts.
Motivation
I attended the AWS Summit 2025 in Singapore. I enjoyed the event. There were booths from various companies which I found interesting, such as GitLab and ClickHouse. More importantly, I got to meet very interesting people.
Among the booths, there was a particular one that caught my attention: AWS was showcasing their ARM-based Graviton processors. I chatted with the AWS folks, and asked a few questions which I had in mind for quite some time.
I compiled a few of my Haskell projects on my Raspberry Pi 4, which is based on the ARM architecture. I was curious to see how some others would perform on the Graviton processors. I could go and compile them on the Graviton processors, on my Raspberry Pi 4, or rather cross-compile them on my x86_64
workstation.
Cross-Compiling Haskell Projects
Cross-compiling Haskell projects always seemed intimidating to me. I do not know if it is practically possible, either. Even statically linking Haskell binaries is quite a challenge, especially under Nix. Instead, I am currently statically compiling my Haskell projects under a Docker image that is built and published by benz0li:
https://github.com/benz0li/ghc-musl
I am using a script that generates a cabal.project.freeze
from my Nix setup, compiles the project inside a Docker container from the above image, copies the binary to the host, and then compresses it using upx.
You can check the script under my Haskell project template repository.
I knew that benz0li publishes the Docker images for both x86_64
and arm64
architectures. He has even recently published additional images to deal with the GMP licensing restrictions.
So I decided to try running the ARM-based Docker image on my x86_64
host, which I had never tried before. First, I needed to make sure that I can do that. This is the normal invocation of the Docker container:
$ docker run --rm quay.io/benz0li/ghc-musl:9.8.4 uname -a
Linux 8c14a21fc636 6.12.30 #1-NixOS SMP PREEMPT_DYNAMIC Thu May 22 12:29:54 UTC 2025 x86_64 Linux
As expected, the uname -a
command ran inside the container shows that it is running on the x86_64
architecture. Now, we can try to run the ARM-based Docker image:
$ docker run --rm --platform linux/arm64 quay.io/benz0li/ghc-musl:9.8.4 uname -a
# exec /usr/bin/uname: exec format error
Configuring QEMU Support on NixOS
That is expected: We cannot run an ARM-based Docker image on an x86_64
host without some additional setup, in particular, using QEMU.
Most of the tutorials I found online suggested using:
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
... which I decided would not work on my NixOS host. Instead, I used the NixOS option to enable QEMU emulation:
{
boot.binfmt = {
emulatedSystems = [ "aarch64-linux" ];
};
}
This did not work, either. Apparently, Docker needs the static binaries provided by the multiarch/qemu-user-static
image. So I changed my configuration as advised:
{
boot.binfmt = {
emulatedSystems = [ "aarch64-linux" ];
preferStaticEmulators = true; # Make it work with Docker
};
}
Good News
And it worked as such:
$ docker run --rm --platform linux/arm64 quay.io/benz0li/ghc-musl:9.8.4 uname -a
Linux 15afb3b1a45b 6.12.30 #1-NixOS SMP PREEMPT_DYNAMIC Thu May 22 12:29:54 UTC 2025 aarch64 Linux
Now, I could change the script to consume arbitrary arguments and pass them to the docker run
command:
bash build-static.sh --platform=linux/arm64
Honestly, I was not expecting it to work, but it did, although it was noticeably slower! One thing I noticed was being able to run both the x86_64
and arm64
binaries on my x86_64
host, which I was not expecting at all. Apparently, my system is now capable of running both architectures at the same time -- with the latter running under emulation.
You can check the script and adopt it for your own Haskell projects.
Conclusion
It was an interesting day.
Firstly, I ran a non-x86_64
Docker image under emulation on my x86_64
host, which I had never done before. Secondly, now I know that I can cross-compile my Haskell projects for ARM architectures using the arm64
Docker image provided by benz0li. Going forward, I can fearlessly cross-compile my Haskell projects for any supported, non-native architectures.
And I am definitely going to try the Graviton processors, as soon as I spin up a Graviton EC2 instance on AWS.
Subscribe to my newsletter
Read articles from Vehbi Sinan Tunalioglu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Vehbi Sinan Tunalioglu
Vehbi Sinan Tunalioglu
My name is Sinan. I am a computer programmer and a life-style entrepreneur. You can check my LinkedIn and GitHub profile pages for more information, and send an email to vst@vsthost.com to contact me. I am re-publishing my technical blog posts on hashnode. My website is available on thenegation.com, and its source code is available on GitHub.