NixOS: What is it, how does it work, and should you use it?

The NixOS logo.
BlogLink to blog
Blog
11 min read

I talk about my experience using NixOS after two weeks of using it as my primary OS. I'll explain what it is, how it works, and whether you should try it.


Contents


On this week’s episode of “Which meme Linux distro is Aires installing?”, I decided to go all-in on NixOS. And hoo boy, this is an interesting distro. It’s been about two weeks and I still feel like I’m only just starting to grasp it. But what is NixOS? What’s it all about, how’s it work, and why do some techies treat it as the next best thing since containers? I’ll try to cover as much of this as I can, while talking about my own experience, in this blog.

Note: This blog will go into some pretty technical concepts and assumes you have at least a cursory understanding of Linux. If not, you can still read, but this might start looking like hieroglyphics real soon.

What is NixOS? 

NixOS is built on top of Nix, a “purely functional package manager.” What does that mean? Well, there are two key problems Nix solves:

First, dependency hell. Most software requires other software in order to run. These are called dependencies. For instance, if I want to install Firefox in Arch, I also need to install 37 other packages 🔗, and they may need to be specific versions. What if I install another package that uses the same dependencies, but different versions? Or what if I want to install a beta version of Firefox alongside the official version? Nix lets you do that by managing each package + version combination as its own unique thing, and then linking dependencies together.

Second, declarative configuration. What do you normally do after installing a Linux distro? Add some users? Install some packages? Tweak the settings? What if all of this was done for you, before you ever closed the installer? With Nix, you can define the state of your system in text-based configuration files. You then apply these config files and Nix modifies your system state to match the definition.

NixOS takes these concepts and applies them to the entire operating system. It manages everything from the bootloader to the kernel to Systemd services and even your home directory. You can declare user accounts, set passwords and login behaviors, decrypt drives, and so much more. It’s like Ansible built into a Linux distro, or if you’ve played with containers, it’s Kubernetes for an entire PC.

What’s the big deal about NixOS? 

“Ok, but Aires, you devilishly handsome lion you,” you may be thinking, “what makes this so neat?” Well my accurate and perceptive friend, there are a few reasons:

  1. Recoverability. If I ever need to reinstall my OS due to a hardware failure or accidental breakage, I don’t need to try to remember what software I used, or re-configure all of my apps. I just install the base version of NixOS, run my config files, and everything’s right back to the way it was.
  2. Reproducibility. Nix can pin software to specific versions, so even if you hand someone else your config files, they’ll have the exact same setup as you. Flakes—an extension to Nix—takes this one further by tracking the specific versions of any libraries or modules used in your config files. That means—get this—Nix can pin itself to a specific version, so you can rely on its output to always be identical.
  3. Re…uh…visibility? Because everything’s defined in config files, I can get a complete understanding of my system just by reading the config. I can figure out which packages I have installed, what services I have running, which kernel modules I’m using, etc, without having to go digging around a dozen different tools.

These are just my own personal benefits. Other people have different things they get out of NixOS, but I think these cover the broad swath of reasons.

Cool…so how does it work? 

Like I’ve mentioned a hundred times, Nix and NixOS work using config files. These files are written in a language specific to Nix, though it looks very much like JSON. All Nix files have the extension .nix, or at least I recommend using it. A plain NixOS system will have its main config file at /etc/nixos/configuration.nix. For example, here’s what the default config (as of this posting) looks like:

{ config, pkgs, lib, ... }:

{

        # Allow unfree packages
        nixpkgs.config.allowUnfree = true;

        # Some programs need SUID wrappers, can be configured further or are
        # started in user sessions.
        # programs.mtr.enable = true;
        # programs.gnupg.agent = {
        #   enable = true;
        #   enableSSHSupport = true;
        # };

        # Enable the OpenSSH daemon.
        # services.openssh.enable = true;

        # Open ports in the firewall.
        # networking.firewall.allowedTCPPorts = [ ... ];
        # networking.firewall.allowedUDPPorts = [ ... ];
        # Or disable the firewall altogether.
        # networking.firewall.enable = false;

        # This value determines the NixOS release from which the default
        # settings for stateful data, like file locations and database versions
        # on your system were taken. It‘s perfectly fine and recommended to leave
        # this value at the release version of the first install of this system.
        # Before changing this value read the documentation for this option
        # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
        system.stateVersion = "23.11"; # Did you read the comment?

}

To apply the config, use nixos-rebuild. You can choose how and when to apply the changes: immediately, on next boot, dry build, in a virtual machine, so you can preview them, and many more. You have a ton of flexibility.

# Apply the config immediately
sudo nixos-rebuild switch

Each rebuild creates a generation, which you can think of as a change set similar to a Git commit. A generation matches a configuration, with the most recent generation representing your last run of nixos-rebuild. NixOS adds an entry for each generation to your bootloader, so you can boot directly into a previous working generation in case your new one isn’t working out. Isn’t that awesome?

# Get the list of system generations
sudo nix-env --list-generations --profile /nix/var/nix/profiles/system

Generations are independent of each other, so you can poke around in a generation without accidentally breaking your entire system. Check the settings in it, add files, do whatever you want.

Earlier I mentioned that Nix is like Ansible. That’s only partly true. Ansible works by reading your config file(s), then going out and making changes to your system until it matches the target state. It’s cleverly designed to appear idempotent (i.e., you can run it multiple times and it’ll always generate the same result), but under the hood, it’s frantically shuffling things around in just the right way to get the desired result. Nix kinda sorta works the other way around: the system is built up from the configuration. This makes it a lot easier to jump between specific generations, since Nix isn’t moving things around. With Ansible, it’s impossible (as far as I know) to jump between builds—with Nix, it’s part of the design.

Confused yet? Me too! That’s part of what makes this so much fun!

Nix Flakes 

You might note I said “plain NixOS” in the last section. This is because Nix has an extension called Flakes, which add some new functionality and behavior to Nix. Even I don’t know the specific features Flakes add, but the community generally prefers using Flakes, so if you want to reference anyone else’s config files or import extra modules into your code, you’ll most likely want to use Flakes too.

One great thing about Flakes is that it makes it really easy to run your config files from anywhere. Your configuration.nix file will need to be renamed to flake.nix first. After that, you can run the Flake by using:

nixos-rebuild switch --flake .

Flakes add another neat feature: profiles. Profiles let you define different configurations, then specify those configurations when calling nixos-rebuild. As an example, I have two main computers: one is a Lenovo Legion, and one is a Microsoft Surface. I basically have two main profiles: “Legion” and “Surface.” My flake.nix file looks something like this:

{
	description = "Aires' Flake";
	
	inputs = {
		# For SecureBoot support
		nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
		lanzaboote.url = "github:nix-community/lanzaboote";

		# For Flatpak support
		nix-flatpak.url = "github:gmodena/nix-flatpak/?ref=v0.2.0"; # Use github:gmodena/nix-flatpak/?ref=<tag> to change release version, or remove to track main (unstable).

		# Home-manager
		home-manager = {
	      url = "github:nix-community/home-manager/master";
    	  inputs.nixpkgs.follows = "nixpkgs"; # Use system packages list where available
	    };
	};

	outputs = inputs@{ self, nixpkgs, lanzaboote, nix-flatpak, home-manager, ... }: {
		nixosConfigurations = {
			Surface = nixpkgs.lib.nixosSystem {
				system = "x86_64-linux";
				modules = [
					...
					./hosts/Surface/default.nix
				 ];
			};

			Legion = nixpkgs.lib.nixosSystem {
				system = "x86_64-linux";
				modules = [
					./hosts/Legion/default.nix
					...
				 ];
			};
		};
	};
}

There’s quite a bit going on here, so we’ll take it step-by-step.

First, there are three main fields a Flake has: description, inputs, and outputs. description is just a text field explaining the contents of the Flake. inputs lets you define modules and other functions that will be passed on to outputs, which is where the real work happens. Under outputs is a section called nixConfigurations, which is where we define each of our different profiles. You can load in other Nix files under modules, like I’ve shown above. In this case, I’ve given each profile its own file under the hosts/<hostname>/default.nix.

Now, if I want to install NixOS to my Surface, I’ll run:

sudo nixos-rebuild --flake .#Surface

You can, of course, reuse imports between profiles. So instead of having to re-define a bunch of options, I can create a single “workstation.nix” file that defines things like default software, user interface settings, shell settings, and users, and import that into both host-specific files. I love this, because it means I can define my ideal system setup just once and have it automagically apply to all of my systems!

One last thing: Nix will pin the versions of the Flake modules you use. You can see these yourself in the auto-generated flake.lock file. If you’re familiar with package management tools like NPM, this is the same idea. This file doesn’t get updated automatically, so if you want to use the latest and greatest, you’ll want to add --update to your rebuild command:

sudo nixos-rebuild --update --flake .#Surface

Should I use NixOS? 

I won’t lie, I mostly included this section for SEO purposes 😹 I don’t intend to convince you whether or not to use Nix or NixOS. But it’s still a good question to ask. Should you use NixOS, and if so, what would it take to get from a fresh installation to your ideal system?

The honest answer is: ehh, probably not. If you have only one or two computers that you use, you have them set up the way you like, you don’t plan on replacing them soon, and you don’t plan on reinstalling or reformatting your OS, you probably don’t need NixOS. If you’re using a distro like Arch, where you have access to tens of thousands of software packages, you probably don’t need NixOS or Nix.

Where Nix and NixOS shine:

Now, if you still want to try it out, just be aware that:

And most importantly: NixOS is not like any traditional Linux distro. Sure, it runs the Linux kernel, sure it uses systemd and GNU coreutils and all that good stuff, sure it runs applications built for Linux, but the way it does this is almost completely unique to any other distro.

Think about it this way: using a traditional distro is like getting pre-made fast food. You go to the counter, pick from a pre-built list of food, then walk away. Maybe it’s not exactly what you wanted, but it’s fast, easy, and accessible. Using NixOS is like writing your own recipe from scratch. It takes more time, there’s a lot of trial and error, and the meal may come out awful at first, but once you fine-tune the recipe, it comes out delicious every time.

…ok, that was a terrible analogy, but whatever. I’m sticking with it.

Conclusion 

Anyway, that was my rant about NixOS. Despite all the shit I talked about it, I do still recommend checking it out. If you want to try it without nuking your computer, start with Nix, the package manager side of it. You can actually use Nix on any distro as sort of secondary package manager. I also recommend checking out Home Manager 🔗, a Nix module that can manage configuration files in your /home/<user/ directories. I use Home Manager to do things like install and configure the ZSH shell, set up Git, and start Syncthing. Home Manager also slots nicely into your main NixOS configuration, so if you do go all-in with NixOS, you can run it alongside your main system configuration.

Ok that’s it, I swear. This blog post has gone on long enough. But if you found it useful, let me know 🔗!

Previous: "Reflecting on Everything Everywhere All at Once"Next: "Installing a Custom Package With Nix"
atmospheric breaks breakbeat buddhism chicago code disco fiction funk furry house house music kubernetes lgbt linux logseq mastodon mental health movies music nixos obsidian personal philosophy pkm poetry prompt second life social software soul technology writing