BIRKEY CONSULTING

ABOUT  RSS  ARCHIVE


15 Apr 2025

Nix: Better way for fun and profit

Nix is started in 2003 as a research project aimed to solve the problem of reliable software deployment. The PhD thesis titled The Purely Functional Software Deployment Model proposed a novel way of building software where the final artifact is purely dependent on the inputs to the build system, which is a pure function in a mathematical sense. Regardless of where you are in your nix journey, I can't recommend this paper (thesis) enough. It is very approachable and worth a read so you learn from first principle of what, why and how about Nix.

Nix is a software build and management system that can replace traditional package managers, build environments and configuration tools. Due to the inherent complexity of the problem domain nix is designed to solve and its long history, it has pretty steep learning curve but not unsurmountable. One of the common point of confusions is how the term `Nix` is used in documentations, tutorials and blogosphere. So let me clarify few terminologies that often gets overloaded.

After few false starts and restarts, below are what I believe to be better ways for getting started, using nix for fun and profit.

Installation

I have a following bash script to install a specific version so I can have control over which version to install, what features enable and disable.

#!/usr/bin/env bash
set -Eeuo pipefail

VERSION='2.28.1' # replace it with the latest version
URL="https://releases.nixos.org/nix/nix-${VERSION}/install"
MY_CONF="$HOME/.dotfiles/nix/nix.conf"
sh <(curl --location "${URL}") \
     --daemon \
     --no-channel-add \
     --nix-extra-conf-file ${MY_CONF}
# conf file has this content
experimental-features = nix-command flakes

The `–no-channel-add` and the extra conf file needs some explanation. Nix called a remote url a channel that gets automatically installed, where nix uses to retrieve package definitions (Nix DSL) to manage packages. It introduces a state, which is currently installed channel url that is outside of Nix DSL, thus defeating the purpose of reproducibility. It is considered legacy feature and not needed by flakes, an experimental feature already widely adopted by the community. So I highly recommend enabling flakes and additional commands to interact with it.

Using for fun and sanity

Every project depends on existing software that is beyond your control. Nix DSL enables you to declaratively specify your projects dependencies, a repo or a tar-ball down to the file digest of its content, which is what gives nix superpowers of being a deterministic and reproducible package manager. This means that if your inputs stays the same, nix guarantees that it produces the exact same output regardless of when and where. Below is a flake that pulls in latest version of Clojure into your project.

{
  # optional attribute
  description = "My awesome Clojure/ClojureScript project";

  # required attribute
  inputs = {
    # nix dsl fns useful for writing flakes
    flake-utils.url = "github:numtide/flake-utils/v1.0.0";
    # Pins state of the packages to a specific commit sha
    pinnedPkgs.url = "github:NixOS/nixpkgs/c46290747b2aaf090f48a478270feb858837bf11";
  };

  # required attribute
  outputs = { self, flake-utils, pinnedPkgs }@inputs :
  flake-utils.lib.eachDefaultSystem (system:
  let pinnedSysPkgs = inputs.pinnedPkgs.legacyPackages.${system};
  in
  {
    devShells.default = pinnedSysPkgs.mkShell {
      packages = [
        pinnedSysPkgs.clojure
      ];

      # commands to run in the development interactive shell
      shellHook = ''
        echo To get Clojure REPL, Run:
        echo clojure
        echo To get ClojureScript REPL, Run:
        echo clj -Sdeps \'{:deps {org.clojure/clojurescript {:mvn/version "1.11.132"}}}\' -M -m cljs.main --repl
      '';
    };
    packages = {
      docker = pinnedSysPkgs.dockerTools.buildLayeredImage {
        name = "My awesome Clj docker image built by nix";
        tag = "latest";
        contents = [pinnedSysPkgs.clojure];
      };
    };
  });
}

Do not worry too much about not understanding above nix dsl code. The most important thing to know is that it is nix dsl referred to as a flake that specifies its inputs and outputs declaratively. Save above code as `flake.nix`, which is a convention, then run `nix develop` to get an interactive shell with Clojure in your path. Nix can do way more than this. However, I recommend you just start with solving project dependencies problem. Above flake gives you following benefits:

  • Ability to pin the exact versions of your project dependencies.
  • Cross platform development environment that works both in MacOS and various flavors of Linux.
  • Determinate and reproducible development environment that eliminates "it works on my machine" tooling issues.

One important thing to notice here is the way I chose to reference the url inputs of the flake. I deliberately used tags or commit sha to prevent the state of the urls (thus the state of the nix DSL) change under me, which defeats the purpose of having a determinate and reproducible way to get a development environment. I have following bash script that prints available tags and corresponding commit hash:

 git_tag_sha () {
   repo="$1"
   echo "********************************************************"
   echo "Available release and commit sha for pinning are:"
   echo "********************************************************"
   printf "\033[1m%-12s %s\033[0m\n" "release" "commit sha"
   curl -s https://github.com/$repo/tags | grep -oP 'href="\K[^"]*(releases/tag|nixpkgs/commit)[^"]*' | awk -F '/' 'NR%2{tag=$NF; next} {printf "%-12s %s\n", tag, $NF}'
   echo
   echo "****************************************************************************"
   echo "Please replace the commit sha of following line to pin pkgs to a commit sha: "
   echo "pinnedPkgs.url = github:$repo/<commit>"
   echo "****************************************************************************"
   echo
}
# You can run it like this:
 git_tag_sha "NixOS/nixpkgs"

Profiting in CI/CD and production

This is probably one of the most frictionless and rewarding outcome of using nix. Nix is designed to solve the problem of software deployment after all but the wholesale adoption in production might prove to be too much for the final gain. To spare yourself countless hours of frustration, I highly recommend you start with using it to build docker image if you happened to use docker and Kubernetes. Nix has superb built-in support for making the smallest possible docker image otherwise impossible. Above flake already includes `docker` image as one of its packages output. Here is how you build and load the docker image:

nix build .#docker # the image will be in ./result
docker load < ./result # to get it ready to be deployed

It is a declarative way (using the power of Nix DSL compared to using series commands in YAML file) to deterministically reproduce layered Docker image that saves time and money in your DevOps journey. Have fun and enjoy!

Tags: nix