A painting of fields filled with tulipsA painting of fields filled with tulips

My Tale of Woe

For most of my computing career, I've existed on a semi-random periodic cycle of OS reinstalls. I'll inevitably download more projects, games, or apps, and fill up my primary SSD. Because SSDs have always been more expensive than spinning disks, and this process started when they first came out, I've had to do it a lot. 120/250/500GB is just not a lot of space anymore.

too much stuff, not enough space

In a previous cycle I learned about ZFS, an advanced filesystem with some very neat features such as easy snapshots, and easy transmission of filesystem volumes over the network. Sign me up! The peace of mind is amazing. Accidentally rm -rf / ? No problem! I can just restore the snapshot from an hour ago. Realize I deleted a file months ago that I now desperately need? Backups are available on the remote host. Everything was peachy.

That is, until this cycle came again, and I tried to replace my 500GB SSD with a 2TB one. I'm still a little fuzzy on the how, but my home directory volume got wiped. I can mount it, load the encryption key, but it was empty! And when I checked the backups, it had stopped uploading them months ago! D'oh!

The classic blunder, not checking your backups!

Thankfully, my data volume was still quite alive and well, and as one does, I went to vent my frustration on Twitter. Twitter, Twitter on the wall, what's the fairest distro of all?

Reading through that thread, I realized I was optimizing for the wrong thing. I had my data volume, which was good, but what if my root os could be stateless, and the instructions to create it saved? What if I had a fully declarative operating system, all rigorously defined and backed up? Enter NixOS.

NixOS: An OS from a Config File

Installation

Installation of NixOS was straightforward, roughly equal with any other graphical linux distro, albeit with fewer options for configuration in the installer itself. Initially the installer refused to run in the live environment, saying it was not connected to the internet. This was caused by the need to enter my WIFI details, but the GUI provided no way to do so. Most distros would have some way to enter details graphically, or a note about how to connect. It was at this point that I found the NixOS manual, and it helpfully suggested I needed to run nmtui in the terminal. I got a bit farther in the Installer GUI before I bailed out. It was a bit basic. These days, I usually like to see different filesystems available for root, and an option to encrypt it. Thankfully it was very easy to bail out of the graphical installer and go to the terminal.

I'm told the graphical installer is brand new in this release, so it's bound to have rough edges that'll improve once it's worked on!

Because I had been running on a ZFS root, I decided to replicate that setup in NixOS, and the documentation for that was painless. All I had to do was to get my SSD's device ID with ls -l /dev/disk/by-id and plug it in.

Configuration

If there's one thing to know, its that almost everything in NixOS is controlled by your configuration.nix file in /etc/nixos and it's support files in that directory. By default, those are configuration.nix and hardware-configuration.nix. When you run through the install process, it'll generate a sample conf file with most of the options commented out, and a hardware-configuration.nix file. I do not recommend editing the hardware file, it is autogenerated. You'll need to familiarize yourself with the available options, enable the ones you want and then save the configuration file. Once saved, you actually regenerate your OS itself with nixos-rebuild switch. If you added new packages, that'll install them. If you enabled a setting or a service that'll start or restart them with your changes. It's both refreshing and drastically different than the experience in Ubuntu or Arch. It reminds me of the pre-systemd days in Arch, where a lot of things were set in your rc.conf but a lot more powerful.

By issuing nixos-rebuild switch, you're creating a NixOS generation, which is a snapshot of your system in that particular state. NixOS makes it easy to rollback to that configuration with nixos-rebuild switch --rollback, and it adds that generation to your GRUB menu. If you happen to make a mistake editing your configuration.nix file and make your system unbootable, one can load the previous configuration in the Grub menu and revert the changes. One gotcha to note is that booting from an old config does not restore your old configuration.nix file or its support files, so if you screw those and you don't know how, you'll have to start again or hope they can be recreated with nixos-generate-config. I highly recommend you commit the files in /etc/nixos to git or keep copies in a safe place. Done properly, you can recreate your exact OS from these files and a NixOS live cd!

There's very little that can't be done from configuration.nix. `You can enable wifi networks, touchpad drivers, sound, printing, a graphics environment and much, much more. The options seem endless, but common operations can be found in the NixOS manual.

Adaptation

If you happened to take a look at my original tweet, you might have noticed that I said I was getting tired of shenanigans. Let's be clear here, NixOS is not "shenanigan free", but most of them are "good shenanigans".

I swear to god I'm going to pistol whip the next guy who says shenanigans!
Shenanigans *ducks*

It's a distro from a config file, with sane defaults, but needs to be optimized for best performance. And because some programs were not designed on or for it, you may have to go through extra hoops to get things installed and configured.

Take Visual Studio Code, for example. It has a package in the repository, but what about its plugins? Well, if it happens to be a package in the NixOS repo, you can include it in the first array below.

Nix code
(vscode-with-extensions.override {
         vscodeExtensions = with vscode-extensions; [
           bbenoist.nix
           ms-python.python
           ms-azuretools.vscode-docker
           ms-vscode-remote.remote-ssh
           github.vscode-pull-request-github
           editorconfig.editorconfig
           matklad.rust-analyzer
           mkhl.direnv
           jock.svg
           usernamehw.errorlens
           vadimcn.vscode-lldb
           bungcip.better-toml
           golang.go
           prisma.prisma
           jdinhlife.gruvbox
           ms-vscode.cpptools
           bierner.emojisense
           svelte.svelte-vscode
           jakebecker.elixir-ls
           denoland.vscode-deno
           graphql.vscode-graphql
           esbenp.prettier-vscode
           dbaeumer.vscode-eslint
           bierner.markdown-emoji
           _2gua.rainbow-brackets
           phoenixframework.phoenix
           mechatroner.rainbow-csv
           catppuccin.catppuccin-vsc
           brettm12345.nixfmt-vscode
           bradlc.vscode-tailwindcss
           shd101wyy.markdown-preview-enhanced
           ms-azuretools.vscode-docker
           justusadam.language-haskell
           xadillax.viml
           ] ++ pkgs.vscode-utils.extensionsFromVscodeMarketplace [
           {
             name = "remote-ssh-edit";
             publisher = "ms-vscode-remote";
             version = "0.84.0";
             sha256 ="33jHWC8K0TWJG54m6FqnYEotKqNxkcd/D14TFz6dgmc=";
           }
        ];
   })

My VsCode configuration, look upon it and weep

But if it's not in the nix repo, then it goes in the second array. Take remote-ssh-edit above. You need its package name(not the pretty shown one), publisher name, version, and sha256 hash. Most of that can be found on the VsCode Marketplace, all except for the hash. For that, you put in a wrong value, try to rebuild your config, and put in the hash that it gives you when you throw an error. A similar thing occurs for neovim, my IDE of choice, but without the marketplace extensions.

Some things are too tricky for a simple NixOS package to install and configure correctly. A good example of this is Steam. One might think that it'd be easy to install, just add the steam package to your list of packages to install in configuration.nix like so:

Nix code
environment.systemPackages = with pkgs; [
...
steam
...
];

For me, this errors out. It turns out that Steam needs extra configuration. It needs a NixOS module. A NixOS module is a file combined by NixOS to produce a system configuration. It has the ability to change parts of your configuration itself. It might use nixOS packages inside it, but it is fundamentally different. To use a module, all you need to do is enable it like so programs.steam.enable = true;, and potentially set some other options inside it

Nix code
programs.steam = {
  enable = true;
    remotePlay.openFirewall = true; # Open ports in the firewall for Steam Remote Play
    dedicatedServer.openFirewall = true; # Open ports in the firewall for Source Dedicated Server
};

You would be forgiven for mixing those up. While you can't easily search for modules themselves on NixOS's website, you can search for Options. Options are provided by Modules, and the providing module is included in the search results. For example, programs.steam.enable = true is an option provided by the steam module. If that option fails you, you can browse/search the nixpkgs/modules repo, where modules are kept, and see if you can find it in there. Keeping this in mind, and reading the Wiki first, can help prevent some headaches.

The Nix Language

I'd be remiss if I didn't mention the Nix language, which is used to write all of the .nix files and most of the Nix tools. It is a special language, used (exclusively?) for Nix and its Nix tools, and it is odd. Lists are space delineated, but attrSets aren't. Functions are created by assigning to a variable. Arguments are passed with spaces and are separated by colons. There's no marking around what are the function inputs and what is the function body. Functions are called by putting the function name followed by its arguments. No static typing or typing of any kind. No mutable state. Implicit returns. And everything needs a semicolon. 100% functional.

Nix code
      supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
      whatIsThis = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);

Can you guess what this does?

In this code block, supportedSystems is a new array that contains two strings. The next line defines the whatIsThis function, it takes f as an argument, and calls nixpkgs.lib.genAttrs with the supportedSystems array and a (system: f system) function (which takes a system argument and calls f with that argument). Did you get it right?

Coming from a Rust, Typescript, C, and Python background, this is uniquely difficult for me to parse. Admittedly I'm not a Haskeller, and I've never used Cue or Scheme, so it's probable that some people might have an easier time of this than me. I find myself wishing Rust was used for everything, or just that a language that is more C-like, but Nix predates Rust, so c'est la vie.

Nomenclature

At this point, you could be forgiven if all the different Nix tools and things are starting to run together. Everything has Nix in the name, and it seems people aren't careful to distinguish them. As Gabriella Gonzalez helpfully points out, Nix is included in the name of a language, a package store, a packaging tool, an OS, and probably more. Check out their blog post for the details. It's an ecosystem of things all somewhat confusingly named, that gives you superpowers.

Next Steps

Once you get all your packages installed, services configured, and settings set, you might think you're done. But you're not. Not really. For you have only scratched the surface of the potential of NixOS.

The sickos meme, with Graham Christensen standing outside in the dark whose shirt says NixOS and they say Yes... HA HA HA... YES!
Graham Christensen approves, probably

Dev Shells

Dev Shells in NixOS are virtual environments, which should be familiar to you if you've ever used venv in Python. By adding a shell.nix file and defining the nix package dependencies, one can very easily create a virtual environment with all the packages and environment variables you need, simply by changing into the folder in a terminal and typing nix-shell. It makes it easy to have different versions of node or rust, python and its packages, set environment variables as needed, and provide a completely isolated and reproducible environment!

direnv

By installing the direnv and nix-direnv packages, and doing some config in the project itself, one can even automatically load a virtual environment whenever you cd into that directory and unload it when you leave! No more fuss! I am in love with the efficiency of this feature!

Flakes

Flakes are a new, unstable feature offering functionality similar to Rust's Cargo.toml and Cargo.lock mechanisms. Flakes can create a Nix package, define a Dev Shell, or even generate a complete NixOS system. They can be passed into nixos-rebuild to build the system with nixos-rebuild switch --flake .. Flake's Flake.lock mechanism ensures your dependencies are consistent, and not dependent on a git hash or the current channel package version. Flake based Dev Shells are faster than their predecessors as well, primarily by caching the nix environment evaluations, and they're natively supported in direnv!

From project specific environments to creating packages in a standardized format to making system installation easier, Flakes represent a major overhaul for the Nix ecosystem. If you're tempted to use them, you should, but it's worth remembering that they are unstable and may change in upcoming releases. It certainly hasn't stopped me or many others from using them!

Deployment

Because it is so easy to define a NixOS system, plenty of people use Nix to handle the configuration of multiple machines or servers. Tools like deploy-rs and nix-deploy make it easy to do that. One can even use it to build Amazon EC2 instances or Docker images!

Home Manager

Once one gets comfortable with Nix, one can use Home Manager to manage the deployment of all the config files in your home directory that aren't being handled by NixOS already. Home Manager also has some packages that further declaratively define your environment. It's worth checking out, but it's warned that it's not for the inexperienced NixOS user due to the possibility of weird errors and the need for manual intervention.

Conclusion

I didn't expect to like NixOS quite as much as I did. I think it's going to remain my Linux distro of choice for a while. For me, tools(and distros) usually fall into one of four categories.

  1. Easy to learn but limited
  2. Easy to learn, and powerful
  3. Hard to learn, and bad
  4. Hard to learn, but worth it

Here it is in handy graph form.

A four quadrant graph. X axis is labeled power and Y axis is labeled Learning Curve. Rust is highest in both power and learning curve, NixOS is a bit less in power and learning curve, Typescript is a bit less in power and learning curve. NixLang is in quadrant II, meaning it is lower in power but still fairly high in Learning Curve. Python is in Quadrant IV, making it low in learning curve but still fairly high in power

NixOS falls solidly into Quadrant I, where things go that are hard to learn but offer real benefits.

Should One Use NixOS and Nix?

It's not for the faint of heart, it requires solid knowledge of Linux fundamentals, learning at least some of an oddly different language to configure it, and adapting your imperatively defined packages into declaratively packaged ones.

I am not sure I would have stuck with it if not for the help of @a_hoverbear, @jakehamiltondev, and @TauOns on Twitter. Huge thanks to them for answering all my questions! It ended up taking about 3 days, but I now have a working NixOS Workstation, with VS Code and Neovim installed, and all the necessary compilers for Rust and Typescript. And plenty of things I could upgrade and change to make things even more powerful.

It's a solid distro for a workstation or server. The benefits of easy reproducibility, stability, seamless creation of virtual dev environments, and simple rollbacks make it a powerful tool that I hope to learn better in the months ahead. If you're a seasoned Linux user and are tempted by the benefits, I'd say it's well worth picking it up and give it a whirl.