I've been experiencing with Nix, little by little for some time now, so I thought it's time to summarize my thoughts about it.
I love functional programming, so it was almost inevitable that I would start tinkering with Nix sooner or later. However, the Nix ecosystem is huge, and it tends to be daunting at first.
My first step into the world of Nix was installing the nix package manager on my Mac OS. For a period of time I used it simultaneously with Brew. As a package manager, it works just fine, the commands are a bit strange, and searching for packages tends to be slow. By the way, since my first install, the
nix search solved the slow package lookup problem. At this point, nix is not really so much different, from Brew or MacPorts. Under the hood, nix has an interesting approach: it has a storage folder, where all your installed files will be saved, with a hash which represents the exact version, so multiple versions of the same packages can coexist. These packages will then be linked to you nix profile folder, which is added to the PATH environment variable.
By the way, there is a small glitch when installing Nix on Mac OS Catalina, but here is a solution for it: https://github.com/NixOS/nix/issues/2925#issuecomment-564493970
And here is a useful page for Nix beginners: https://nixos.wiki/wiki/Cheatsheet
The true magic starts with the
nix-shell. With this, you can switch between packages, that are in store.
Earlier, I used nvm, rvm or asdf like tools, and sometimes even Docker to create a separate development environment with different versions of node or ruby, etc. However, there are a few problems with these tools: every language has its own tool, and of course every tool uses different commands and options. Docker is too heavy-weight, it drains the battery, uses too much memory for such a simple task. And all they can do is to set the version for one dependency, but what happens, when you have both Ruby and JS in a project. It's a mess.
One day I was working on a monolithic Ruby project, with several OS level dependencies and JS. It would never behave the way I wanted, I had to debug the build steps from time to time. Then I got fed up, uninstalled all the dependencies, and wrote my first
shell.nix file. Surprisingly, it was rather easy, and it just worked every time I needed it. And the feeling that I don't need to install stuff globally just for that one project is wonderful.
However, it wasn't perfect in from the start... After I updated my nix-channel, the repository for packages, my build wouldn't work anymore. Nix promises reproducible builds, but because the package names in the
shell.nix file doesn't contain version numbers, so these can change and break the build. Tip number 1: always pin your nixpkgs.
I also experienced some problems with reproducibility when I tried to use the same settings on a different device. The thing is,
nix-shell still uses global packages. If you have a library installed globally, your program will see it inside the nix-shell, but of course it won't be there on other computers. So **tip number 2: try running your nix-shell with the --pure flag`. This way, only packages in the shell.nix file will be available.
I realized, that Nix could serve as a full replacement for Docker. I mostly use Docker locally to spin up a DB and run my project in the desired environment. I knew how to do the latter, but I still needed a DB instance, but I didn't want to install one globally. So, my next step was not only installing dependencies with
nix-shell, but also running them using the
shellHook property. And also stopping them, when I exit the shell. As tip number 3:
trap commands are useful!
If you haven't done it already, it's a good time to learn the Nix programming language. The syntax looks strange, but it's not at all difficult: https://nixos.org/nixos/nix-pills/basics-of-language.html
As I dug myself deeper into Nix, it sometimes becomes necessary, to overwrite some packages. I had node.js application, which uses yarn as a package manager and build tool. After I setup the nix-shell, I realized that the node version in yarn is different than the one in npm. The reason is that yarn won't use the global node in the PATH, but the one it is built with - if it wouldn't be like this, the build would not be reproducible. Yarn was using an older version of node. Fortunately, overlays are relatively easy to do, so I could override the build dependency. However, at this point I guess you'll need to understand more about the programming language of Nix. It is not at all complicated, but is a bit different, than what most of us are used to.
The world of Nix is way much more, than I could discover, I am just in the doorway. Deployment using Nix, creating packages, building monorepos, the possibilities are endless. Also the nix infrastructure is still growing, so a lot of new tools are appearing, the best practices are still forming.
If you haven't tried Nix yet, I definitely advise you to give it a go. You don't have to go all in, you can still use it as a simple package manager, and incrementally get swallowed by it.