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.
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!
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?
NixOS
— Sasha Savage (@TauOns) September 23, 2022
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".
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.
(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:
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
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.
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
Stop calling everything #Nixhttps://t.co/0Habs0ReYo
— Gabriella Gonzalez (@GabriellaG439) August 29, 2022
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.
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.
- Easy to learn but limited
- Easy to learn, and powerful
- Hard to learn, and bad
- Hard to learn, but worth it
Here it is in handy graph form.
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.