Declarative Configuration with NixOS

NixOS is a GNU/Linux operating system based around the Nix package manager. Nix was developed as a research project but is now fully functional open source project with a vibrant community.

The advantage of Nix is that the software configuration can be completely defined by a single file containing a single “Nix expression” in its own domain specific language and always evaluates with the same result. This means that you can be sure that the system will always end up in the same state if it is supplied the same configuration file.

This post is centered around my experience with NixOS and Nix on it. Though, it should be noted that it is possible to install Nix on top of other distributions/OSs such as Arch Linux, Mac OS X etc as well.

The NixOS Pitch Link to heading

1. Declarative configuration Link to heading

In NixOS, configuration is defined by declaring what is needed. Textbook example would be declaring GNOME or KDE as desktop environment.

services.xserver.desktopManager.gnome3.enable = true;
services.xserver.desktopManager.plasma5.enable = true;

These two lines will install functional GNOME and KDE Plasma Desktop Environment (DE). Visit list of available desktop environments to check if your favourite DE is supported. Read on to learn how to use these lines.

2. Instant & Painless rollbacks Link to heading

On every configuration change and system rebuild, Nix stores the complete state of the system as described in the updated configuration as a new “generation”. If you start with GNOME enabled and then replace it with KDE and rebuild, it will cause Nix to generate a new generation with KDE enabled numbered 1 higher than the last generation. All of these generations are accessible via the boot menu in NixOS.

Switching generations or rollbacks are done by selecting one of the previous generations during the boot process. So, if your most recent change or system update broke your computer, you can simply reboot, select previous generation and get on with your work. You could also try correcting your changes and report troubling updates.

image

3. DevOps heaven Link to heading

Building a new generation will never cause a server to crash as all the builds are done in an isolated environment and they never affect the current running system. In fact, updates in NixOS are atomic in nature because updating on NixOS simply means updating symbolic links which is an atomic operation in Linux. In addition to that, components (barring the Kernel and the Init system) can be updated even during runtime without any issues.

With declarative configuration and atomic updates, there is never a reason to worry about the state of a system. One can be assured that the computer will always end up in a known state. So, with NixOS, there is no need to tend to every server computer like a child. No need to manually setup server and/or run frail shell scripts or non portable housekeeping programs to bring them up and keep them running.

Why’d I come to NixOS? Link to heading

  1. I’ve heard good things about NixOS from a lot of sources.
  2. Recent Arch Linux update broke my installation which gave me a reason to try NixOS.
  3. NixOS looked too good to be true. So, I had to see it for myself. I mean: reproducible single file configuration and instant rollbacks. Who wouldn’t want that?

Quickly setup NixOS Link to heading

NixOS is a very young distribution and they don’t have a GUI installer as of yet. So, you’ll have to be comfortable with the terminal to install it. Also, you’ll need a fast internet connection because the installer downloads lot of data during the install. This section is an overview of the install process as I do not intent to regurgitate what is already written in the official install guide.

1. Partition your disk Link to heading

You’ll need at least one partition where NixOS will be installed. With UEFI (which is enabled on all modern computers) you’ll need a boot partition to install the bootloader and need to disable Secure Boot. Note that you can probably reuse a boot partition already provided by Windows or other distributions. You also need a Linux swap partition if you want suspend-to-disk/hibernate enabled.

2. Boot the installer Link to heading

Download the latest ISO files for NixOS and then ‘dd’ it to a flash drive.

$ dd if=iso_image.iso of=/dev/flashDriveNode bs=1M # substitute correct values

Then boot the flash drive on your computer.

3. Prepare for installation Link to heading

Mount the partition where you want to install NixOS at /mnt, then mount the UEFI boot partition at /mnt/boot and finally turn on swap if you have a swap partition.

# mount /dev/rootFSNode /mnt # substitute correct values
# mkdir -p /mnt/boot
# mount /dev/UEFINode /mnt/boot # substitute correct values
# swapon /dev/swapNode

4. Generate initial configuration Link to heading

Let Nix detect your hardware and generate a starter configuration file.

# nixos-generate-config --root /mnt

Then you can edit the generated file at /mnt/etc/nixos/configuration.nix with your favourite editor to specify the bootloader you want to use.

For UEFI:

# ... some lines
imports = [
    ./hardware-configuration.nix
];

boot.loader.systemd-boot.enable = true; # you need these line
boot.loader.efi.canTouchEfiVariables = true;
# ... more lines

for MBR:

# ... some lines
imports = [
    ./hardware-configuration.nix
];

boot.loader.grup.device = "/dev/bootDiskNode"; # substitute correct values
# ... more lines

5. Install NixOS Link to heading

Make sure that your internet is working and then run this command:

# nixos-install

This should set you up with a working NixOS installation and you should be able to reboot into NixOS after this.

Stuff your install with awesomeness Link to heading

Now that you have a working installation, you can take a look at a basic NixOS configuration to see how the pieces fit together and how you can add more flair to your installation.

# Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).

{ config, pkgs, ... }: {
    imports = [ # Include the results of the hardware scan.
        ./hardware-configuration.nix
    ];

    boot.tmpOnTmpfs = true; # mount /tmp as tmpfs

    # Use the systemd-boot EFI boot loader.
    boot.loader.systemd-boot.enable = true;
    boot.loader.efi.canTouchEfiVariables = true; # modify NVRAM

    networking.hostName = "localhost";
    networking.networkmanager.enable = true;

    time.timeZone = "Asia/Kolkata";

    # List packages installed in system profile. To search, run:
    # $ nix search wget
    environment.systemPackages = with pkgs; [
        gcc # compiler
        git # distributed version control system
        # you can add more packages here
    ];

    # Enable the X11 windowing system.
    services.xserver.enable = true;
    services.xserver.layout = "us";
    services.xserver.xkbOptions = "eurosign:e";

    # Enable touchpad support.
    services.xserver.libinput.enable = true;

    # Enable the KDE Desktop Environment.
    services.xserver.desktopManager.plasma5.enable = true;
    services.xserver.displayManager.sddm.enable = true; # GUI Login screen

    users = {
        mutableUsers = false; # enable declarative user management
        extraUsers.someuser = {
            description = "Some User";
            extraGroups = [ "audio" "networkmanager" "wheel" ];
            hashedPassword = "substitute hashed password from mkpasswd here";
            isNormalUser = true;
            uid = 1000;
        };
    };
    system.stateVersion = "18.03"; # Compatible NixOS version
}

You can also view my computer’s configuration in shreyanshk/nixos-config repository for more examples. :-D

Working with NixOS Link to heading

1. Virtual environments on steroids Link to heading

Many projects need multiple components in different languages, file formats, libraries etc. Generally, for such projects one may have to make multiple virtual environments for all the different components. For example, if an application is using Python and friends for number crunching and Elixir for the website then two virtual environments need to be taken care of as two different languages are involved. It can certainly be improved as shown by Nix.

Nix improves on this by providing a system-wide standard way to work with virtualenv(s) with a single file. Consider the configuration file below:

with import <nixpkgs> {};

stdenv.mkDerivation rec {
    name = "somename";
    env = buildEnv { name = name; paths = buildInputs; };
    buildInputs = [
        cudatoolkit
        elixir_1_5
        python36
        python36Packages.numpy
        python36Packages.pytorch
    ];
}

A shell in this virtualenv can be launched by simply running:

$ ls
shell.nix
$ nix-shell
[nix-shell:~/dir]$

image

In fact, if provided with full configuration file it is possible to quickly launch a Virtual Machine containing the required configuration.

$ NIXOS_CONFIG=./config.nix nixos-rebuild build-vm
... lots of lines
Done. Virtual machine can be started by running <some long path here>
$ ./result/bin/run-<hostname>-vm

image

2. Imperative installations on NixOS Link to heading

Sometimes, you simply need to install rarely used applications quickly. It is useful to be able to skip all the hoops and just get on with your task at hand. To do this one can use the nix-env utility. For example to install Wireshark, you could run this command:

$ nix-channel --list
nixuns https://nixos.org/channels/nixos-unstable
$ nix-env -iA nixuns.wireshark
installing 'wireshark-qt-2.6.3'

That should get you up and running with Wireshark in a single command.

Advantages of NixOS Link to heading

1. Version controlled system configuration Link to heading

Since the configuration can be defined in a single text file or in a directory containing multiple files, the system state can now be committed to a version controlled repository just like application code.

2. No dependency conflicts Link to heading

If you ever had to simultaneously work with different applications which depend on conflicting versions of libraries or programming languages, you’ll see that it’s very tricky to get them working correctly because environment variables and hard coded paths.

In NixOS, dependencies are not a problem. I don’t mean “almost”. It’s really never. Everything is installed in it’s own isolated environment and the environment is marked read-only after the install. During invocation, NixOS makes sure that an application’s dependencies are passed to it. For example, flask (an awesome micro web framework) executable is really a bash script which sets up the environment and then call the actual script.

#! /nix/store/<path to bash>/bash -e
export PATH='/nix/store/<some long paths>'${PATH:+':'}$PATH
export PYTHONNOUSERSITE='true'
exec -a "$0" "/nix/store/<actual path to flask>"  "${extraFlagsArray[@]}" "$@"

Heck, you can install Flask without installing Python. You can work with applications depending on Ruby 2.3, 2.4 and 2.5 simultaneously.

3. Great out-of-the-box experience Link to heading

I was pleasantly surprised to find out the attention to details given in NixOS. Few of the things that I would like to point out are:

  • Hardware works together nicely and most hardware keys work without any extra steps. The keyboard lights turn off with display backlight. Display brightness changes are smooth and gradual in NixOS. Going standby pauses media playback.
  • Available shells are already configured with sane defaults. This is possibly the first time I don’t mind using the provided shell configuration for extended time.
  • Enabling most software such as Plymouth and SDDM is a breeze and a boolean variable away.
  • Amazing laundry list of packages available in the repository to choose from.

4. Binary based? Source based? Both! Link to heading

Nix packages are maintained in the nixpkgs-channels git repository. This repository contains Nix expressions for building all supported packages and is cached locally by Nix. The Nix community also also created their own Hydra build system which continuously builds packages on updates to the git repository and caches the results. So, if anything is used from here, it’s binary package is downloaded from the cache and directly used like in ArchLinux.

On the other hand, Nix also gives you the flexibility to declaratively define your custom packages or override instructions for predefined packages right in your configuration file. In these cases, the packages are build locally like in Gentoo.

For example, you could override build instructions of the Linux kernel package to you likings and put it inside the system configuration file. In this case, only the Linux kernel will be build locally and as specified by you but everything else will be downloaded from the cache.

Fact: If the cache goes down due to any reason, it will not affect you in anyway as Nix will then simply build the required packages locally.

Disadvantages of NixOS Link to heading

1. Awful documentation Link to heading

[NixOS community: I’d love to be proven wrong. Please prove me wrong.]

NixOS needs much work in the documentation department. Coming from Arch Linux, my expectations were high and it was only after reading NixOS documentation I realized the value of proper documentation.

Most of the official documentation is not up to date with current best practices in the community. Other sources of documentation in form of blog posts/mailing lists responses/email threads/forums are also useless because Nix is rapidly moving and hence, they become obsolete fast.

I also feel like the official documentation expects that the reader is already deeply knowledgeable in the Nix philosophy and doesn’t explain the details of what’s suggested. For example, the wiki on Python shows a snippet of code but doesn’t explain how/where to use that.

with pkgs;
let
  my-python-packages = python-packages: with python-packages; [
    pandas
    requests
    # other python packages you want
  ];
  python-with-my-packages = python3.withPackages my-python-packages;
in ...

From the above piece of code, it’s not immediately clear where these lines should go as it assumes the user will know how to work with the ’let’ block. That can be improved if the wiki at least pointed the user to an example on how to use this block or the ’let’ block man page but they don’t. Unfortunately, that’s the case with most of their documentation.

Even the package listing is hard to navigate. For example, the latest version of Firefox is listed under three different names: firefox, firefoxWrapper & firefox-wrapper. I ultimately settled on just the plain firefox and the difference is still not clear to me but I didn’t look much into it because “firefox” worked well for me.

In my experience, it is better to just search and look up at other people’s Nix configurations on GitHub or directly ask on the IRC channel #nixos on Freenode for pointers.

[Arch Linux community: Guys, thank you for your amazing documentation.]

The competition: GuixSD Link to heading

GNU took the Nix ideas and philosophy and decided to run with it. The result was called GuixSD. GuixSD is based around the Guix package manager and uses GNU Guile to define configuration files.

GNU Guile is far more approachable than Nix for declaring the configuration because the syntax is much cleaner as it is based on the Scheme programming language and the code is mostly self documenting. For example, this is GNU Hello in Guix:

(package
  (name "hello")
  (version "2.10")
  (source (origin
            (method url-fetch)
            (uri (string-append "mirror://gnu/hello/hello-" version
                                ".tar.gz"))
            (sha256 (base32 "0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i"))))
  (build-system gnu-build-system)
  (synopsis "Hello, GNU world: An example GNU package")
  (description
   "GNU Hello prints the message \"Hello, world!\" and then exits.")
  (home-page "https://www.gnu.org/software/hello/")
  (license gpl3+)))

Scheme has a long history so it’s documentation is readily available and it has features which makes it suitable for the type of tasks at hand and needed for declarative systems: minimalism, metaprogramming and functional.

But GuixSD is even younger than NixOS and many popular packages are still absent from it’s repository. Additionally, GuixSD is a GNU project and will not support any non-FOSS software without any workarounds because of the policies in place. This means that using any proprietary software is out of question which also rules out peripherals drivers/firmware needed by a lot of hardware for basic functionality.

Still, There is value in using GuixSD in server deployments if one is willing to write custom configuration to handle the proprietary software which is mostly straightforward.