Intermediate Nix and NixOS

Now that you have successfully driven a NixOS system, let's dive deeper in.

Table of Contents

Nix and NixOS has a lot of features, I will be covering these for now:

All of these features together enables the user to create declarative environments with reproducibility that most of the users praise, favour and want.

Rebuilds and Rollbacks

These are probably very important, and life-changing for usual Linux users. First, let me tell you how to rebuild:

sudo nixos-rebuild switch 
# switch to a newer rebuild based on configuration.nix

sudo nixos-rebuild switch --flake /etc/nixos 
# switch to a newer rebuild based on flake.nix

And secondly, with BTRFS, you can boot into the NixOS systemd boot screen and see previous generations. Or alternatively rollback with:

sudo nixos-rebuild switch --rollback

You can clean generations and unspecified packages(like via nix-shell), with:

sudo nix-collect-garbage -d

And those can be very interesting and useful for even daily usage. As this means that when something breaks, you can instantly rollback to a previous version that did not break. And it does not consume a lot of disk space.

File Strcture

By learning this, you already know most of the differences of NixOS and other traditional linux distros.

First, you have /etc/nixos/ which stores your system-wide configs And, /nix/store/, which stores most of your software

Those are important to know, and NixOS looks at /nix/store, and create symlinks(via ln) to execute software.

Nix-shell

Nix-shell is a tool that you should know, it can be split into these usages via these 2 procedures:

# Open your terminal first, whether GNOME terminal or Ptyxis in KDE
nix-shell -p fastfetch 
# This is nix-shell, it allows you to test tools that can be cleaned via garbage collection
# After some time, you can do fastfetch
fastfetch

# Now you can exit the shell 
exit

# And close the terminal
exit

And when reopening the terminal, you will see that fastfetch is gone, which I have explained, NixOS no longer knows the path and forgets the software, although it is still installed in /nix/store.

Secondly, let's try shell.nix. It is how you define a shell environment, here I will show you a basic example of shell.nix:

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  # Tools installed directly into the shell environment PATH
  packages = with pkgs; [
    nodejs
    git
  ];

  # Environment variables accessible inside the session
  ENV_VAR_EXAMPLE = "hello_world";

  # Commands executed automatically upon entering the shell
  shellHook = ''
    echo "Welcome to your reproducible development environment!"
    node --version
  '';
}

And when in the same directory as where that shell.nix is located at, you can:

nix-shell

And nodejs, git, and all those features will be activated, but upon exiting, nodejs disappears(if not installed globally) However, shell.nix has recently been replaced by flake.nix with flake.lock in most cases.

Nix flake(Project-wide)

Now, flake comes in, but not as system-wide, just like shell.nix but with more features. Here, I will show you an example flake.nix that I use for Raylib:

{
  description = "Raylib C project";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # unstable channel for latest software
  };

  outputs = { self, nixpkgs }:
    let
      system = "x86_64-linux"; # CPU arch
      pkgs = import nixpkgs { inherit system; };
    in
    {
      devShells.${system}.default = pkgs.mkShell {
        packages = [
          pkgs.raylib # for raylib
          pkgs.pkg-config
        ];

        shellHook = ''
          echo "Raylib dev shell ready" 
        ''; # spawns when entering the environment
      };
    };
}
# Enter the dev shell
nix develop

Alternatively, you can install something called direnv, and add a .envrc, which looks like

use flake
# Uses flake.nix automatically

use nix
# Uses shell.nix or default.nix

And run direnv allow to enter the dev shell automatically each time you enter the directory. And this has a lot of features, and things to know. However, for now, I do not have to explain much, and channels are explained later on.

Home manager

Now this is very interesting, it takes Nix even further. It utilizes how a user is defined, for example(/etc/nixos/home.nix):

{ config, pkgs, ... }:

{
  # Home Manager needs a bit of information about you and the
  # paths it should manage.
  home.username = "yourusername";
  home.homeDirectory = "/home/yourusername";

  # This value determines the Home Manager release that your
  # configuration is compatible with. Do not change this value,
  # even if you update Home Manager.
  # This is the original version of installation
  home.stateVersion = "26.05";

  # The home.packages option allows you to install Nix packages into your
  # environment.
  home.packages = [
    pkgs.htop
    pkgs.git
    pkgs.neovim
    pkgs.tmux
  ];

  # Let Home Manager install and manage itself,
  # if on system without `configuration.nix`.
  # Not needed with `flake.nix` or `configuration.nix`.
  # programs.home-manager.enable = true;

  # Enable and configure your shell
  programs.bash = {
    enable = true;
    shellAliases = {
      ll = "ls -l";
    };
  };
}

And that is a short and sweet example of home.nix, however to have functionality of it, you need channels but that will be solved easier with flake.nix system wide. For now I would show you how to add the channel, but do not run that command, if you plan to use flake.nix(with flake.nix, this becomes obsolete, and will likely affect nothing):

nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager # unstable
nix-channel --update

However, it is recommended to use flake.nix, so let's forget that example just now, and move on.

System configuration

This is where you register most of your stuff for the system to work. There are only some things to note:

Nix flake(System-wide)

Now comes the interesting part, flake can also manage a whole system, not only for project-wide, here is what I use in /etc/nixos/flake.nix:

{
  description = "Alexander's NixOS Configuration";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

    home-manager = {
      url = "github:nix-community/home-manager/master";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    nixvim = { # includes NixVim for easy Neovim configuration in home.nix
      url = "github:nix-community/nixvim";
    };
  };

  outputs = { nixpkgs, home-manager, nixvim, ... }:
  {
    nixosConfigurations.nixos = # hostname
      nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";

        modules = [
          ./configuration.nix # links `configuration.nix`

          home-manager.nixosModules.home-manager

          {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;

            home-manager.users.alex = {
              imports = [
                nixvim.homeModules.nixvim # links nixvim
                ./home.nix # links `home.nix`
              ];
            };
          }
        ];
      };
  };
}

This is very minimal, and also uses nixvim to manage my Neovim. And you can rebuild your system using:

sudo nixos-rebuild switch --flake /etc/nixos 
# if you do not have multiple hosts


# with multiple hosts:
sudo nixos-rebuild switch --flake /etc/nixos#hostname 

However, most of the people do not need to have an advanced flake.nix with package tracking, channel tracking and such, but that will be learnt next time. So you can stick with something like this above.

Channels

This is the last part, which is channels. There are 2 types: Unstable(Rolling Release Cycle), and Stable(Point/Fixed-Release Cycle). I daily do use Unstable, but a lot of the times, people utilize how they use channels, eg, for project wide flake.nix, it is almost always on Unstable, while they can have a 26.05 channel system wide. And even more advanced, they can pin packages and allow both for system wide.

And switching channels are easy in flake.nix, you just need to update the URL, to unstable/master, that depends and are fairly easy to know.

Flake Tools

Nix Shell

There are many other tools that are flake related. For example, we used nix-shell earlier, but there is also something as nix shell. Here's an example

nix-shell -p fastfetch # traditional, non-flake

nix shell nixpkgs#fastfetch # new, flake based

nix shell nixpkgs/23.11#neofetch # previous nix channels

nix shell github:aluoty/speedfetch # custom software from github

Nix Profile

Although nix profile is discouraged to use, I will show you what it is. It is the successor of nix-env. Both are imperative tools and act as a traditional package manager. However, nix profile is flake based while nix-env is traditional. nix profile is also user specific.

Nix Search

Nix search helps you search packages easily, for example search for Firefox:

nix search nixpkgs 'firefox'

Nix Run

Nix run allows running software easily, like:

nix run nixpkgs#fastfetch # runs the default fastfetch, install if not installed

nix run github:aluoty/speedfetch # allows custom software from github

Nix Build

Nix build is a build tool for nix, for example if you are developing and hacking the nixpkgs repo, you can use nix build to test your builds:

nix build # local, requires flake.nix in the current directory

nix build nixpkgs#fastfetch # builds fastfetch into ./result in current directory

nix build nixpkgs#fastfetch -o fastfetch-dir # builds fastfetch into ./fastfetch-dir

Closure

This is the end of the intermediate Nix and NixOS skills guide, hope you enjoyed it and learnt something!