Suckless Nix
Published:
Many people criticize Nix for being an overcomplicated abstraction and that's absolutely true if you're using Nix the way most people do. This blog post will explore my experience learning to use Nix over 12 months and the 'suckless' configuration I've settled on.
Don't Use Home Manager
In my experience, using Home Manager has more negatives than benefits. For example, trying to configure the settings for any program involves this guessing game of what options are supported and how to configure them. Take this snippet from my old home manager configuration:
programs.ghostty = {
enable = true;
settings = {
shell-integration = "fish";
shell-integration-features = true;
mouse-hide-while-typing = true;
cursor-click-to-move = true;
mouse-shift-capture = true;
scrollback-limit = 10000;
link-url = true;
link-previews = true;
background-opacity = 0.8;
background-blur = true;
window-inherit-working-directory = true;
window-save-state = "always";
copy-on-select = "clipboard";
clipboard-read = "allow";
clipboard-write = "allow";
confirm-close-surface = false;
auto-update = "off";
};
};
The entire settings
block is undocumented and you have to go into the Ghostty Option Reference
and guess how to set it up in the Nix langauge. It was precisely this kind of frustrating experience which really made me consider switching away from NixOS.
Suckless Software
Fortunately, I found out about Suckless Software and I basically deleted my whole configuration and started over on X11 with a renewed energy to write a minimal, yet powerful Nix configuration.
The Suckless philosophy has a focus on simplicity, clarity, and frugality which helps keep their software easy to maintain, extend, and modify. I believe applying these principles has helped me create a reliable configuration that I can depend on for work, school, personal use, and running a business.
Simplicity
It took me approximately 10 months of daily use to really understand what Nix is and how it works. Nix was my first functional programming language, so I was unfortunate enough to have to struggle to get to where I am today. The trick to grasping Nix is to avoid using fancy features and just stick to basic features as you are getting started; I like to tell people that the best way to use Nix is to use as little of it as possible.
Flakes are one advanced feature that is an exception from this rule because they give you the ability to pin down the exact version of the inputs used, and they let you easily wire up your configuration for different hosts. A Nix flake is literally just an input and output. Inputs are just some urls to some packages like nixpkgs
or my dwm
package. The outputs are just a fully configured, installed and setup system like a nixosConfiguration
or a darwinConfiguration.
The way that I solved the problem with Home Manager, and configuring the system in general is to use Nix to do the following:
- Manage dotfiles, and put them in the right place
- Download and install packages, keeping them locked at the right version and isolating their dependencies.
- Start services
Don't try to write your whole system config in Nix. It's a fool's errand, and gives you almost no benefit since you can't really take advantage of the languages features, besides first class file paths. My first choice is to just use Nix to enable and set just a few settings, but if it starts getting too complicated, I usually use a mix of the documented options like enable
and extraConfig
to use the program's configuration syntax
when I need to merge the two, which Nix can handle beautifully.
For dotfile configuration, I just write them into the dotfiles directory in my git repo, which Hjem copies into the Nix Store and symlinks to the config location that the program looks for. This method allows me to take full advantage of existing documentation for the tools, but also use Nix for the reproducibility and management of dotfiles.
Clarity
I was inspired by Andrew Kelley, the creator of Zig programming language, and his philosophy of not letting the compiler do stuff behind your back. To translate this to a NixOS configuration, I opted to make minimal use of overlays, modules, and functions. As a programmer it's very difficult to resist the urge to not repeat yourself but I've found through experience, that I'd rather just repeat myself once or twice or even three times because it's much easier to maintain. Whenever you add these abstractions, the code doing real work get shoved somewhere else and it becomes confusing with regards to what the code is actually doing because you need to jump somewhere else to understand it. Take my Nix Darwin (MacOS) configuration where different configurations need different hostnames and usernames:
darwinConfigurations = {
kenosis = nix-darwin.lib.darwinSystem {
specialArgs = {
inherit inputs;
nvim = nvim-darwin;
username = "andrew";
};
modules = [
./common-darwin.nix
./hosts/kenosis/configuration.nix
./overlays.nix
hjem.darwinModules.default
];
};
bear = nix-darwin.lib.darwinSystem {
specialArgs = {
inherit inputs;
nvim = nvim-darwin;
username = "andrewmontgomery";
};
modules = [
./common-darwin.nix
./hosts/bear/configuration.nix
./overlays.nix
hjem.darwinModules.default
];
};
};
While it is very simple to refactor this into a function, it hides the code away from where I want to see it and be defining the actual configuration, if I need to do a quick fix to add a module.
Also, the code duplication should not really be a problem for personal computing. My flake has 4 configurations for my desktop, homelab, and two laptops, and there are only ~3k LoC. Very manageable.
Something that I really appreciate about Nix, and NixOs is that it lets you say services.program-here.enable = true;, and it will make the systemd timers and services for you, but it also lets you have full control to write them yourself. This simplicity of turning on a service with one line, but also the ability to have deep level of control, lets me feel safe, knowing that I can tweak it if I need to. While this point seems a bit contradictory to the goal of absolute clarity, I think that the level of abstraction is tasteful enough that I don't mind Nix doing a little bit of stuff behind my back, since I can rip it out and replace it with my own whenever I like.
Frugality
Something I've noticed when using X11 is that the extremely mature ecosystem has plenty of great tools that just do exactly what you need them to do. For example Simple X Hot Key Daemon (sxhkd) was extremely easy to setup, and it can run any shell script you want it to. Moreover, adhering to a strict UNIX philosophy, ensures that every new thing that I add can extend existing tools leading to even better workflows.
On my system, I tend not to use GUI programs because often they lack features that I need, or it's just faster to do it on the command line. Most tasks I do on my computer involve writing, editing, or manipulating text, so it doesn't make sense to have 5 different programs to all have to build integrations with each other, when they could all go through a unified interface like the command line or a real extensible editor like GNU Emacs.
For instance, I recently replaced KeePassXC with the unix password-store (pass) utility. While KeePassXC was enjoyable in GUI heavy workflows, the program tries to write to the readonly file in the Nix store, which causes some problems customizing the program. Not wanting to touch a massive codebase and keep up with security patches, I opted for a simpler utility that just works and I haven't had much problem with password-store. It even does browser autofill, and OTP codes!
NixOS vs OpenBSD
My utmost concern, when it comes to operating systems, is that they must be reliable enough that I can leave them in the background, and they will do their thing, forever unchanging, until I change them. In essence, I want to be able to trust my computer is working for me, not someone else.
In my experience, there are two reasonable ways to solve this problem: Either lockfile everything down and be fully reproducible OR be extremely stable and have a dedicated first party way of doing things
This lead me to explore two different operating systems which take these two routes, and discover the subtle needs that warrant the right tool for job (which could be different depending on the circumstances).
NixOS as a Desktop
A desktop PC needs to run a graphical display; play audio, and games; easily add and remove new software; and more. I've omitted obvious stuff like security and performance, since these should be givens, not a feature. For this application, I think that NixOS is clearly the winner because of better compatibility support and the ability to quickly hop into a nix shell 'nixpkgs#program-here'
and start tinkering with a new package. Moreover, the ability to rollback to a working configuration in this volatile environment is extremely handy.
NixOS as a Server
As a server, NixOS is also great because there are so many different programs and services that can be easily setup such as nfs, git daemon, Soju IRC bouncer, firewall, and more! It's very simple to enable a service and configure it how you like, and be able to reproduce that configuration on another machine with one command.
That being said, there are occasional issues with SystemD isolation causing certain services to be unable to access a file or other program which involves some debugging and ugly fixes. Though the benefits definitely far outweigh the problems when it comes to this approach.
OpenBSD
OpenBSD approaches this problem from a different perspective by prioritizing correctness, good documentation, and having builtin ways of doing things. The first thing I noticed when I setup an OpenBSD VPS to host my static site and do TLS termination, was the great man pages and solid library of builtin programs. I use httpd, and acme-client to handle serving my website html pages and SSL certs. I also use pf and relayd to handle routing different packets to where they need to go, between my homelab and httpd. Overall, the operating system experience is very well thought out, and a breath of fresh air compared to Linux.
OpenBSD is probably the only operating system, I know, that I would trust to touch the public internet.
- Its core philosophy of correctness and continuous security audits keep it secure.
- The unified vision ensures that programs work well with each other since they have the same expectations. For example: httpd and relayd can work harmoniously without interfering with each other even though they touch similar areas.
So then why don't I use OpenBSD as my desktop OS? Well, I think that's a great question. I am already pretty happy with my current NixOS setup, and it wouldn't be much benefit to switch over to a BSD yet.
The Problems With Nix
- Poor Documentation: If you just use basic features then search.nixos.org
and mynixos.com
have got you covered. It's ironic that I have to mention two sources of docs, since the official nixos.org one has great detail, but it's lacking Nix-Darwin options. On the other hand, mynixos.com has better organization, but is missing package sets like
emacsPackages - The
sudo nixos-rebuild switch --flake .rebuild and evaulation time take more than 10 seconds every time. It's frustrating because you can't get a fast iteration loop to test out small config changes, and I wish they could make it faster. - NixOS depends on SystemD which means that it would be difficult to migrate to a different init system.
- Nix store making things read only which causes some programs to break.
- Disk bloat which caused problems trying to update my laptop which was running out of disk space.
- Not really a Nix problem, but Neovim tries to be the opposite of suckless by constantly changing and adding new updates/package managers. At the time of writing this, I'm trying out GNU Emacs.
Conclusion
Overall, I highly recommend Nix for power users. It has helped me create a reproducible dotfiles configuration, manage & survey all the programs I installed on my computer, and have an easy to change, yet stable operating system.
After using NixOS for a year, I'm still figuring out some of the more advanced features of the language:
- Modules, and whether they are really worth hiding away parts of my code for reuse as my configuration.nix gets longer.
- Where to draw the lines between where certain configuration should live.
- What is configuration and what is data that should stay out of Nix.