How I Nix

Jan 24, 2022

The earliest commit I could find in my dotfiles repository of me using NixOS is 5th of February, 2020. So I have been running/using NixOS for about that long, which is not a lot. I definitely do not consider myself an expert. That being said, I put in a lot of hours into my NixOS setup. I enjoy everything being neat and tidy.

I will try to explain how this works from the perspective of someone who is familiar with running Linux, but has little to no Nix/NixOS experience. If you're already familiar with NixOS and home-manager and just want to see my setup, the dotfiles repository README might be enough.

## What is Nix?

Nix is a functional programming language mainly used to power nix, the package manager.

NixOS uses the nix package manager, as well as nix the programming language to define and manage an entire Linux distribution, system settings, packages, etc.

## Why NixOS

The most important thing for me is being able to find the exact versions of everything on my system with reasonable certainty, as well as the ability to update them. Of course, some of it might involve some digging, but I don't mind that.

Secondly, I can easily reproduce any of my systems, entirely. My systems run the same version of everything. I've had to reinstall. I accidentally rm -rfed parts of my system. I could trivially get everything system or configuration-related back.

It also makes it easy to have different versions of various software installed (and even eaiser through the use of nix-shell and direnv / lorri).

Another big plus for me is the ease of system configuration. I am not an expert in running Linux systems, and having the most common system options browsable through nixopts makes it a lot easier.

It also makes trying out new things very easy (as long as it's already packaged by somebody else), including trying out new system settings. You can also trivially revert your system to a previous configuration.

However, with all the good, there's also downsides. The main problem is that it's definitley not mainstream so if you don't use relatively popular hardware, software, etc., you might end up having to dig quite a bit.

Another downside is that since NixOS doesn't install anything globally, you will not be able to run pretty much any binary without nixifying it first, which at a minimum means patching its dynamic library paths. The good news is that there's already tools to do that. But it will still not work out of the box.

You can read a bit more about how nix and NixOS work over on the official NixOS how it works page.

## Trying it out

When I tried nix out, I wanted to get a good feel for it, so I jumped straight in by buying an extra disk and installed NixOS on it. It was fairly painless to set everything up, so a few months later I concluded the experiment was successful and migrated it to my main disk.

That being said, you can take it slower by installing nix the package manager.

Another option is installing a NixOS virtual machine.

This post details the first option (although you can easily use the same ideas in a NixOS VM).

## Installing NixOS

The NixOS installation guide is quite good and should get you up and running.

Make sure you read the X Window System section of the configuration and setup a minimal X server unless you are 100% comfortable using the console after the first reboot.

I highly recommend you add a few of the things you need to your initial list of programs in your install configuration.nix:

# Uncomment this if needed (you'll know).
# nixpkgs.config.allowUnfree = true;

environment.systemPackages = [
# or 'emacs' or 'vscode' or whichever editor you prefer
# just make sure you have one
pkgs.vim

# or 'chromium' or 'google-chrome' or whatever
pkgs.firefox
];

You can lookup more programs that you might need over on the NixOS package search.

Note: if a package is named haskellPackages.ghcid, then you'll need to add pkgs.haskellPackages.ghcid.

## Nix Channels, nix-env, and why I avoid them

Nix channels are a way to manage your system and globally installed programs (via nix-env). How this works is essentially, you subscribe to a channel (say, nixos-21.11) and then you can update to the latest released patch by doing nix-channel --update. The problem with that is that it's not easily reversible. Also, nix-env isn't a very pleasant package management experience.

The package search I mentioned earlier shows install instructions using nix-env. I strongly recommend against that.

Luckily, there's a pretty good alternative: we can use niv to point to specific commits in the nixpkgs repository. Those are essentially the same as channels, but we get it written down in a file we control.

We can also have multiple versions pinned for specific software if we need to.

## Pinning your NixOS configuration and system packages

There's a few things we need to do:

1. Setup a pin for nixpkgs
2. Create a shell.nix file to make use of said pin
3. Create a configuration.nix file
4. Set everything up

### Pinning nixpkgs

Create a new directory, grab niv and init the repository.

mkdir nixfiles
cd nixfiles
nix-shell -p niv
niv init

You should now have a nix directory with two files: sources.nix and sources.json. The former contains basic nix code to load everything in the json file, while the latter contains the repository data including the commit SHA each dependency is currently pinned. If you put this file under version control, you can easily revert to a previously known working configuration.

And now run niv show. You should see two dependencies: niv itself and nixpkgs. If nixpkgs is pinned to NixOS/nixpkgs-channels, it means you have an older version of niv and you need to update this dependency by running

niv drop nixpkgs
niv add nixos/nixpkgs --branch nixos-21.11

Note: you should probably use the latest released version available on the nixpkgs repository instead of nixos-21.11.

### Creating a shell.nix file

Next, we need to create a shell.nix file:

let
# Import pinned repositories
sources = import ./nix/sources.nix;
# Grab nixpkgs from there
nixpkgs = import sources.nixpkgs { config.allowUnfree = true; };
in
# Create a shell
nixpkgs.mkShell {
nativeBuildInputs = [
];
# Force this nixpkgs to be available for commands such as
# nix-shell -p <package>
NIX_PATH =
"nixpkgs=${sources.nixpkgs}:nixos-config=/etc/nixos/configuration.nix"; } We can now launch a shell by running nix-shell. You can check that it's working by checking your NIX_PATH value: nix-shell echo$NIX_PATH

And you should see something like nixpkgs=/nix/store/ynv2jfdrw7arx3q4xjir7mn0j2w97pcy-source:nixos-config=/etc/nixos/configuration.nix.

### System configuration file

The easiest way to get started is to copy the nixfiles that you used during the install phase over. They should be a good place to start:

cp /etc/nixos/*.nix .

Without any additional changes, we can now rebuild everything using the new pin.

### Setting everything up

In order for all this to work, we'll need to remove the files from /etc/nixos and symlink to their copies:

sudo rm /etc/nixos/*
sudo ln -s $PWD/configuration.nix /etc/nixos/ And now you are ready to switch to this config using the freshly created pin: nix-shell sudo --preserve-env nixos-rebuild switch You don't need to re-enter the nix-shell if you have not left the one we entered when verifying the NIX_PATH. That's it! You can now update to the latest commit of the current branch by running niv update. You can also switch branches to a different NixOS release. Note that you should also update your configuration.nix's system.stateVersion accordingly. ## Setting up home-manager The above is great for setting up your system, but it's not ideal for config files and user programs. For this, home-manager is the better choice. First off, it does not need root (sudo) rights to change the settings. Secondly, it has a few more options for some programs, and some helpers around things like user services, emails, etc. In order to get started, we'll: 1. Add home-manager to our pins 2. Update our shell.nix accordingly 3. Create a basic home.nix file to start us off ### Pin home-manager We'll start in the same directory where we created the shell.nix file previously, and add a pin for home-manager: niv add home-manager --branch release-21.11 Make sure the branch release matches your NixOS version! ### Update shell file Next, we need to reference this in our shell.nix: let # Import pinned repositories sources = import ./nix/sources.nix; # Grab nixpkgs from there nixpkgs = import sources.nixpkgs { config.allowUnfree = true; }; # Grab home-manager as well home-manager = import sources.home-manager { }; in # Create a shell nixpkgs.mkShell { nativeBuildInputs = [ nixpkgs.niv # grab the latest version of niv ]; NIX_PATH = "nixpkgs=${sources.nixpkgs}:home-manager=${sources.home-manager}:nixos-config=/etc/nixos/configuration.nix"; } Note that we added home-manager to our NIX_PATH. That's all we need to do for now. ### Create our first home nix file Let's create a new home.nix file. I added some ideas of things you might want. Feel free to remove anything you don't care about: { home.packages = [ pkgs.killall pkgs.ripgrep pkgs.wget pkgs.unzip pkgs.zip ]; home.file = { # If you keep these, you'll have to move the files here first. ".config/nvim/init.vim".source = ./init.vim; ".config/nvim/coc-settings.json".source = ./coc-settings.json; }; programs = { # Better 'cat' bat = { enable = true; config = { theme = "TwoDark"; pager = "less -FR"; }; }; # Really useful for auto-running 'shell.nix', see also: lorri direnv = { enable = true; enableBashIntegration = true; enableFishIntegration = true; }; # Better 'ls' exa = { enable = true; enableAliases = true; }; # My favorite shell fish = { enable = true; package = pkgs.fish; interactiveShellInit = '' set fish_color_normal "#a4c337" set fish_color_command "#77c337" set fish_color_quote "#37c393" set fish_color_redirection "#37b5c3" set fish_color_end "#3776c3" set fish_color_error "#c33759" ''; shellAliases = { # exa ls = "${pkgs.exa}/bin/exa";
ll = "${pkgs.exa}/bin/exa -l"; la = "${pkgs.exa}/bin/exa -a";
lt = "${pkgs.exa}/bin/exa --tree"; lla = "${pkgs.exa}/bin/exa -la";

# git
gs = "${pkgs.git}/bin/git status"; # bat cat = "${pkgs.bat}/bin/bat";
};
};

fzf = {
enable = true;
enableBashIntegration = true;
enableFishIntegration = true;
};

git = {
enable = true;
delta.enable = true;
aliases = {
lol = "log --graph --decorate --oneline --abbrev-commit";
lola = "log --graph --decorate --oneline --abbrev-commit --all";
hist =
"log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short";
};
extraConfig = {
init.defaultBranch = "main";
pull.ff = "only";
merge.conflictstyle = "diff3";
};
ignores = [];
};

# Should probably keep this
home-manager = {
enable = true;
};

# This makes it so that if you type the name of a program that
# isn't installed, it will tell you which package contains it.
nix-index = {
enable = true;
enableFishIntegration = true;
enableBashIntegration = true;
};
};
}

You can find all settings for home-manager on their wiki page.

Next, you'll have to symlink this file:

ln -s $PWD/shell.nix$HOME/.config/nixpkgs/home.nix

### Installing home-manager

The first time you install home-manager, you'll have to run:

nix-shell '<home-manager>' -A install

The same command needs to be executed if you update the pin for it.

However, if you just update your configuration file, you can simply run

home-manager switch

## Have fun!

And that's pretty much it! You can find more inspiration in my config files.

Please note I use modules a lot which I have not covered here. I do plan to write a blog post about it later.

Questions? Comments? Tweet them at me or post an issue on this blog's github repo.