A first look at Nix
At my company we have a hackathon every 6 months where we essentially get to experiment with new technology for two days and then present our findings. This time I decided to dive into Nix and explore it's capabilities for building docker images.
Scope
We're using a Monorepo for our over 60 microserivces written in Go. Since I only had two days my goal was relatively simple, use nix to build one of the services and create a docker image for it. The main requirement was to be able to build the docker image without relying on the docker daemon - since we don't have a docker daemon available in CI either.
Building a go binary with Nix
After doing some research, the state of the art seems to be to create a so called flake for your service. To do so, we add a flake.nix to to the root of our services directory. A flake.nix consists of 3 parts
- description - just metadata
- inputs - the required dependencies
- outputs - what we are going to build
{
description = "This is an awesome flake";
inputs = { };
outputs = { }:
}
The above is a very minimal and very useless flake. When it comes to the inputs, we can stay rather minimal, we only need 2 things
- nixpkgs
- flake-utils
so let's define in them in our flake.nix file
{
description = "awesome service";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
}
Let's not worry about flake-utils to much, this is just a nice-to-have but nixpkgs on the otherhand is gonna be essential for pretty much everything you would want to do with nix, this is the largest collection of software packages that exists today, larger even than the AUR. You can also check it out on GitHub and admire the almost 400.000 Pull Requests that keep all these packages maintained - https://github.com/NixOS/nixpkgs/pulls
Now with that out of the way, let's define our outputs, what we want to build, in this case, a go binary. For that we will use the buildGo123Module which is essentially a Nix wrapper around go version 1.23.
{
description = "consumer-campaign-view service";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
consumer-campaign-view = pkgs.buildGo123Module {
pname = "consumer-campaign-view";
version = "0.1.0";
src = ./src;
# Replace this after first build attempt
vendorHash = null;
subPackages = [ "." ];
ldflags = [
"-s"
"-w"
];
CGO_ENABLED = "0";
doCheck = false;
meta = {
description = "awesome service";
mainProgram = "awesome";
};
};
);
}
most of the options of buildGo123Module should seem familiar if you have been working with Go before. The only odd one here is
vendorHash = null;
This will be the hash of the program we are building - once you build it with
nix build .#
it will fail the first time with something like
error: hash mismatch in fixed-output derivation '/nix/store/...':
specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
got: sha256-Xe1+w4r2KxQg3p8y... (some real hash)
This real hash we then put as the vendorHash.
Building a docker image with Nix
Final thoughts
The worst part about nix, in my short experience of using it, are the error messages. Look at this one
TODO bad error
The relevant part is not at the beginning nor the end, it's in the middle, hidden in between a bunch of gibberish.