Published on

NixOS for Apt/Yum Users: a Gift That Keeps on Giving

Authors
Table of Contents (Click to Hide/Show)

Foreword

This article is organized in 2 parts. The 1st part reflects on my observations, tries to give shape to my thoughts, and touches very briefly on the most common nix concepts. The 2nd part is the hands-on part. Reading the 1st part is in no way crucial for the understanding of the 2nd, but I encourage you to finish the 1st one, and then let yourself internalize it by taking a small break before proceeding to the hands-on. The footnotes in the article are not obligatory, so I suggest reading them afterwards.

If you've found this article via Google with a specific issue in mind, you may want to jump straight to the hands-on part.

Musings

This article is first and foremost dedicated to those, who may have found home with the distributions like Ubuntu and Fedora. Those, who are still at such a stage of their learning journey, that they may wonder what the difference between apt-get update and apt-get upgrade is (and why some online tutorials may omit the -get part). Those, who may have used ssh a couple of times already, but would not recall the process of generating and installing a key-pair. That someone is also me, many-many years ago, but since I have found shelter with NixOS.

I also hope to offer something new to those, who are already familiar with these concepts (and way beyond), so do not give up on this article just yet.

The idea of the Arrest of Change belongs to Plato. A critique of this idea makes a nice book1. Plato envisions an ideal world, a State, in which every person has their place and knows their place. Roughly speaking, in the Plato's State, no change would take place, since his State is ideal, and any change would only bring with itself harm. Alluding to this idea, I would tell the reader about me and maybe let them recognize themselves too.

I remember the days when tinkering with computers was a novelty and not some unpleasant mundane task. Nonetheless, even back then I've learned the feeling of mundane tasks creeping into my tinkering (and gaming) flow: being on Windows, I would want to avoid the day, when the systems would need a reinstall just because the software decided to irrecoverably rot. A tiresome process, followed by an even more tiresome process of installing and configuring the applications to a good-enough state resembling that of the previous system. It's important to mention, that it comes with a feeling that you are not in control of your system2, and in turn not in control of your time. A famous series3 tells us the story of taking back the control. Now, the question is, how much control is one able to take back, what exactly that control is, and what tools are out there to help us? This is where the NixOS enters the stage.

Before jumping to NixOS itself, let's have a metasurvey of the environment. More importantly, let's take a loon on the articles you may have read already, maybe tried to follow, on the scattered all over the place documentation, on the huge single page NixOS manual, and on the inherent problems with those. Telling, that the documentation is one of the weakest points of NixOS is nothing new4. Now, what makes its documentation and the likes so unaproachable? One of the biggest issues I would like to mention, is that the vast majority of NixOS materials bake-in too much of assumption about prior Linux- and NixOS-related knowledge, and are not helping in the way, that they do not provide a shortest actionable shortcut to some desired outcome. Authors of those materials prefer to systematically dive into rabbitholes instead of supplying the tutory material with just enough amount of concepts needed for immediate use to achieve a task or resolve an issue. There is often no task in mind as well. Neither is there an approachable entrypoint -- where exactly should a novice begin their way? Such materials start with explaining the nix language syntax, which is in no way neccessary for the understanding of how to build and deploy your first customized system, start implementing complex derivations, stray into devops++ territory by mentioning the hermetic builds, etc. "If you wish to make an apple pie from scratch, you must first invent the universe" vibes. Some authors would start with the theoretical base and suggest early that you may want to read the 267-pages-long PhD thesis of NixOS creator, Dutch computer scientist Eelco Dolstra. I do not in any way want to devalue the work of others; more than that -- I'm very grateful to the community, to all the people, who put such a titanic effort into spreading the word and supporting the NixOS ecosystem, including the documentation. That biased way of presenting things may be inherent to the NixOS itself, which attracts very smart people. Thes are the hackers writing for other hackers. Yes, NixOS is complex5. Nix Expression Language does not make it easier. It's a functional programming language, and the functional programming may be a completely foreign concept to many. In Nix Language, it may be rather hard to start with doing by copying, since the eye of uninitiated programmer may lack devices for pattern-matching within the language. Copy-pasting some stuff verbatim in hopes that it would works is one thing, copy-pasting from multiple sources and trying to stitch something sensible is another. Another issue to mention is the deprecation of tech (NixOS and technology in general), which goes at a very high pace. You may not immediately stumble upon a mention, that nix-env or channels are a no-go, and you simply should not waste your time trying to learn those. And you would be quite happy to learn early, that flakes are the way you should be packaging the applications, or that home-manager is how you should manage user-specific dotfiles.

To give you a clue, NixOS is not that hard. One may compare it to playing a violin vs playing a piano. Those, who play (or tried to) both, would know, that it's very hard to start with the violin. Right from the beginning, you have to accomodate quite a lot of new stuff in your head and muscles, that is even before you can produce a single sound. You get to learn how to support the violin by its neck, as well as hold it by your own neck, how to put your playing hand's fingers on the bow, how to hold the bow, how to move your elbow, how the bow should make contact with the strings. And even after all of that, your first try (and N-th try too, with N being quite large) would produce nothing but some squeaky noises. Contrast that with starting on the piano -- you press a key and, immediately, you get a valid sound! You still have to learn how to properly touch the keyboard (touché), how to sit in front of it. But the feedback is immediate. Those of you, who play piano and/or violin professionally, would also know, that the moment you've grokked the violin past the initial stage, you acquire the pace and start making significant progress in your ability to play it quite fast (assuming you do give in the required hours to practice). Not so with the piano: when you reach a certain point, each improvement, both technical (think Chopin etudes) and expressive -- your ability to convey the message with your performance -- results in diminishing returns. Ubuntu in this case is the piano and NixOS is the violin. After you have spent some time with NixOS, you'll see a near-limitless potential for application, both in your professional and personal venues. You may even suddenly feel an urge to put it on all the devices you own. The gift keeps on giving.

Now, back to the metasurvey, Plato and finally onto the NixOS itself. What also is lacking in the community (despite its best efforts) is a simple statement in layman's terms, why exactly NixOS is the solution to a layman's problem. A pitch deck.

And here it is: NixOS reclaims you the control back. NixOS is the Plato's Ideal. And everything that comes with that. Isolation and stuff6. Why is it important? What does that mean? To me personally, and to many others, it means the following:

  • Never have to fear breaking the system irrecoverably, since rolling back to a previous completely functioning state takes less than a single shell command (all of your previous configurations are actually shown at the boot to be chosen from, so it is really a matter of three key strokes -- Right, Down, Right)
  • Total elimination of software rot, software cruft and configuration drift. You may even want to explore the possibility of building a custom immutable LiveUSB and using it as your daily driver, which would fullfill all your needs
  • Access to the largest pre-packaged applications repository among all the distributions -- more than 80 thousand nixpkgs and counting!
  • Unmatched ease of setting up and tweaking your system with full control and understanding of what is being installed. Each configuration (which is called the "generation") is isolated and idempotent, so it does not care about your previous ones
  • Holistic approach to configuration: your text editor configuration, your browser extensions configuration, your hardware, filesystem, packages, environment configurations are all first-class citizens working together and in accord (with no yaml involved!)

To note, I find it quite magical, that NixOS exists and does what it does (and does that more than well). Remember, how you may have traditionally set up your system? -- Lots of googling, many lines of code. Each person would then have their own collection of imperative procedures (shell scripts and what not), which they either remember by heart and muscle memory, or store in git, or even organize into playbooks with provisioning tools like ansible, or maybe simply copy from numerous tutorials online only to forget those immediately after the script is run and the job is done. The Nix Expression Language shines here in the way that it is the medium (and I dare say the most convenient one) to incorporate all those single-use scripts into a holistic system. All those scripts are pieces of knowledge, and when the medium does not allow it, the knowledge is fractured, not shared, and even lost. With Nix Language, everyone could finally agree on something, and that something brings us together and ignites a desire to contribute. Observing, how others start with nix, I could tell, that it takes about a single week of time with no prior knowledge whatsoever to package an application and make it work on NixOS, in case it hadn't been packaged before. And one may suddenly feel the want to make their work a part of something bigger, so they fearlessly make a PR to nixpkgs. Contrast that with the approach of compiling from sources or installing from a repo in other distributions. Even though you still may have to tweak something here and there, maybe install a library or two, it will most likely work, and most likely on the first try. But you don't get the feeling of it being holistic, it still feels one-shot. Your tacit knowledge is not shared. Intricacies of your implementation, which arise from configuration differences, are never told. NixOS is different. It gives you a universal knowledge sharing language for system configuration. The knowledge is not lost, and more that that, the knowledge is amplified and put into a very neatly sturctured form, which is approachable by others.

These were my musings which I wanted to share, and now, let's get to the hands-on part.

The Hands-on

Prerequisites, KVM

All the code for this article is available at GitHub.

Since this is "NixOS for Apt/Yum Users", we would install NixOS in a virtual machine, with Ubuntu or Fedora as the host system.

Let's ensure we got a working QEMU KVM:

Ubuntu:

sudo apt install \
    qemu-kvm \
    libvirt-daemon-system \
    virtinst \
    libvirt-clients \
    bridge-utils

Fedora:

sudo dnf install \
    qemu-kvm \
    virt-manager \
    libvirt \
    virt-install \
    bridge-utils

After that, complete the configuration with the following commands (for both distributions):

# enable virtualization daemon
sudo systemctl enable libvirtd
sudo systemctl start libvirtd

# add your user to required groups
sudo usermod -aG kvm $USER
sudo usermod -aG libvirt $USER

After QEMU KVM is installed, grab the NixOS 22.05 minimal installation ISO image to hack on.

Start the QEMU KVM and navigate to the installation wizzard:

  • Press the top-left button Create a new virtual machine;
  • The default set Local installation media (ISO image or CDROM) radiobutton is what we need, click Forward;
  • Press Browse..., then Browse Local in the pop-up, and select the ISO image you have just downloaded;
  • Un-tick the box for Automatically detect from the installation media/source at the bottom, type in "generic" into the input field and select Generic default (generic), press Forward;
  • Let's give the machine at least 8000 MiBs of Memory and 4 CPUs to work with. Less is okay too, but we want our machine to be swifty. Press Forward;
  • Enable storage for this virtual machine and Create disk image for the virtual machine are both set by default, use those and type in 60 GiBs into the input field. Press Forward;
  • Now, we're at the last step of the prompt, but do not press Finish as yet. It's very important to tick the Customize configuration before install, or else we wouldn't be able to modify it afterwards.

Since we have ticked the Customize configuration before install box, we would be able to use the UEFI for the boot. Your PC would most likely feature the UEFI (or both UEFI and BIOS with the option to use either of those), and as you may eventually want to replicate the current setup for the actual hardware host, in order to eliminate the differencies between virtualized and physical hardware configuration, we would be using the UEFI instead of the legacy BIOS.

What is UEFI though? And why would an average user pick it over BIOS?

The UEFI (Unified Extensible Firmware Interface) is a specification that defines a software interface between an operating system and platform firmware. UEFI is meant to replace the Basic Input/Output System (BIOS) firmware interface. The UEFI interface is the first thing you see when you turn on your computer. It’s a graphical interface that allows you to choose how you want to boot your computer. The UEFI is designed to be more user-friendly than the legacy BIOS. It also has a built-in boot manager, which makes it easy to choose which operating system you want to boot.

The main advantages of UEFI over BIOS are:

  • UEFI has a much richer and more powerful interface;
  • UEFI is better at handling errors and recovery from boot failures;
  • UEFI provides more security features, such as secure boot and trusted platform module (TPM) support;
  • UEFI is faster and has better support for large hard drives.

Overall, UEFI is a more modern and robust firmware interface that provides more features and better security than BIOS.

After pressing Finish, we see the overview of configured system. We're going to change just one more option: in the Overview pane (the one you should be seeing by default), set the Firmware at the bottom to UEFI x86_64. After that, press Apply, and then the Begin Installation button.

Set Up the root User Password and Log In with SSH

We are greeted by the graphical interface. Press enter to proceed with NixOS installation.

You should now see the shell. The default user nixos has an empty password.

It won't be quite convenient to use the interface provided by the KVM. We won't be able to copy-paste from and to the NixOS terminal, and thus, we should log in with ssh. Ssh is a secure shell protocol that allows you to securely connect to a remote computer. Ssh uses encryption to protect your data and provides a secure connection between two computers. It doesn't matter if our host system and KVM (guest) system are actually on a single machine -- we still would facilitate the connection with ssh.

First, let's set up a password for the NixOS root user, which we would be using to configure and install the system. Type these in the shell:

sudo su # would switch user to `root`
passwd

and then you'll be prompted to enter a password. Let's set a simple temporary password, like changeme, for example.

Assuming you have not changed the default network settings in the VM configuration, you should be using a NAT network, which means that your virtualized NixOS guest sees the Ubuntu/Fedora host as a router, and gets a dedicated IP from it.

Type in ifconfig, to see something similar:

# ifconfig
ens3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.122.68  netmask 255.255.255.0  broadcast 192.168.122.255
        inet6 fe80::5054:ff:fe14:e974  prefixlen 64  scopeid 0x20<link>
        ether 52:54:00:14:e9:74  txqueuelen 1000  (Ethernet)
        RX packets 66  bytes 10795 (10.5 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 67  bytes 8388 (8.1 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 80  bytes 6400 (6.2 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 80  bytes 6400 (6.2 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

Alternatively, you may use ip -c address, where ip is a program, which is intended to supersede the "outdated" ifconfig, -c provides the colorful output, and address shows the IPv4 and IPv6 addresses of the available network devices:

# ip -c address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:14:e9:74 brd ff:ff:ff:ff:ff:ff
    altname enp0s3
    inet 192.168.122.68/24 brd 192.168.122.255 scope global dynamic noprefixroute ens3
       valid_lft 2078sec preferred_lft 1456sec
    inet6 fe80::5054:ff:fe14:e974/64 scope link
       valid_lft forever preferred_lft forever

Note the highlighted line in both outputs. It containes the IP-address we'd be using to connect to the guest from the host. Next, in your main host's shell, type in the following:

ssh \
    -o "UserKnownHostsFile=/dev/null" \
    -o "StrictHostKeyChecking=no" \
    root@192.168.122.68

This command would open a guest NixOS shell in your current host shell. Here, we also use two options:

  • -o "UserKnownHostsFile=/dev/null" to tell the ssh programm not to save the NixOS guest's sshd fingerprint, since the fingerprint (the piece of information used to identify the machine you're connecting to) is going to change after the installtion, while staying on the same IP
  • -o "StrictHostKeyChecking=no" so you won't be asked, if you trust the host, since it's on the same machine

Enter the root user's password you've specified (changeme) to enter the NixOS shell.

Partitions and File Systems

Next, we would proceed with partitioning the disk to then install a filesystem.

Now, what's a partition? And what is the difference between a partition and a filesystem?

A partition is just a section of a disk, like a slice in a pie. If you need more than one filesystem on a disk, or if you want to keep different kinds of data in different places, you can have more than one partition on the same disk. Each partition can be formatted with a different filesystem.

A filesystem in turn is what determines how data is organized on the partition. This includes things like the layout of files and how they're named, as well as how the system keeps track of where everything is stored.

Why do we have to use both abstractions though? As it turns out, most operating systems (including Linux) need some kind of partitioning to work. Each partition can only contain one filesystem, but one filesystem can span multiple partitions. For example, you might have a root partition that contains your user data, and a boot partition that contains the files needed to boot the system. These could be on the same disk or on different ones. One advantage of having multiple partitions is that it's more difficult for a single corrupted filesystem to bring down the whole system. If one partition is damaged, you may still be able to boot from another and access your data. Also, some operating systems (including Linux) can use different partitions for different purposes, such as putting frequently accessed files on a separate, faster disk.

If you followed the QEMU setup instructions, you should have a single virtualized disk called sda. Type in lsblk command, a command line utility that lists information about all available or the specified block devices:

# lsblk
NAME  MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
loop0   7:0    0 792.9M  1 loop
sda     8:0    0    60G  0 disk
sr0    11:0    1   825M  0 rom  /iso

We want to create 3 partitions, each with its own purpose and corresponding partlabel:

  • uefi -- contains information on how to start our NixOS system
  • root -- contains the NixOS system itself
  • swap -- provides additional memory in case our system should run low on it

Virtually all systems, including the BSDs, Windows and MacOS come with the tool called fdisk. On NixOS fdisk is an umbrella package featuring 3 flavors:

  • gdisk, the GPT fdisk, an interactive prompt. Running fdisk would actually run gdisk
  • cgdisk, a curses-based GPT fdisk. Curses is a library which provides the functionality of a graphic interface in the console. This is the utility we would be using to better understand the process of the disk partitioning.
  • sgdisk, a command line utility, which (obviously) accepts arguments. One would want to use that in installation scripts, however, I find it quite unituitive for the newcommers when used in tutorials.

There's also a similar tool called GNU parted (parted), which you may have seen already and even used its graphic interface, for example, when installing Ubuntu -- the GNOME Partition Editor (gparted). We would stick to cgdisk though.

There are 4 things, which would define each of our partitions:

  • the patitions' starting sector (block) on our disk
  • the end sector
  • partition type code
  • an optional partlabel

Type in cgdisk /dev/sda to jump into interactive text-based interface.

The boot Partition

Warning! Non-GPT or damaged disk detected! This program will attempt to
convert to GPT form or repair damage to GPT data structures, but may not
succeed. Use gdisk or another disk repair tool if you have a damaged GPT
disk.

                  Press any key to continue....

Ignore the scary message and press space (or any key, really).

                          cgdisk 1.0.8

                      Disk Drive: /dev/sda
                    Size: 125829120, 60.0 GiB

Part. #     Size        Partition Type            Partition Name
----------------------------------------------------------------
            60.0 GiB    free space


    [ Align  ]  [ Backup ]  [  Help  ]  [  Load  ]
    [  New   ]  [  Quit  ]  [ Verify ]  [ Write  ]

              Create new partition from free space

The free space line should be highlighted, navigate to [ New ] with Left and Right arrow keys (or you may simply press N).

Let's start with a uefi parition. You're prompted to enter the first sector, leave it as is and press enter:

First sector (2048-125829086, default = 2048):

Now, type in 512MiB and press enter to make the partition 512-mebibytes-big:

Size in sectors or {KMGTP} (default = 125827039): 512MiB

Next, we should enter the partition type code, we see the following prompt:

Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300):

Type in L and press enter to get list all the codes:

Hex code or GUID (L to show codes, Enter = 8300): L

Type in efi and press enter:

Type search string, or <Enter> to show all codes: efi
ef00 EFI system partition

Press enter once again, to get back to the type code prompt, and enter ef00, which you've learned earlier:

Hex code or GUID (L to show codes, Enter = 8300): ef00

Enter the new partition's name uefi. Note, that the "name" in this context means "partlabel", and we'll use the partlabels to reference the partitions later during the installation setup. However, there is also a "label", which is not equal to the "partlabel". The "label" piece comes from the file system configuration, not from the partition configuration.

Current partition name is ''
Enter new partition name, or <Enter> to use the current name:
uefi

Finally, you'll see:

                          cgdisk 1.0.8

                      Disk Drive: /dev/sda
                    Size: 125829120, 60.0 GiB

Part. #     Size        Partition Type            Partition Name
----------------------------------------------------------------
            1007.0 KiB  free space
   1        512.0 MiB   EFI system partition      uefi
            59.5 GiB    free space


    [ Align  ]  [ Backup ]  [ Delete ]  [  Help  ]
    [  Info  ]  [  Load  ]  [  naMe  ]  [  Quit  ]
    [  Type  ]  [ Verify ]  [ Write  ]
                  Delete the current partition

Good! Don't worry, if you've made a mistake or don't quite follow, we're only preparing the configuration for partitions, and no changes have been written to the disk yet. In case you have to correct a mistake, simply navigate to the newly created partition with the Up and Down arrow keys, and then to the [ Delete ] button with Left and Right (or press D.)

The root Partition

One parititon is configured, two to go. Navigate with the Up and Down arrow keys to the third line with 59.5 GiB free space and press [ New ]:

                          cgdisk 1.0.8

                      Disk Drive: /dev/sda
                    Size: 125829120, 60.0 GiB

Part. #     Size        Partition Type            Partition Name
----------------------------------------------------------------
            1007.0 KiB  free space
   1        512.0 MiB   EFI system partition      uefi
            59.5 GiB    free space


    [ Align  ]  [ Backup ]  [  Help  ]  [  Load  ]
    [  New   ]  [  Quit  ]  [ Verify ]  [ Write  ]

Empty default for the starting sector of the root partition:

First sector (1050624-125829086, default = 1050624):

50GiB for the size:

Size in sectors or {KMGTP} (default = 124778463): 50GiB

Default 8300 type code for the Linux filesystem (empty value, so press enter) :

Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300):

root for the partition label:

Current partition name is ''
Enter new partition name, or <Enter> to use the current name:
root

Now we have:

                          cgdisk 1.0.8

                      Disk Drive: /dev/sda
                    Size: 125829120, 60.0 GiB

Part. #     Size        Partition Type            Partition Name
----------------------------------------------------------------
            1007.0 KiB  free space
   1        512.0 MiB   EFI system partition      uefi
   2        50.0 GiB    Linux filesystem          root
            9.5 GiB     free space


    [ Align  ]  [ Backup ]  [ Delete ]  [  Help  ]
    [  Info  ]  [  Load  ]  [  naMe  ]  [  Quit  ]
    [  Type  ]  [ Verify ]  [ Write  ]
              Change the filesystem type code GUID

The swap Partition

I'll tell you what the Swap is in the next section, but for now, let's set up a partition for it.

Press Down arrow key once to select the 9.5 GiB free space and press [ New ].

Empty default for the starting sector of the swap partition

First sector (105908224-125829086, default = 105908224):

Empty default for the size in sectors (utilizes the entire free space):

Size in sectors or {KMGTP} (default = 19920863):

Since you already know how to find the partition type codes, I'll just tell you straight away: 8200 (not 8300 !) is the code for the Linux swap:

Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): 8200

swap for the partlabel:

Current partition name is ''
Enter new partition name, or <Enter> to use the current name:
swap

It now looks like this:

                          cgdisk 1.0.8

                      Disk Drive: /dev/sda
                    Size: 125829120, 60.0 GiB

Part. #     Size        Partition Type            Partition Name
----------------------------------------------------------------
            1007.0 KiB  free space
   1        512.0 MiB   EFI system partition      uefi
   2        50.0 GiB    Linux filesystem          root
   3        9.5 GiB     Linux swap                swap


    [ Align  ]  [ Backup ]  [ Delete ]  [  Help  ]
    [  Info  ]  [  Load  ]  [  naMe  ]  [  Quit  ]
    [  Type  ]  [ Verify ]  [ Write  ]

Navigate to and press [ Write ], then type in yes to confirm the action:

  Are you sure you want to write the partition table to disk?
(yes or no): yes
              Warning!! This may destroy data on your disk!

The partitioning is complete! Press Q or [ Quit ] to exit the interface back to the shell.

File Systems and Swap

Setting up the file systems is extremely simple, and this is where we reference the freshly created partitions by their respective partlabels:

mkfs.ext4 /dev/disk/by-partlabel/root
mount /dev/disk/by-partlabel/root /mnt

mkfs.fat /dev/disk/by-partlabel/uefi
mkdir /mnt/boot
mount /dev/disk/by-partlabel/uefi /mnt/boot

mkswap /dev/disk/by-partlabel/swap
swapon /dev/disk/by-partlabel/swap

Done!

mkfs, as evident from the name, "makes a file system". EXT4 is the standard file systems you would be using with Linux. FAT, however, is a file system originally coming from DOS. The UEFI specification is based on the FAT file system, thus, we should use mkfs.fat for the uefi parititon. Just FYI, Apple has its own tool (which differs in FAT's implementation), that should be used for the UEFI on Mac, but this is out of the scope of this article.

You may also stumble upon mkfs.vfat command in the wild, so just letting you know, that mkfs.vfat is an outdated version of the same tool and actually symlinks to mkfs.fat, and that would not make any difference in the end.

And what is swap, again?

The swap is a separate part of the computer's hard drive that is used as a temporary storage area when the computer's main memory (RAM) is full. It allows your computer to temporarily move some of its active data to the swap space so that it can continue working on other tasks. It is nice to have some swap, but it is not obligatory.

mount is a command that "mounts" a filesystem under some directory. swapon is similar to mount, except it produces a special type of mount (roughly speaking), which basically tells the system to treat the partition as swap.

Declarative Configuration

So far, so good. Actually, the tedious part is over, and we can finally embrace the power of NixOS.

And the long-awaited command to showcase that power is:

nixos-generate-config --root /mnt

NixOS would create two configuration files: /mnt/etc/nixos/configuration.nix and /mnt/etc/nixos/hardware-configuration.nix. After we edit one of those files, we can begin the installation.

You don't really have to touch the /mnt/etc/nixos/hardware-configuration.nix, since it matches whatever hardware-specific configuration (like the partitions and the files systems, as well as the state of hardware devices themselves) you've got already. If you are ought to change it, say, install a new disk, partition it and apply a file system, you would just run nixos-generate-config again, and your hardware-configuration.nix would be regenerated to reflect the latest changes.

The configuration.nix is the center of NixOS. This is THE declarative configuration, and the actual system mirrors whatever is defined in this configuration. To complete the install, we're going to edit this file and add the following:

  • Set up the bootloader;
  • Configure the network;
  • Provision the environment, enable graphics and sound;
  • Provision the users;
  • Enable SSH and generate a key to be used with it;
  • Configure system packages to be installed.

Before we proceed to the configuration, I would like to mention some quirks of the nix lang. It is not quite intuitive, especially for those without a background in functional programming.

The things to mention to a newcommer are:

  1. It may not be obvious or evident from the documentation, but all these are equal to each other:
something.here = "abc";
something.somethingElse = {
    param = True;
};

# equals

something = {
    somethingElse.param = True;
    here = "abc";
};

# equals

something.somethingElse.param = True;
something = {
    here = "abc";
};

That expression above is called the "attribute set" -- the stuff, that is contained within the curly brackets {} and is also accessible via dot-notation.

  1. Quotes are either "like this" or
    ''
        like this for
        multiple lines
    ''

'single quotes' are not a valid expression.

  1. It is very easy to lose a semicolon ; at the end of the expression, so either use a linter (we'll set up one later), or pay the due attention. The last curly bracket } in configuration.nix (or in any configuration file) does not require a semicolon ;.

  2. Lists items are separated by space or newline:

a = [ 2 "foo" true (2+3) ];
somePackages = [
    vim
    git
    curl
];

This is just enough information for the start, so we continue.

Setting Up the Bootloader

A bootloader is a computer program that loads an operating system or some other program from a computer's persistent storage, such as a hard disk drive into the computer's main memory. A bootloader starts up the computer by initializing the hardware and then loading and starting the operating system. For our bootloader, we would go with GRUB2.

GRUB2 (GRand Unified Bootloader 2) is a free and open source bootloader package from the GNU Project. It is used to boot a wide variety of Linux distributions, as well as Microsoft Windows. It is the successor to the GRUB bootloader and is designed to be simple and easy to use. It is also very modular, so it can be easily extended to support new operating systems and new features. There is also some other bootloader called systemd-boot, which we could have used instead, but we'd stick with GRUB2, since it has multiple advantages over systemd-boot:

  • systemd-boot only supports UEFI, while GRUB2 supports both UEFI and BIOS;
  • systemd-boot can only boot from GPT disks, while GRUB2 can boot from both GPT and MBR disks;
  • systemd-boot can only boot Linux kernels, while GRUB2 can boot Linux kernels, as well as other operating systems such as Microsoft Windows.

As you see, GRUB is more robust overall (and so is UEFI we had chosen earlier is more robust than BIOS). Now that we know what a bootloader is, and why we would choose GRUB2 over systemd-boot, let's continue with our configuration.

As was evident from the 1st part, this article assumes no prior knowledge of Linux. To edit the configuration files, you should be using a text-based editor within the terminal. The most popular choices would be either nano, vim or emacs.

nano is the easiest one to start with, so if you're not familiar with the others, you would use nano /mnt/etc/nixos/configuration.nix command to edit the file.

To set up a bootloader on our system, add the following snippet to the configuration.nix (don't worry, I'll show you the final result in the end):

boot.loader = {
  grub = {
    enable = true;
    version = 2;
    device = "nodev";
    efiSupport = true;
  };
  efi = {
    canTouchEfiVariables = true;
    efiSysMountPoint = "/boot";
  };
};

Easy? Not quite, if you do not know this stuff in advance. It doesn't involve any boilerplate or scripting tough, so it is not easy, but is simple.

Networking

You should recall that our VM is NAT-networked and sees the host as its router. The snippet below enables the network manager. You would most likely want to edit the hostname for your NixOS machine.

# networking
networking = {
  networkmanager.enable = true;
  hostName = "yourHostNameGoesHere"; # edit this to your liking
};

Setting Up the Environment, Graphics and Sound

This is also as simple as it gets, ad the following to the configuration.nix:

# QEMU-specific
services.spice-vdagentd.enable = true;
services.qemuGuest.enable = true;

# locales
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
time.timeZone = "America/New_York";
i18n.defaultLocale = "en_US.UTF-8";

# graphics
services.xserver = {
  enable = true;
  # setup the resolution to match your screen
  resolutions = [{ x = 1920; y = 1080; }];
  virtualScreen = { x = 1920; y = 1080; };
  layout = "us"; # keyboard layout
  desktopManager = {
    xterm.enable = false;
    xfce.enable = true;
  };
  displayManager.defaultSession = "xfce";
  autorun = true; # run on graphic interface startup
  libinput.enable = true; # touchpad support
};

# audio
sound.enable = true;
nixpkgs.config.pulseaudio = true;
hardware.pulseaudio.enable = true;

Since we're on QEMU KVM, we've set services.spice-vdagentd.enable = true; and services.qemuGuest.enable = true; to enable the shared buffer between host and guest, so we would be able to use copy-paste seamlessly. The behavior of other options should be evident from their respecvtive names.

Users and SSH and Installation

users.users = {
  theNameOfTheUser = { # change this to you liking
    createHome = true;
    isNormalUser = true;
    extraGroups = [
      "wheel"
    ];
    openssh.authorizedKeys.keys = [
      # we're going to fill this in later
    ];
  };
  root = {
    extraGroups = [
      "wheel"
    ];
  };
};

services.openssh = {
  enable = true;
  kexAlgorithms = [ "curve25519-sha256" ];
  ciphers = [ "chacha20-poly1305@openssh.com" ];
  passwordAuthentication = false;
  permitRootLogin = "no"; # do not allow to login as root user
  challengeResponseAuthentication = false;
  kbdInteractiveAuthentication = false;
};

You remember, that for the initial setup we have used a password authentication to log in with ssh. In the real environment (one, where NixOS would be exposed to the internet) this is prone to bruteforcing and opens up some attack vectors. The above settings for ssh (with the implementation package called openssh provided by the OpenBSD Project, which is considered the de-facto standard implementation) are considered secure, including the limiting selection of algorithms for encryption and key exchange7.

Since we have disabled the password authentication, we should generate a private-public keypair, which would allow us to log in.

In your host system's shell, type in the following:

ssh-keygen \
    -q \
    -N "" \
    -t ed25519 \
    -f ~/.ssh/id_ed25519_for_nixos

Using the ed25519 algorithm, this will generate two files, where:

  • ~/.ssh/id_ed25519_for_nixos is the private key, with which we're going to authenticate ourselves
  • ~/.ssh/id_ed25519_for_nixos.pub is the public key, that we're going to add to the NixOS configuration

We will put the contents of the public key (~/.ssh/id_ed25519_for_nixos.pub) inside configuration.nix. First, read the contents on your host system (be sure that you are reading the key ending in .pub):

cat ~/.ssh/id_ed25519_for_nixos.pub

You should get something like this as the output in console:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBv9RyYuStPdBzst/sO5Uiik9zS6Lu/K98T2PJROMc/r yourHostUser@your-host-system

Now, copy this output to the configuration.nix (you may omit the names of the user and the host, as they play no role in this configuration):

openssh.authorizedKeys.keys = [
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBv9RyYuStPdBzst/sO5Uiik9zS6Lu/K98T2PJROMc/r"
];

It's okay to store the contents of the public key in plaintext, since your private key stays secure with you. The public key is used to validate, that you are indeed in control of the secret key.

Done!

Initial System Packages

Now, we're going to add some packages (the applications) to our system. Don't worry, we will add more packages after the installation, so this initial batch here is just to showcase the feature.

environment.systemPackages = with pkgs; [
  # cli utils
  git
  curl
  wget
  vim
  htop

  # browser
  chromium
];

Very simple! To the installation!

The Installation

Our /mnt/etc/nixos/configuration.nix file should look like this:

{ config, pkgs, ... }:

{
  imports =
    [
      ./hardware-configuration.nix
    ];

  boot.loader = {
    grub = {
      enable = true;
      version = 2;
      device = "nodev";
      efiSupport = true;
    };
    efi = {
      canTouchEfiVariables = true;
      efiSysMountPoint = "/boot";
    };
  };

  networking = {
    networkmanager.enable = true;
    hostName = "yourHostNameGoesHere"; # edit this to your liking
  };

  # QEMU-specific
  services.spice-vdagentd.enable = true;
  services.qemuGuest.enable = true;

  # locales
  # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
  time.timeZone = "America/New_York";
  i18n.defaultLocale = "en_US.UTF-8";

  # graphics
  services.xserver = {
    enable = true;
    resolutions = [{ x = 1920; y = 1080; }];
    virtualScreen = { x = 1920; y = 1080; };
    layout = "us"; # keyboard layout
    desktopManager = {
      xterm.enable = false;
      xfce.enable = true;
    };
    displayManager.defaultSession = "xfce";
    autorun = true; # run on graphic interface startup
    libinput.enable = true; # touchpad support
  };

  # audio
  sound.enable = true;
  nixpkgs.config.pulseaudio = true;
  hardware.pulseaudio.enable = true;

  # user configuration
  users.users = {
    theNameOfTheUser = { # change this to you liking
      createHome = true;
      isNormalUser = true;
      extraGroups = [
        "wheel"
      ];
      openssh.authorizedKeys.keys = [
        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBv9RyYuStPdBzst/sO5Uiik9zS6Lu/K98T2PJROMc/r"
      ];
    };
    root = {
      extraGroups = [
        "wheel"
      ];
    };
  };

  # ssh
  services.openssh = {
    enable = true;
    kexAlgorithms = [ "curve25519-sha256" ];
    ciphers = [ "chacha20-poly1305@openssh.com" ];
    passwordAuthentication = false;
    permitRootLogin = "no"; # do not allow to login as root user
    challengeResponseAuthentication = false;
    kbdInteractiveAuthentication = false;
  };

  # installed packages
  environment.systemPackages = with pkgs; [
    # cli utils
    git
    curl
    wget
    vim
    htop

    # browser
    chromium
  ];

  system.stateVersion = "22.05";
}

Notice, what the last line says: system.stateVersion = "22.05";. It was generated by nixos-generate-config --root /mnt with rest of the file initially, and we should leave it as is.

Run the following command to begin the installation:

nixos-install

You may now relax and observe a mesmerizing dance of same-length hashes reeling in front of you during the install for some minutes. As the last step, you will also be prompted to enter the desired password for the root user:

Installation finished. No error reported.
setting up /etc...
/etc/tmpfiles.d/journal-nocow.conf:26: Failed to resolve specifier: uninitialized /etc detected, skipping
All rules containing unresolvable specifiers will be skipped.
setting up /etc...
/etc/tmpfiles.d/journal-nocow.conf:26: Failed to resolve specifier: uninitialized /etc detected, skipping
All rules containing unresolvable specifiers will be skipped.
setting root password...
New password:

Enter it (I've used changeme once again) and you may now safely reboot:

reboot

Voila! We have a working NixOS system!

Post-installation

Set User's Password and Log In to Graphical Desktop Environment

After the reboot, in your QEMU Virtual Machine, you should see a GRUB2 menu (the boot loader). Wait for several seconds, or for the impatient: just press Enter or Right arrow key to select the Default configuration (the only one currently available to us).

You would be greeted by a login menu. However, you won't be able to login here with your normal user just yet, since we had not specified any password for this user during the installation. Is that something to worry about? Absolutely not, since we have configured ssh. We'll set up the password over ssh to then be able to use both ssh and graphical interface.

In your shell on host system you would also see:

[root@nixos:/mnt/etc/nixos]# reboot
Connection to 192.168.122.68 closed by remote host.
Connection to 192.168.122.68 closed.

Use the following command from your main host to connect to NixOS:

ssh theNameOfTheUser@192.168.122.68 -i ~/.ssh/id_ed25519_for_nixos

If case you've somehow missed the information regarding the host fingerprint change and used ssh in the way you may be used to (so, without specifying the -o "UserKnownHostsFile=/dev/null" that I've mentioned earlier) , you would see the following:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
...

Run the following command to remove the fingerprint from the list of known hosts, so you would be to add it again after the succesful connect:

# remove the fingerprint of from the `known_hosts` file, be sure to enter the right IP
ssh-keygen -f "~/.ssh/known_hosts" -R "192.168.122.68"
# connect to NixOS guest via ssh
ssh theNameOfTheUser@192.168.122.68 -i ~/.ssh/id_ed25519_for_nixos

After entering the NixOS shell, type in the following command:

su root

to become root (superuser). Then enter

passwd theNameOfTheUser

where theNameOfTheUser is obviously the name of the user you have used inside configuration.nix. Enter the password you want to use for this user (twice). Done! You may now log in with graphic interface as well.

Add a Chromium Extension to the System

First, let's tweak our system a bit to explore the configuration possibilities and to learn how NixOS manages the changes in a declarative way.

We have added chromium as a package to be installed, but Chromium API as well as the abstractions predefined with nix lang for the chromium package provide us with the possibility to also specify the desired browser's extensions. This only works with the extensions, which were published to the Chrome Web Store.

Specifying an extension to install is extremely simple: just add programs.chromium and include the hash of extension, which is available from the URL in the Chrome Web Store.

We'll add the all-time classics: the uBlock Origin, and, as you can see from the URL, its hash is cjpalhdlnbpafiamejdnhcphjbkeiagm.

Edit the the configuration with nano /etc/nixos/configuration.nix, where nano is run with sudo or as root. Note, that after the install, the configuration.nix should be found under /etc/nixos/configuration.nix, and not /mnt/etc/nixos/configuration.nix. Add the following snippet before the closing bracket:

programs.chromium = {
  enable = true;
  extensions = [
    "cjpalhdlnbpafiamejdnhcphjbkeiagm" # uBlock Origin
  ];
};

Now, for changes to take the effect, run the following command, again, with sudo or as root:

nixos-rebuild switch

It will instruct NixOS to do 2 things:

  • rebuild the configuration (while preserving the previous ones)
  • apply the new configuration system-wide

When the procedure is finished in a minute or so, you would be able to see the uBlock Origin extension in Chromium.

Add VSCode to System

Now, we'll add the VSCode editor and some of its extensions to the system.

nixpkgs.config.allowUnfree = true;

environment.systemPackages = with pkgs; [
  # cli utils
  git
  curl
  wget
  vim
  htop

  # browser
  chromium

  (vscode-with-extensions.override {
    vscodeExtensions = with vscode-extensions; [
      bbenoist.nix # syntax highlight for .nix files in vscode
    ];
  })
];

Since VSCode is a proprietary application, we should add the nixpkgs.config.allowUnfree = true; line to our configuration.

Note, both the specific package of VSCode (the one, which allows us to install extensions declaratively), as well as the extension itself go into our declarative configuration under environment.systemPackages. The extension we have added is bbenoist.nix, which adds nix language support to VSCode (syntax highlighting and basic punctuation linting).

You should understand, that this abstraction provides access to only those extensions, that were already defined in nixpkgs. Any extension outside of that would be treated as an undefined variable. The full list of defined extensions is available in the package declaration on GitHub

Luckily, we can also declaratively specify any VSCode extension published to the Visual Studio Marketplace. The user experience however is not as smooth as one could wish. Let's take a sample extension available on the Visual Studio Marketplace, but not on nixpkgs: the search-crates-io, an extension for Rust developers. Modify the snippet in the following way:

nixpkgs.config.allowUnfree = true;

environment.systemPackages = with pkgs; [
  # cli utils
  git
  curl
  wget
  vim
  htop

  # browser
  chromium

  (vscode-with-extensions.override {
    vscodeExtensions = with vscode-extensions; [
      bbenoist.nix # syntax highlight for .nix files in vscode
    ] ++ pkgs.vscode-utils.extensionsFromVscodeMarketplace [
      {
        name = "search-crates-io";
        publisher = "belfz";
        version = "1.2.1";
        sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
      }
    ];
];
];

We have specified all the information required to fetch the extension and build it from the source: the name, the publisher, the version and the something called sha256. This sha256 naming however is not quite correct, since it is not sha256, but a sha256-based Subresource Integrity String (SRI).

We cannot know in advance, which exactly SRI should we specify, since it requires NixOS to evaluate the configuration and to try building it. Thus, we first put a placeholder SRI: sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";

Then, we do the usual nixos-rebuild switch for it to error on the incorrect hash and tell us the actual one in the console output:

error: hash mismatch in fixed-output derivation '/nix/store/ymrx4rqznf0y106v739wclmma01d554g-belfz-search-crates-io.zip.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-K2H4OHH6vgQvhhcOFdP3RD0fPghAxxbgurv+N82pFNs=

After that, we again edit the configuration.nix to match the specified and actual hashes:

  (vscode-with-extensions.override {
    vscodeExtensions = with vscode-extensions; [
      bbenoist.nix # syntax highlight for .nix files in vscode
    ] ++ pkgs.vscode-utils.extensionsFromVscodeMarketplace [
      {
        name = "search-crates-io";
        publisher = "belfz";
        version = "1.2.1";
        sha256 = "sha256-K2H4OHH6vgQvhhcOFdP3RD0fPghAxxbgurv+N82pFNs=";
      }
    ];
  })

Now,

nixos-rebuild switch

in console as root or with sudo.

After the changes take effect, and since we're running a graphic desktop environment, in order to find the freshly installed VSCode in the Applications Menu, you may need to log out and log in again with your current user.

After that, we get a working VSCode with 2 extensions, one from nixpkgs, and one from the Visual Studio Marketplace!

Conclusion

In this not so short introduction to NixOS and Nix Expression Language we have learned the basic concepts to get a working NixOS system running. This is very far from being comprehensive, and I have not covered varios topics, which include but are not limited to:

  • deep dive into nix language and nix package manager
  • flakes
  • home-manager
  • packaging a precompiled binary
  • compiling from source
  • setting up dev environments
  • and many other things!

I plan on releasing a series of articles covering the NixOS concepts in detail in the upcoming weeks, and this is the Part I.

You may want to sign up for the newsletter to stay up-to-date about the series progress.

Footnotes

  1. "The Open Society and Its Enemies" by Karl Popper, but this is not the case study for this article

  2. Indeed, you are not, which is more evident by the day. "This PC" is not your computer.

  3. Mr. Robot

  4. Hacker News Search: "nixos documentation"

  5. And I really want to share this recent article about complexity and cognitive load: Cognitive Loads in Programming

  6. Modules, monoliths, and microservices, an article about isolation

  7. Secure Secure Shell

Made it this far? Enjoying the read?
Sign up for the newsletter to stay tuned.