How to Build Multi-Arch Docker Image on NixOS
With the release of Apple silicon, the adoption of ARM-based CPUs is surging. Many developers are facing a new challenge: compiling their software for ARM-based CPUs. And it’s not just about the binary – Docker images are also getting a multi-architecture makeover.
If you search build multi arch docker image
on Google, most of the articles will reference using buildx. While this tool makes building multi-architecture images much easier, I’m always curious to explore alternative solutions, especially when I was told it is the ONLY solution. Since I’m running NixOS, I decided to dive deeper and learn what I can do with it.However, like many things in nix, while it is very powerful, it’s often difficult to find good examples that can be used straight away. Even when tools or solutions are available, there is often little documentation explaining HOW to use them.
In today’s post, I will show you how to use a single flake to build both x86_64 and arm64 images.
Here are some resources that I encourage you to check out to understand why we use Nix to build docker images and what is available.
- Nix is a better Docker image builder than Docker’s image builder by Xe
- Building and running Docker images
- pkgs.dockerTools
Let’s get started.
IMPORTANT: dockerTools only works on NixOS. [source]
Cross compilation
If you are running NixOS on a x86_64-linux and want to build amd64 images, we need to add below config to the nixos configuration.nix.
This option will also be useful if you use colmena to build and deploy derivations to the hosts running different architectures.
1 | boot.binfmt.emulatedSystems = [ "aarch64-linux" ]; |
Get template
I’ve created a template to help you get started. You can generate this flake.nix with the following command:
1 | nix flake init -t github:liyangau/flake-templates#docker |
The main part of this flake is shown below, taken from the examples.
1 | pkgs.dockerTools.buildImage { |
We will modify the flake to build different images in the next few sections.
Images
Interactive container
buildImage
If you are fine with the default image name and tag, you can directly use the flake template to build an image. This template creates an image with pkgs.bashInteractive installed. Simply run nix build .#amd64
or nix build .#arm64
(if your CPU is ARM), and you should get a result
in the same folder.
To check your current system, run
nix run github:nix-systems/current-system
.
Load the result into Docker with docker load < ./result
and then run the image with docker run --rm -it example:latest-<arm64 or amd64> bash
.
1 | [fomm@nixos:~]$ docker run --rm -it example:latest-arm64 bash |
If you want bash
to run automatically when container starts, simply add the following configuration under the copyToRoot section. config is used to specify the configuration of the containers that will be started off the built image in Docker. The available options are listed in the Docker Image Specification v1.2.0.
1 | config = { |
buildNixShellImage
As we can see the buildImage
function is similar to using a Dockerfile, where you need to specify what packages to be installed and the entrypoint. If you already use Nix to set up your dev environmet, you are likely to have a flakes for your dev enviromnent. To package that environment in docker image, you can reuse your flake and build the image with buildNixShellImage
.
For more information on using Nix to set up dev environment, please check out my blog post.
Let me use the example from my flake shell template.
1 | default = pkgs.mkShellNoCC { |
We can build our new image as shown below.
1 | pkgs.dockerTools.buildNixShellImage { |
After loading the result and running it, we should get a nix-shell
with hello
installed.
1 | [fomm@nixos:~]$ docker run --rm -it example:latest-arm64 |
CLI container
If your application is a CLI tool, you can build a docker image for the tool without an OS or interative shell.
Package available on Nix
Let’s start with something simple - a CLI tool that is already available on Nix. Here we use the neo-cowsay package.
1 | pkgs.dockerTools.buildImage { |
After loading the result and running it, we can use the cowsay
command with the new image.
1 | [fomm@nixos:~]$ docker run --rm -it example:latest-arm64 hello world --random |
Package not available on Nix
Next, let’s try to create a package that is not available on Nix. We’ll use the case-cli as an example.
Firstly, download the default.nix
from here.
Next, use the pkgs.callPackage to bring this package into our flake.
1 | let |
The build should complete successfully, but we’ll get some errors. To resolve these issues, add pkgs.genconf
and pkgs.bash
in copyToRoot.paths
. After making these changes, the container should be able to run the case-cli
tool successfully. As you may have noticed, case-cli
is a Node.js application that requires Node.js to be present in the environment at runtime. With Nix, we don’t need to worry about setting up the runtime environment because Nix takes care of it.
Layer image
buildLayeredImage
All the examples we’ve covered so far use buildImage
, which package everything into a single layer. You can also use buildLayeredImage to build your docker images which separate layers for each nix store path.
Let’s reuse the previous example and use buildLayeredImage
instead.
1 | let |
When we load this new image to docker, we can see multiple layers.
Multiple buildImage
We can also use create layers with buildImage
. Below is an example where I define a base image layer bash and build another layer on top of it.
1 | let |
When we import this image, we can see there are two layers.
Use image from Dockerhub
Now that we know we can build image based on another image, is there a way to build ontop of an image on DockerHub? The answer is yes. Let’s say I want to use alpine:3.20.2 as my based, I can use a tool called nix-prefetch-docker
to prepare the the image data for us.
1 | [fomm@nixos:~]$ nix run nixpkgs#nix-prefetch-docker -- alpine --image-tag 3.20.2 |
Then we can use the data as below.
1 | let |
Once we load and run this image, we can see the OS is alpine.
1 | bash-5.2# cat /etc/os-release |
Create multi arch image
Now we understand how to build docker images, let’s explore how to create multi arch image. To simply the process, you can simply type nix run
in the same folder as your flake.nix
, which will build two results for you. This is handled by below section of the flake.nix.
1 | apps = forEachSupportedSystem (system: { |
Push image
${system} is the result of
nix run github:nix-systems/current-system
${imageName} and ${imageTag} are defined on the flake.nix.
Please subsitute these variables accordingly or put these commands on the flake under apps section above.
After the images are build and fully tested, time to push the images to a image registry. For Dockerhub, you can simply load the image to docker and push them to DockerHub.
1 | docker load < result-${system}-amd64 |
Now, these two images should be available on DockerHub separately.
Tag multi arch
To create 1 tag to servces both type of images, we need to use docker manifest
to create a new manifest and associate these two images with the manifest.
1 | docker manifest create ${imageName}:${imageTag} ${imageName}:${imageTag}-amd64 ${imageName}:${imageTag}-arm64 |
If you want to use your latest tag as well, just create a manifest for it.
1 | docker manifest create ${imageName}:latest ${imageName}:${imageTag}-amd64 ${imageName}:${imageTag}-arm64 |
The last step is to simply push the manifest.
1 | docker manifest push ${imageName}:${imageTag} |
Summary
Nix is clearly a great tool. One of its streight is flexibility, but unfortunately, complexity often comes with it. One of the most challenging aspects of using Nix is that it is not opinionated—there are many ways to achieve the same outcome. As a result, when searching for answers, we often encounter different solutions, making it difficult to determine which one to use. My template might be considered incorrect by advanced users, but it serves as a starting point for new users like myself to begin using Nix to solve real-world problems. I really hope to see more advanced users create examples that new users can apply immediately. It’s okay to be opinionated when we want to encourage more users to try Nix. Nix can only thrive when new users experience more success than frustration. Its growth also depends on the community sharing their use cases and ensuring there are resources for others to follow.
That’s all I wanted to share with you today, see you in the next one!