I’ve been using NixOS recently on my laptop, because I know how great declarative configuration is from my experience deploying software on servers with tools like Kubernetes and Terraform. Along the way, I’ve had a few experiences with it that have left me impressed with how powerful a declarative approach to the configuration of my personal computers can be.
If you’ve used any electronic device that you can install software on, you are already aware of the imperative approach to device configuration.
If a piece of software doesn’t exist on your machine and you want it to, you run an installer or tell the package manager to install it:
apt-get install -y neovim
A problem with installing packages this way is that over time you may build a collection of software with incompatible versions of dependencies. You might want to upgrade all the software on your machine, but you don’t have any guarantee that the upgrade will succeed. And if you do break something, the only way to fix it might be to reinstall. You might even want to reinstall every couple years anyways to start fresh.
NixOS lets you define the configuration of your system declaratively. The
software you want to be installed on your system is listed (or enabled) in
a config file — configuration.nix — and after you add new software to
this file, you tell NixOS to rebuild your entire system by running
nixos-rebuild switch.
{ config, pkgs, ... }:
{
# ...
programs.thunderbird.enable = true;
programs.fish.enable = true;
users.defaultUserShell = pkgs.fish;
programs.neovim = {
enable = true;
defaultEditor = true;
};
# Allow unfree packages
nixpkgs.config.allowUnfree = true;
# List packages installed in system profile. To search, run:
# $ nix search wget
environment.systemPackages = with pkgs; [
# vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
wget
];
}
Every time you rebuild your system, Nix “evaluates”
all the packages you’ve chosen and their dependencies, and saves the output
— the build artifacts — in the Nix store. Usually, nix doesn’t need to build
the packages you’ve chosen and they can be downloaded directly from a Nix “cache
server”.
After download or build, the packages involved in the
active configuration have their binaries symlinked into a special directory included
in the PATH.
On my machine, this is /run/current-system/sw/, and /run/current-system/sw/bin/nvim
is a symlink to /nix/store/xz3jy2lzzzycq3dnavn7v1hyr598pyr9-neovim-0.11.5/bin/nvim,
for example. In fact, /run is mounted as a tmpfs that is setup when the machine boots
or the configuration is switched. Changing the configuration is an atomic operation
that can be reversed easily, including by rebooting and selecting an older configuration
from the GRUB startup menu. A NixOS system, including the installed packages
and system-wide configuration, is essentially itself a build artifact — the result of an
evaluation of your configuration.nix.
There are a number of configuration options available in NixOS that will both install any relevant packages as well as write (and symlink) any configuration files.
The networking.firewall option set will install iptables for you, and you
can use allowedTCPPorts to open up ports as needed.
{ ... }:
{
networking.firewall.allowedTCPPorts = [ 22 ];
}
allowedTCPPorts and other values are merged during the inclusion of nix
code from other files, so you can put the code to open a port associated with
a service next to the nix configuration for that service.
There’s a whole Nix “channel” of hardware configurations you can import
into your configuration.nix. Importing the configuration for
my 7040 AMD Framework 13,
for example, brings in kernel parameters to fix common issues with the iGPU,
power profiles that work well, and fingerprint reader support.
Something I find exciting about NixOS is that if you need a specific modification
to
one of the packages in your system,
you can put instructions to add a patch to that package in configuration.nix
(or one of the files it includes) too. Overlays
are the feature that lets you do this.
Want an unreleased feature in a piece of software you use that only exists as
a pull request? You can apply a .patch at build time.
Or maybe you just want to build a version from git that nixpkgs doesn’t have yet.
You can specify an exact commit to build the package from.
You can use Rust uutils instead of GNU coreutils by replacing them as a dependency system wide.
After using NixOS on my laptop for a few months, I decided to install it on my desktop. Unfortunately, the installer never loaded, always crashing with a trace related to the open source nvidia driver “nouveau”. There are text mode installers, but I found the automatic partitioning with Hard Drive Encryption extremely handy.
Amazingly, I found that I could make an installation ISO with a small .nix file with
vendor nvidia drivers instead of the default open source ones. Below is the
entirety of the .nix file I used to do that. It imports the
Gnome (& calamares) installation CD that I was using, and the rest of the
configuration resembles what you would use to actually configure nvidia drivers
on a machine post-installation.
# nix.iso
{ config, pkgs, ... }:
{
imports = [
<nixpkgs/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix>
];
nixpkgs.config.allowUnfree = true;
services.xserver.videoDrivers = [ "nvidia" ];
hardware.nvidia = {
open = false; # Set to true for open kernel module (RTX 20+)
nvidiaSettings = true;
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
# Blacklist nouveau (usually automatic, but explicit doesn't hurt)
boot.blacklistedKernelModules = [ "nouveau" ];
}
Then, this command produces an ISO that I was then able to use to install NixOS:
nix-build '<nixpkgs/nixos>' -A config.system.build.isoImage -I nixos-config=iso.nix
Easy!
After the install I was unable to boot — the installer had written and
evaluated the default configuration.nix onto the system. I had to boot into
the ISO I had generated, mount the new installation under /mnt and use the
nix-enter utility to “enter” the installation so I could rescue it by
pasting the nvidia configuration into configuration.nix. So — not a perfect
experience, but it does show off the power and flexibility of NixOS.
I don’t have a full mental of what is going on in Nix (the language) and NixOS. I’ve been playing around with it for a month or two now.
I have a much more complete mental model of what is happening in an imperative paradigm like scripts or Ansible playbooks. And my understanding is also complete with tools in the declarative paradigm like Terraform and Kubernetes. NixOS, however, still feels like magic in that I’m getting a lot of value out of it without understanding the full mechanics of what is going on inside.
Current limits of my understanding include:
configuration.nix — which
returns values — get converted into a collection of built components?Maybe after I publish this I’ll go digging to find out.
The built packages reside in /nix/store and paths inside the store
are the hash of their inputs. Applications are linked against libraries
in the store with their /nix path. You could actually have two versions
of the same library installed, with different applications linked against
the same library they were built with.
This is an elegant solution to avoid dependency hell, but it creates problems, both for building applications that depend on an FHS build environment, and for running binaries not from NixOS that depend on libraries in the FHS location.
Nix-ld is a workaround that installs a library loader that can load configured libraries for applications that might expect them in the “normal” location.
This is a minor inconvenience as a developer, also. Early on I discovered that I wanted to casually build software against external libraries and I couldn’t, because pkgconfig doesn’t know about the Nix store. This workaround will let you link against libraries that are installed.
# This tells NixOS to link the .pc files into a global directory
environment.pathsToLink = [ "/lib/pkgconfig" ];
environment.variables.PKG_CONFIG_PATH = "/run/current-system/sw/lib/pkgconfig/";
I’m not sure I want to run my development environments in “the Nix way” quite yet. Neovim and other IDEs also like having a “normal” build environment for making tooling work, etc.
Don’t distribute the binaries you generate with this workaround — they will be linked against the Nix store paths. They may also break if you garbage collect your Nix store.
If you want to try out NixOS, a good tip I’ve heard is that you can run NixOS in a virtual machine, get the configuration just the way you want it, then save the configuration and apply it to your system after you’ve installed it on bare metal.
You’ll want to check out Nixpkgs search to find packages to install. You’ll also want to check out “options” before “packages”. Many applications (for example, Firefox) are installed by enabling configuration options if they also change the system beyond simple installation of runnable software.
The wiki is a good resource for information about software that might require
this kind of extra configuration beyond inclusion in systemPackages. I found the
pages on 1Password,
Docker and
Ollama helpful.