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.

Published on 29 May 2025 Technical Notes Docker Hacking Haskell Nix