I fell in love with NixOS
/ 11 min read
Table of Contents
From Arch Linux to NixOS: Why I Switched
For years, Arch Linux was my Linux distribution of choice. It gave me a minimal system, access to almost everything I needed through the package manager and AUR, and—perhaps most importantly—the feeling that I was in control of my computer.
Eventually, though, I started to become interested in a different kind of control.
I didn’t want to spend less time configuring my system. I wanted to spend less time remembering how I had configured it.
That distinction is what eventually led me from Arch Linux to NixOS.
The problem with a traditionally configured Linux system
A conventional Linux installation is largely an accumulation of changes.
You install a package. You edit a configuration file. You enable a service. You change a system setting. You install a plugin. You tweak something in /etc. Six months later, your computer works exactly the way you want—but you may not remember everything you did to make it that way.
This isn’t necessarily a problem. Linux distributions such as Arch make this process relatively transparent, and Arch’s documentation is excellent.
But there is an unavoidable property of this approach: the state of the system is not necessarily represented by a single description of the system.
Your computer is the result of all the things you’ve done to it.
NixOS approaches the problem from almost the opposite direction.
Instead of describing the steps I took to create my system, I can describe what my system should be.
That is what “declarative” means.
What does declarative mean?
There is an important distinction between imperative and declarative configuration.
An imperative approach tells the computer what to do:
Install Firefox.Enable SSH.Create this user.Install these packages.Change this setting.Start this service.A declarative approach describes the desired result:
This system has Firefox installed.SSH is enabled.This user exists.These packages are available.This setting has this value.NixOS then figures out how to get from the current state to that desired state.
My system configuration is therefore not a collection of instructions I’ve executed over the years. It is a description of the system I want.
For example, I can specify packages in my NixOS configuration:
environment.systemPackages = with pkgs; [ git neovim ripgrep fd bat];If I reinstall the machine, I don’t need to remember that I once installed ripgrep, fd, and bat.
They’re in the configuration.
The configuration is effectively the memory of the system.
Rebuilding the system
This becomes particularly useful when making changes.
Instead of manually installing something and changing configuration files, I edit my NixOS configuration and rebuild:
sudo nixos-rebuild switch --flake .The system is changed to match the configuration.
This also changes how I think about experimentation.
If I want to try something, I can change the configuration, rebuild, and see what happens. If I don’t like it, I can change the configuration again—or roll back to an earlier generation.
The system isn’t just a machine that happens to have a particular configuration.
The configuration is the source of truth.
Reproducibility
This is probably the biggest conceptual advantage of Nix.
Suppose I have spent two years configuring a computer. Eventually I buy a new machine.
With a traditional installation, I might start from scratch:
Install Linux → install my programs → copy my dotfiles → remember everything else I changed → discover things I forgot → fix things as I encounter them.
With NixOS, much more of the system can be represented by configuration files.
I can take that configuration and use it to build another machine.
This doesn’t mean that everything magically becomes identical. Hardware-specific configuration, secrets, user data, and other external state still need to be dealt with.
But the important part is that the configuration of the operating system can be expressed as code.
That gives the system a property that is difficult to achieve with traditional configuration:
reproducibility.
If the configuration says that I want a particular set of packages, services, users and system settings, another machine can be built from that same description.
Nix isn’t just NixOS
One of the things that initially confused me about NixOS is that Nix and NixOS are related but aren’t the same thing.
Nix is the package manager and build system.
NixOS is an operating system built around Nix.
And the Nix package manager can be used on systems other than NixOS.
This distinction is important because a lot of the interesting ideas behind Nix don’t actually depend on using NixOS as your operating system.
The Nix package manager provides a way of describing software environments and their dependencies in a reproducible way.
Instead of thinking only in terms of:
apt install ...pacman -S ...you can describe an environment in Nix.
That environment can then be recreated elsewhere.
Nix flakes
This is where Nix flakes become particularly useful.
A flake is essentially a standardized way of describing a Nix project and its inputs.
For example, a project might contain:
my-project/├── flake.nix├── flake.lock└── src/The flake.nix describes the project and its dependencies.
The flake.lock records the exact versions of those dependencies.
That second part is extremely important.
Imagine that my project depends on some Nix package. If I simply say “use the latest version”, then rebuilding the project six months from now could produce a different environment.
The lock file pins the inputs.
So instead of merely saying:
I need this package.
the project can effectively say:
I need this particular version of this particular dependency, from this particular revision.
That makes the environment much more reproducible. Here is a link to my nix flake templates:
Reproducible development environments
This is one of my favourite uses of Nix.
Imagine working on a project that requires Python, a particular Python version, several Python libraries, and perhaps some command-line utilities.
Normally, you might document the requirements:
Python 3.xnumpyscipypandas...But someone else still has to recreate that environment.
With Nix, the development environment itself can be defined.
For example, a project can provide a development shell:
nix developNow everyone working on the project can enter essentially the same environment.
This is particularly appealing for research software.
Rather than writing in a README:
You need Python version X, package Y, package Z, and this particular system dependency…
I can put the environment into the project itself.
The project carries its environment with it.
A project becomes more self-contained
This also changes how I think about dependencies.
A traditional project might depend on the state of my computer:
My computer ├── Python ├── system libraries ├── command-line tools └── various packages installed years ago ↓ My projectA Nix-based project can instead describe its environment:
Project ├── flake.nix ├── flake.lock └── development environment ↓ My projectThat doesn’t eliminate all sources of environmental differences, but it moves a significant amount of configuration into something version-controlled and reproducible.
And because the Nix files are just files, they can live alongside the source code.
Git becomes a record not only of changes to the program, but also changes to the environment in which the program is developed.
Packages without polluting the system
Another interesting property of Nix is that packages are treated differently from traditional package managers.
Nix stores packages in the Nix store, where different versions can coexist.
This means I don’t necessarily have to replace one version of a dependency with another globally.
Different projects can use different versions.
This is particularly useful when working on older software.
One project might need an older version of a compiler or library, while another requires a newer version. Rather than trying to make the entire operating system satisfy both requirements simultaneously, Nix can provide each project with its own environment.
Dependency conflicts become much less frightening.
The system becomes version-controlled
Perhaps the biggest change after switching to NixOS is psychological.
My operating system is now much closer to a Git repository.
I can look at the configuration and understand why something is installed.
I can make a change, rebuild, and commit it.
I can move the configuration to another machine.
I can go back to an earlier version.
And if something breaks, I have a much better idea of what changed.
There is something satisfying about knowing that my computer isn’t the result of years of accumulated configuration drift.
Instead, there is a description of what I want the computer to be.
Is NixOS perfect?
Definitely not.
Nix has a steep learning curve.
The language is unfamiliar if you’ve never encountered functional programming. Error messages can sometimes be intimidating. Documentation is distributed across different sources, and figuring out the “Nix way” of doing something can take considerably longer than running a conventional package-manager command.
The declarative approach can also initially feel unnecessarily complicated.
On Arch, I can type:
sudo pacman -S somethingand I’m done.
With NixOS, I might instead need to figure out where that package belongs in my configuration, modify a .nix file, and rebuild the system.
That’s more work for a one-off change.
But that misses the point.
The advantage isn’t that declaring a package is faster than installing a package.
The advantage is that the declaration persists.
Months later, I don’t need to remember what I did.
The configuration tells me.
A practical example: PsychoPy experiments
One of the places where I think this becomes particularly useful is scientific research.
I use PsychoPy to program fMRI experiments. PsychoPy itself changes over time, as do the Python packages that experiments depend on. A script that works perfectly today might behave differently after upgrading PsychoPy, Python, or one of its dependencies. In the worst case, packages can become incompatible with one another and the experiment may no longer run at all.
This is a serious problem for reproducibility.
Imagine that I write an experiment today and run it on my computer. Two years later, another researcher wants to run exactly the same experiment. If they simply install the latest version of PsychoPy and the latest versions of its dependencies, they aren’t necessarily running the same software environment that I used.
With Nix, I can instead define the entire environment in a flake.
The flake can specify the Python version, PsychoPy version and other dependencies required by the experiment, while the lock file records the exact versions of the inputs. The experiment’s software environment therefore becomes part of the experiment itself.
For example, rather than simply telling someone:
Install Python and PsychoPy, then run the experiment.
I can give them the project containing the experiment and its Nix configuration:
experiment/├── flake.nix├── flake.lock├── experiment.py└── stimuli/They can then recreate the environment with Nix and run the experiment inside it.
The important part is that this isn’t dependent on what Python packages happen to be installed on their computer. The environment is defined by the project, and thus self-contained.
This means that I can come back to the experiment years later and recreate the software environment in which it was originally developed, rather than hoping that the current versions of Python, PsychoPy and its dependencies still happen to be compatible.
For scientific experiments, this is a much more meaningful benefit than simply having a convenient package manager.
The code, dependencies and environment can all become part of the reproducible research project.
And that’s probably the clearest example of why I find Nix interesting: reproducibility isn’t just about being able to rebuild my operating system. It can also mean being able to reproduce the computational environment in which an experiment, analysis or piece of research was actually performed. Here is my nixos PsychoPy flake template:
Why I ultimately switched
Arch Linux gave me a huge amount of control over my system.
NixOS gives me something slightly different: control over the state of my system.
The distinction sounds subtle, but it has become increasingly important to me.
I don’t want my computer to be a unique snowflake that I carefully assembled and then have to reconstruct from memory when something goes wrong.
I want to be able to say:
This is my configuration. Build this.
That’s the fundamental appeal of NixOS.
The operating system becomes something that can be described, versioned, reproduced and shared.
And the same idea extends beyond the operating system. With Nix flakes, individual projects can describe their dependencies and development environments in exactly the same spirit.
The result is a computing environment that feels less like a collection of things I have installed and more like code that happens to run my computer.
That is ultimately why I switched from Arch to NixOS.
Reactions
Comments
Loading comments...