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.

0
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.