NixOS: the good, the bad, and the ugly

:: computers, nixos, operating-systems, package-management

I’ve started and thrown out a few drafts of a post about NixOS over the last of couple years. At this point I see blog posts about NixOS so frequently that perhaps there is little left to say. But I want to say something, so here is an overview of some of my favorite features and most irritating issues.

1  The Good

My experience with NixOS is mostly very positive.

1.1  Transactional, functional package management

The computer can die at any point in an update and come back to a good state, which finally allows fearless auto-update for the whole system. Packages have no pre/post-install scripts, and therefore need no uninstall scripts. Things that need install scripts in other distros, such as icon databases that include icons from all installed packages, are instead built as a kind of dynamic package depending on the relevant installed packages (eg. the ones with icons). You can roll-back installs to get to a known-good state, and you can keep several “generations” of your OS installed to go back and forth at will. Manually triggered or fully automatic garbage collection keeps disk space use in check.

1.2  Mixed stability and package sources

I’ve tried many times on many operating systems to mix multiple package repositories, and in particular the stable and unstable branches of the main OS repository. It always ended in tears. Until NixOS, where it works beautifully.

1.3  Nix shell environments

It’s easy to make limited environments with exactly the packages you want. This is great for reproducible development and experimentation.

1.4  Uniformity

This is not unique to NixOS, exactly, but now that I have all of my (non-phone...) machines running NixOS, I have a uniform system to work with everywhere. It’s really nice to not have to deal with different OSes for different machines. NixOS is the first OS that I feel fits my needs for all of my machines: servers, laptops, TV-boxes, etc.

☎: Imagine if my phone also had all of these advantages – actually saving all my configuration in a git repository, shared code with my laptop, and transactional package management even for the core OS – no bricks, even if the battery dies while updating the kernel. Don’t get me started about how the very concept of a bricked computer is complete and utter nonsense.

1.5  Full-system declarative configuration and library-ification

While I’ve kept my OS configurations in git repositories for a long time, NixOS has made (more-or-less) reproducible systems significantly easier. Many pieces of configuration that were previously implemented as imperative scripts can now be declarative configuration. Declarative configuration is much easier to keep honestly in sync than imperative configuration, so I’m much more confident that my systems are sufficiently replicable.

Between having a much more declarative and programmable configuration system and the uniformity of my computer fleet since I’ve switched to NixOS, it is easier and makes more sense to turn shared configuration into a library. I’ve used some shared components for managing my systems ever since I started keeping configurations in git, but now I have much more ambitious sharing. For instance, one particular issue I’ve solved is service email. I’ve long enabled a (local-only) email server on each machine to give me warnings about cron job failures, warnings from mdadm (the RAID health monitor) and smartd (the disk health monitor), etc. But unless I logged on to the system in question, I would never see those emails! Now I have a small configuration library that moves all these emails to a central server, so I actually see them. As a shared submodule of each configuration repository, this service only needs a couple lines in each machine’s main configuration.nix file to enable and configure.

1.6  Easier custom packages

NixOS makes it easier to keep custom packages as part of your OS configuration. Rather than writing custom, imperative install scripts to run when setting up your machine, you can simply list your custom packages in your system’s package list, and they will be built automatically. Previously I’ve usually built custom “packages” by checking out and building the git repository in my home directory rather than going through the trouble of making an OS package then scripting its build and installation. Now most of these are migrating to my NixOS configuration instead.

2  The Bad

There are some issues that annoy me about NixOS, however.

2.1  The language

There are several things I dislike about the Nix expression language. The biggest issue, however, is that it’s a stand-alone DSL that re-implements the bulk of a general-purpose language2. The Nix expression language isn’t that bad, but it’s a shame that it’s yet another weird little language you need to learn to accomplish one niche thing. Sure, there is a core domain-specific element you would need to learn either way. But as a stand-alone language you have to learn (and the developers have to write and maintain) another standard library that’s a bit different from the others, another set of semantic oddities, etc, for parts of the language that are essentially orthogonal to the domain-specific interests of system configuration and package management. It’s wasted effort both for implementors and users.

2: My second biggest issue is that the documentation is greatly lacking, but that’s largely a symptom of the fact that it’s hard to document an entire standard library and set of (largely general-purpose) language features in addition to documenting the domain-specific parts.

Guix took a much better approach here. Rather than building a bespoke language with yet another standard library for strings, lists, dictionaries, math, filesystems, IO, error handling, etc, Guix just makes a DSL library that provides a set of features relevant to packaging and system configuration on top of Guile Scheme. So why am I not using Guix? As of a couple years ago when I tested them, Guix was just not sufficiently stable and mature while NixOS was. NixOS has a much bigger community, NixOS has a bigger package repository, and both of these are growing faster for NixOS than for Guix. And, of course, Guix has its own set of design decisions that can complicate some of my use cases©. That said, it’s quite likely that I’ll switch some day, since I think the language choice (and various other decisions) in Guix are much better than in NixOS.

©: For example, I really want to use the same OS everywhere, including my laptop, my server, the computer I administer for my parents, etc. For some of these I make the unideal but expedient choice to use a few proprietary packages. I generally agree with Guix’s moral stance that proprietary software is bad, but purposely making it extra difficult to use in your OS doesn’t generally mean people stop using proprietary software, it means people don’t use your OS. I think Debian’s approach of maintaining a separate, not-included-by-default repository for proprietary software that is discouraged but not hidden or difficult to use is the best choice.

⚔: Guix had the benefit of being designed after seeing how NixOS worked. They are both great systems, and from what I can tell the communities are in collaboration as much as in competition. I’d like to see them both succeed and borrow ideas from each other.

2.2  Workable, reliable development environments

If you don’t go out of your way to specify an exact commit to get your packages from, your environment will change as your nix-channel updates and it will not be reproducible. Worse, even if you don’t care so much about hard-core reproducibility in your laptop development environment, if you don’t pin your packages all of the nix store paths that were statically compiled into your program at configure time will be garbage collected and your environment will just not work anymore. This is especially painful when working with software that doesn’t have a good built script for a clean step, so you need to nuke the repository to get back to a buildable state. But mind, you don’t just need to pin the packages, you need to use pinned functions when building the environment (eg pinnedPkgs.mkShell, not pkgs.mkShell), or some core packages from the non-pinned nix-channel package set will still come through and ruin everythingυ!

υ: While it may look obvious that you should use pinnedPkgs everywhere instead of pkgs, it’s easy to make the mistake when working under the with pkgs; namespace. I often do this out of convenience and because examples that I cargo cult do it too.

Needless to say, it took me several tries to make a reliable development environment using nix-shell, to say nothing of a reproducible one. Additionally it was difficult to figure out which packages are necessary for basic things like Unicode support to work in the nix-shell. This is mostly a documentation issue. Many examples in the documentation elide both pinning issues and issues of obscure packages being necessary for things to really work properly.

However, part of this is also painful because nix-shell builds an environment where everything resolves to /nix/store paths. In other words, rather than linking store paths into generic paths like $PREFIX/lib, programs find libraries, executables, etc, through environment variables (like $PATH and $LD_LIBRARY_PATH) full of dozens of paths directly into the nix store. The fhsUserEnv feature provides a more dynamic environment with non-store paths, but it only goes half way since things like pkgconfig still point directly into the store.

2.3  Broken Images

A lot of images for GUI programs, such as buttons and desktop icons, are broken. In particular, the LXQT desktop (my favorite desktop to give to Windows refugees) seems to have nearly (but not quite!) all the icons in its launcher broken for reasons that I haven’t been able to debug. The issue doesn’t bother me personally too much, because I primarily work in the terminal. But this is a critical bug for serious use of NixOS in GUI-centric workflows.

3  The Ugly

The system profile is stripped of shared libraries, headers, and other development-oriented files. There is no configuration option to opt-in to having these files. The NixOS developers are dogmatic uncompromising on this point.🔥

🔥: Ok, perhaps my complaints go too far in this section. This issue may technically run deeper than I realize and make it hard to provide an option even if the developers want to. In that case, this largely boils down to the static vs dynamic tradeoff discussed in the conclusion. But if that’s not the case, then this design deserves this frustrated rant and more. NixOS developers, you are awesome, but this issue is just atrocious. If there are technical reasons this is impossible, please cite them in online discussions about these things, don’t just repeat the mantra that “that’s the wrong way”.

This is easily the worst aspect of NixOS. I think the library and header stripping behaviour is a great option, and perhaps the right default. But obstinately requiring this behavior makes a lot of sensible workflows impossible or needlessly complicated in the name of computer users doing “the right thing”.

Often I find little projects online that I want to try out. On systems like Arch Linux it is often easy to just clone a repo, make, and experiment. However, NixOS practically requires that software be packaged for NixOS to be built on NixOS. Want to quickly build software in your home directory, trusting that your all-purpose development machine configuration already has the right libraries installed to “just” build it (because you purposely installed practically all of the development libraries in the repository particularly to make this one activity easy)? No! That’s not doing “the right thing”.

This issue is not merely an inconvenience for lazy software experimentation and development. If you want to dynamically write and evaluate code that interacts with and affects your window manager, shell, and other things in your “global interaction environment”, it really needs to run outside of a nix-shell environment, but NixOS makes it really hard to run some things without nix-shell. This design decision really hampers dynamic workflows unless each component needed for the workflow is already well packaged and configured for NixOS.

Despite all the things I love about NixOS, this “feature” has made me consider going back to Arch Linux for my development machine several times. If my whole system configuration is in a git repository, then the whole system is sufficiently reproducible for a whole host of use cases that are better served by NOT using nix-shell environments. It’s almost as if it’s reasonable to treat a version-controlled, carefully tuned system configuration like a purpose-built development (and dynamic interaction!) environment without jumping through several extra hoops!

4  Conclusion

There’s a lot to love about NixOS, and a few things, perhaps, not to.

While it hits most of the points I’ve mentioned I want in an ideal package mananger, it is more static than I would like for many applications. In other words, I would like to declare what packages are in my main system profile, other user or service profiles, or specific nix-shell-style environments, but I would also like packages and configurations to resolve to more abstract paths where package files can be injected (in various views of the file system) rather than statically resolving to the nix store. This is not to say that the more fully static approach of NixOS is necessarily wrong – just that there are tradeoffs between the two. Ideally, one system could provide both, but that would perhaps imply two versions for each package – a dynamic version and a static version. However, my OS (packages included) doesn’t take up terribly much storage, so doubling that for the extra flexibility would not necessarily be impractical. At the moment I know of no “dynamism friendly” OS that includes features like transactional, script-free package management and whole-system declarative configuration, so I’ll just have to muddle along with NixOS to get those features.Δ

Δ: Actually, I’m not sure whether Guix is like NixOS in this regard or not. I realized this was a major issue after I had already fairly well ruled out Guix and settled on NixOS. I’ve not given time to experimenting with other operating systems lately, so I may not find out for some time if nobody informs me (*wink wink nudge nudge*), but perhaps this is the feature that will push me to switch to Guix. We’ll see.

While I’ve complained about issues that bug me primarily on my development machine, I’ve loved NixOS on servers and machines where I don’t casually build things in my home directory. While I’ve in frustration often considered reverting my laptop to Arch Linux, there is no way I will go back to Debian for serversδ. NixOS is a huge step forward in operating system design and implementation, especially in package management. The writing is on the wall: NixOS-style package management and system configuration is the future🦖.

δ: Debian was my go-to distro for servers or computers that I administered less frequently. There’s a lot I like about Debian, but Debian package management is decades out of date.

🦖: Well, or maybe downloading untrusted code and running it in Electron inside a Docker image that pulls in an entire Ubuntu image running in a virtual machine to run a text messaging app is the future. I mean, technology doesn’t inexorably march towards universal progress, it shambles wherever people push it. I hope we collectively move in smarter directions like NixOS package management, Lisp Machine deep, dynamic interactions, and simple, structured, hypermedia that supports third-party annotation, quotation, transclusion, user-styling, etc. But we’ve collectively made poor technology choices before, and there’s no reason to believe we’ll stop now. Good luck, NixOS and friends!