When I first started using Nix in the early 2010’s I thought it was amazing. I had been using Arch and pacman, writing PKGBUILDs and submitting AUR packages before they switched to using git. I was making the switch from Bash to Zsh, learning about shell completions and debugging random C code because my dwm crashed again. My goal at the time was to start writing code, but one thing that kept hindering me every time was the coding environments. While languages like Rust and Go have their own, first-party build systems, it’s so much more of a wild west with things like Python, even back then.

The first reason that I tried Nix was just to create shells for developing python during the 2-to-3 switchover at the time. After using it I was hooked. It did exactly what I wanted. When I ended up switching back to Arch it was really due to problems with Nix from a decade ago, and now that I have returned I can see how much it has improved.

I won’t be touching on the drama surrounding Nix and the split that went on, I want to just keep this technical.

Centralized

The main reason I came back to nix was the centralization. I had spent years getting my systems set up, making sure everything connected with my system, modifying yaml, toml, json, ini and other crazy formats for configuration. But every single one was so different, not because the underlying language might be different, but everything I wanted to work on had a different subset of features they wanted, or different ways of dealing with paths (absolute, home-relative, config-file relative, etc.), or different formats for colors (hex, integer, or something else), and so much more. And this doesn’t even get me started on services, installations, containers, and more.

Nix simplifies everything for this. Just for a quick and easy example, I have a terminal and I use zellij in that terminal. I want the color scheme to be exactly the same, but if I was going to do that in the .kdl and .toml file for the two components, modify each of their themes and then make sure it’s all equivalent and I didn’t miss a color. Compared to using this in Nix, I can have a centralized colors.nix file that defines my base colors, and even functions to get bright/dim colors and add alpha layers. From there I can create the configurations for both zellij and rio that use my color scheme, and all the changes I want to make happen just in the colors.nix file. Changes here reverberate through my whole system, changing lock screen colors, waybar css, helix theme, and even the TTY’s 16 color theme.

let
  theme = rec {
    primary = {
      background = rec { hex = "000000"; int = hexToInt hex; };
      foreground = rec { hex = "fffbf6"; int = hexToInt hex; };
    };
    # ...
  };
in
{
  # Hexes just gets the hex value and puts a # before it for formatting
  alacritty.color = mapAttrs (_: hexes) theme;
  # ...
}

This is great for a single machine, but the power multiples with each computer that you want to manage. Both my personal and work machines have the same settings and services enabled, with overrides or extras able to be defined on a per-node basis. Even my virtual machines and serve nodes can be set up with all the desktop software removed or only specific services enabled, and I know that my keys are all set up, my terminal will feel right and everything will Just Work. On top of that, if you get them set up correctly, sharing packages between machines is trivial, so you only ever have to build things once.

Consistent

My previous machines used to be set up with some quick scripts or maybe some git clones of a dotfile repo or two with symlinks in a few places. This was fine, but every time I would go on a vacation or get sucked into doing work for a whole week without touching my personal machine, they would get horribly out of sync. And not just in their configuration, but also the package versions. Dealing with that was (at least on Arch) a bit of a nightmare, especially using software that had configuration syntax changes or major version bumps. At any time a pacman -Syu on one machine could break the configs that I had, and unless I did that at the exact same time, it could break again on the other machine if there’s another update.

Using a flake in my /etc/nixos configuration lets me be overly confident that all of my software and configs will work together, since the packages are kept pinned by the flake.lock file. And I’m no longer worried about waiting to update my work laptop, since as longas I pull the same commit from my repository, it will be the same versions all the way down to the kernel.

Customizability

Having the latest software is great, but sometimes I want stuff that’s even more recent, or that does something different. Getting the latest build of a Cargo crate or rebuilding something with a feature changed is so simple for me. And this applies even to things that you would think would cause major issues. I could, with a few simple lines, change my entire system from using glibc to musl, and it would change every package on my system. If I need to change a library to have a new feature, I can change that for every one of the packges I use that depends on it seamlessly. I also have the choice to not do this, and make custom overrides only for specific packages. Overriding something like the rustToolchain causes a lot of applications to rebuild, but if I only need it for some specific package, then I can choose to change it only for that package or set of packages.

“Okay, I get all that, but I like my system.”

Great! There’s nothing wrong with that at all. You can literally use almost all of the stuff I mentioned above on any distribution of Linux or on OSX/Darwin, and if you look at the systems available, you can use this pretty much anywhere. Windows might be the only place it can’t directly work, but with modern WSL it works smoothly there for that side of things.

Right now I use varying levels Nixification on different systems:

  • Personal laptop: 100% NixOS on x86-64
  • Work laptop: 100% NixOS on x86-64
  • Mac Mini M4: nix-darwin with all native aarch64-darwin packages
  • My homelab VM’s: NixOS on all of them, except the Talos cluster
  • My homelab hypervisors: Proxmox with Nix set up using home-manager

So even if you’re on Ubuntu but think “I want to configure part of my system that way”, you can do that! Nix doesn’t have to exist as the only package manager on the machine due to how it is set up with the /nix directory, so if you just want to always make sure you have the correct configuration for your local shell and the shell on your remote dev machine, you can do that with some super easy configuration.

Problems

Nix isn’t perfect. My main gripe is the ease of using external configurations, themes or packages. Luckily there aren’t that may external packages, but to properly get things installed it does take a few minutes to write up a .nix file and include it in my system packages. External configurations and themes are also a pain, though I’ve seen more and more themes and such include a flake.nix now.

There’s also a bit of a problem with the speed. Sometimes I want to make a quick change to something, but it does take a good few seconds to run a nixos-rebuild test, though all the same ways of configuring something do still exist. Sometimes I’ll go and remove the symlinks that nix or home-manager put in my home directory to make changes direclty to the file, especially if the application has the ability to live-reload from that file, but for the most part it’s honestly fine. I find myself making less and less changes as everything solidifies, and when I do make changes I know what I do is going to work.