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 -rf
ed 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:
- Setup a pin for
nixpkgs
- Create a
shell.nix
file to make use of said pin - Create a
configuration.nix
file - 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 = [
# grab the latest version of niv
nixpkgs.niv ];
# 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:
- Add
home-manager
to our pins - Update our
shell.nix
accordingly - 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 = [
# grab the latest version of niv
nixpkgs.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";
work = "log --pretty=format:'%h%x09%an%x09%ad%x09%s'";
};
extraConfig = {
init.defaultBranch = "main";
pull.ff = "only";
merge.conflictstyle = "diff3";
};
ignores = [];
userEmail = "your email here";
userName = "your name here";
};
# 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.