For the past few months, I’ve been using NixOS as my daily driver for general use and software development. I wanted to share my experience with this distribution, as well as my opinions on it.
The Nix toolchain
Nix and NixOS are actually composed of multiple components, each of which can be configured and used separately.
The Nix package manager is at the heart of NixOS. It essentially provides a reproducible build system, as well as a powerful package management system. The Nix language is used for specifying builds in a declarative and functional way.
The Nix packages collection, nixpkgs, contains almost any package a Linux user could ever need. It contains over 80,000 different packages, which is more than the AUR, and about the same size as the Arch repos and AUR combined.
NixOS provides a system for declaratively configuring the entire operating system. It is based on Nix, configured in the Nix language, and uses packages from nixpkgs, but also provides additional modules that can configure numerous utilities and applications directly from the NixOS configuration.
Declarative OS configuration
One of the main features that drew me to NixOS is the ability to fully configure the base system in a single file, located at /etc/nixos/configuration.nix
. Instead of having to install each package separately and write config files by hand, most system packages and services can be configured by NixOS. For example, one could configure Syncthing, a simple file sync program, in just a couple lines of Nix:
services.syncthing = {
enable = true;
user = "isaac";
dataDir = "/home/isaac/Notes";
};
This configuration system is powerful, well-documented, and most importantly, completely reproducible. If I ever have to install NixOS again, I could just reuse the same configuration, and it should (in theory) install and function exactly the same.
In practice, as the system is so stable (even on the unstable channel) I actually haven’t had to install NixOS a second time, despite having to rebuild the OS many times due to configuration changes. NixOS will create a new ‘generation’ and bootloader entry each time you rebuild your configuration, so if something does break, you simply have to roll back one generation and fix it. This does require NixOS to hold control over your bootloader, although I’m currently dual booting and don’t have an issue with it.
Ecosystem fragmentation
Currently, the Nix ecosystem is introducing a new format for packaging applications (Nix flakes) and an improved command line interface (the nix
command). The current system continues to work and generally there are no usability issues, but it isn’t particularly well documented and was a bit confusing initially (especially as you can’t use the old nix-env
with the new nix profile
).
The point on documentation applies to a good portion of the Nix ecosystem in general. When documentation is available, it’s usually decent from my experience, but I have had to look up a lot of information from the forums and GitHub issues.
The Nix store
Because Nix is a purely functional package manager, instead of swapping out the executables and package outputs when a package is updated, it stores all packages in a directory called the Nix store, at /nix/store
, and symlinks the desired version or adds its location to PATH as needed.
Any change in a package will be reflected in its hash, which allows Nix to uniquely identify a specific version of a package. This allows Nix to track multiple versions of the same package, at once, without fear of any dependency conflicts.
However, this does have some side effects. For one, storing all versions packages that are currently installed or have been previously installed on the system takes up a lot more storage space than a traditional Linux distribution. Currently, my /nix/store
alone takes up about 60GB of data. In comparison, a simple Arch-based development environment I’ve used for a few months took up about 20GB of data, for the entire system. Fortunately, the Nix store can be cleaned and optimized with commands such as nix-garbage-collect
and nix store optimise
, and the storage requirements aren’t even close to those of some modern triple-A video games.
An unconventional filesystem
The Filesystem Hierarchy Standard (FHS) is adhered to by almost all UNIX systems, including most Linux distributions, but not NixOS. However, because Nix puts everything in the Nix store, it doesn’t adhere to the FHS, causing many programs and libraries to break when using NixOS. This normally isn’t much of a problem if the package is patched and distributed via nixpkgs, but occasionally there will be a couple of specific niche packages that will require some workarounds to function.
This was particularly evident when I started to work on a Python project recently. Unlike most languages that I work with regularly (which are compiled), many major Python libraries are written in C for performance reasons (which says a lot about Python itself), and rely on opening dynamic libraries and shared object files to function (which are stored in the Nix store, but the libraries try to find them in standard locations such as /usr/lib
).
There are ways to package Python applications reliably with Nix, but it honestly felt like a lot more effort than it was worth, especially when even though Rust, C/C++, Zig and Go applications can be packaged with Nix in a similar way, using the native package manager for each of those languages would work just fine (unlike pip and virtualenv).
Development environments
Another key feature that drew me to Nix was it’s ability to manage development environments with ease. Nix makes it extremely easy to just spin up an ad-hoc nix-shell
with certain packages, which are copied to the Nix store, and when you leave the shell everything is as before. This is extremely convenient for testing out new software, as the system won’t be affected at all (note that Nix shells shouldn’t be confused with containers or sandboxes).
Nix shells can also be used to configure a development environment, enabling reproducible development environments across systems. The shell can be configured in a shell.nix
file (or even directly in flake.nix
) to persist it across sessions. direnv
can also be used to enter the development shell when changing to the project directory, which allows for seamless switching between projects. Typical packages I would set up in a Nix shell would include the language’s compiler and tooling, code formatters, language servers, etc.
Packaging
Nix was designed as a package manager, so it obviously excels at packaging applications. Derivations (akin to build instructions) are written as Nix expressions, which are evaluated to build the package. Nixpkgs provides the stdenv
, which contains most tools needed to compile applications, but derivations can take any package from nixpkgs as inputs, which can make managing libraries trivial. Nix also integrates with GNU Make, CMake, Meson, etc. out of the box, but the ecosystem also features 3rd party solutions for packaging, for example crane
for building Rust crates.
Another really neat feature Nix has is the ability to create extremely minimal Docker/OCI container images. The image can be easily configured directly from Nix itself, and the resulting image is almost always much smaller than if it was built from a Dockerfile
.
Conclusions
This concludes what I wanted to say about NixOS. My experience with the distribution has been very positive in general, despite a couple of rough edges, like sparse documentation and FHS compatibility issues. I still haven’t tried many other aspects of the Nix ecosystem, for example home-manager
or configuring NixOS with flakes.
In general, NixOS is a flexible, functional, and powerful Linux distribution. If you’re a software developer or power user, consider checking it out!