Intro Flake Config Setup for New NixOS Users

Associated Youtube Video: https://youtu.be/ACybVzRvDhs

Associated Odysee Video: https://odysee.com/@LibrePhoenix:8/you-should-use-flakes-right-away-in:d

Previous Tutorial: Why You Should Use NixOS

Next Tutorial: How to Manage Your Dotfiles the Nix Way with Home Manager

This should introduce you to the basics of setting up a NixOS system, along with getting your configuration setup with a flake, which will be useful when you get into more advanced NixOS stuff.

Tools Needed

  • A familiarity with the Linux command line, like bash and the coreutils
  • At least one of the following:
    • Ability to setup a virtual machine
    • An old-ish computer you’d be willing to install NixOS to
    • A willingess to dive headfirst into the pavement by installing NixOS to your only usable computer and seeing what happens

Install

You know the drill. Go ahead and grab the iso from nixos.org and install it to a machine.

Installing to a VM or non-important computer is recommended. It took me about 3 months of experimenting with NixOS in a VM until my system was in a state that I liked.

Notes if you are installing to a VM:

  • I recommend 100 GB storage minimum, and even more if you can spare it (especially if you make your system as bloated as I do)
    • We need so much storage because when you update the system, everything from before still remains, which allows you to roll back, but your hard drive space can get eaten up pretty quickly…
  • If you can spare RAM and CPU cores, this is good as well, since some of the advanced Nix stuff could have you building some packages from source (NixOS is technically a source-based distro, just like Gentoo)

Quick Start

Once you have NixOS up and running, open a terminal and navigate to /etc/nixos. In it, there should be two files:

/etc/nixos
├── configuration.nix
└── hardware-configuration.nix

hardware-configuration.nix is an auto-generated file specific to your hardware. You typically don’t want to mess with it. Instead we’ll mess with configuration.nix. Go ahead and open it with nano (the only text editor installed by default).

You should see tons of options. Some are pretty self-explanatory, like networking.hostName or time.timeZone. You can customize these now if you want to.

Of more importance is the environment.systemPackages block. To install packages, add them into the list inside of the []:

  # System packages
  environment.systemPackages = with pkgs; [
    vim
    wget
    zsh
    git
  ];

Most package names make sense, like vim and zsh, but there are some packages with non-standard package names. This is the case for many of the Gnome packages, such as gnome.geary.

A tool that I use for finding package names is called MyNixOS. You can simply search for the package, and anything marked as nixpkgs/package is a package. The other things are usually options, which we’ll cover more later.

Once you save and exit out of the configuration.nix file, the new applications won’t be installed automatically. You need to “switch” the system (synchronize the system state with the configuration files). In order to do this, simply execute:

sudo nixos-rebuild switch

At this point (before using a flake, which we’ll cover below), you can update the system with:

sudo nix-channel --update
sudo nixos-rebuild switch

Keep in mind: if you only update the channels, but don’t switch, the system will not be updated. You must run both commands to fully update the system.

Every time you switch, a new generation is created. These are like snapshots of the system state. You will see these generations upon each boot and can select a previous one (in case you mess up the system configuration and it won’t work).

Channels vs. Flakes

The Nix Package Manager has two ways of handling the versions of packages and updates: channels and flakes.

Channels Flakes
Channels are an old way of doing things and are the default (stability) Flakes encompass the “Nix philosophy” a little better, but are considered an “experimental feature” (about as stable as Arch Linux)
Package version information stored in a database separate from config Package version information stored in the flake.lock which can be version controlled with the rest of the config
Importing sources outside of nixpkgs requires you to manually specify a sha256 for every update Flakes automatically retrieve updates, storing new version info in flake.lock

Most people that are really into NixOS are using flakes at this point, meaning that most of the advanced tools and tricks are easier to setup with flakes. Thus, getting a flake setup as quickly as possible is recommended.

Flake Overview

To setup a flake, you will keep your configuration.nix and add an extra file called: flake.nix. The flake.nix points to all of the sources of programs (i.e. nixpkgs) and passes that information to modules such as the configuration.nix, etc…

When you build or update your system using a flake, the flake.lock file will be automatically generated, which keeps track of precise versions (down to the git commit!) of everything on the system.

Pre-flake Preparation

In order to work with flakes, you must add a setting to your configration.nix:

nix.settings.experimental-features = [ "nix-command" "flakes" ];

Then, switch into the configuration again with:

sudo nixos-rebuild switch

Creating Flakes

Although it isn’t necessary, I like to create a directory in my home directory to store all of my dotfiles. In my case, I use ~/.dotfiles. If you do this you’ll need to copy everything from /etc/nixos into this directory like so:

.dotfiles
├── configuration.nix
└── hardware-configuration.nix

The reason I do this is that it keeps my dotfiles close by, and I’m able to track everything with git without needing root.

IMPORTANT NOTE: If you do this, it is advised that make root own the system files (configuration.nix and hardware-configuration.nix). If an attacker got access to your local account and modified these files without your knowledge, then upon your next system rebuild, the attacker could get root access (which is obviously bad).

When we discuss home-manager, you can add files to this directory that are “user-level” rather than “system-level.”

From here, wherever your dotfiles are, create and open a flake.nix file with your favorite text editor.

Flake Structure

A flake has 3 basic components:

  • description: A string describing what the flake is
  • inputs: All of the information about sources
  • outputs: All of the information about what the flake should create with the inputs

Starting out, it could look like this:

{
  description = "My first flake!";

  inputs = {};

  outputs = {};
}

The Inputs Block

The inputs block must point to nixpkgs, which is the standard collection of nix modules that can be used to install normal software. We do this by adding nixpkgs.url to the inputs block:

{
  description = "My first flake!";

  inputs = {
    nixpkgs.url = "PATH TO NIXPKGS";
  };

  outputs = {};

}

nixpkgs.url is actually shorthand for:

nixpkgs = {
  url = "PATH TO NIXPKGS";
};
# equivalent to:
nixpkgs.url = "PATH TO NIXPKGS";

The . simply means “go down one level in the hierarchy.” If you want to get technical, these are nested attribute sets.

nixpkgs.url needs to be set to the actual nixpkgs repo by setting it to github:NixOS/nixpkgs/{BRANCH-NAME} and specifying an actual branch. Recommended branches are the latest stable (nixos-23.05 at the time of writing this) or the unstable branch (nixos-unstable). Putting that all together, you would get:

{
  description = "My first flake!";

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

    # use the following for unstable:
    # nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  };

  outputs = {};

}

Any git repo can be included this way (github:USER-OR-GROUP/REPO/BRANCH, or (gitlab:USER-OR-GROUP/REPO/BRANCH, etc…), but since nixpkgs is considered special to Nix, we can actually shorten this even further by removing the github:NixOS portion of it (since Nix knows where nixpkgs is). This then finally becomes:

{
  description = "My first flake!";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-23.05";

    # use the following for unstable:
    # nixpkgs.url = "nixpkgs/nixos-unstable";

    # or any branch you want:
    # nixpkgs.url = "nixpkgs/{BRANCH-NAME}";
  };

  outputs = {};

}

The Outputs Block

To build a NixOS system, the outputs block requires that you define nixosConfigurations, which can be comprised of multiple configurations, but we’ll just start with one. Most people simply have their output name be the same as their hostname, which reduces the amount of command line flags that are necessary when running nix flake commands. Your configuration will be generated using the nixosSystem function from nixpkgs.lib, which needs two arguments system for the system architecture and modules listing all of the Nix modules to build from. In our case, the only module we need to include right now is: the configuration.nix! Putting this together:

{
  description = "My first flake!";

  inputs = { ... };

  outputs = {
    nixosConfigurations = {
      YOURHOSTNAME = lib.nixosSystem {
        system = "x86_64-linux";
        modules = [ ./configuration.nix ];
      };
    };
  };
}

While it shows the basic structure, the above block by itself will not work, and the reason is that it doesn’t know what lib is. lib is a part of nixpkgs and it is essentially a bunch of convenience functions you can use within your config. lib.nixosSystem is one of these functions! It builds your system for you.

In order for the nixosConfigurations section to have access to lib, we must pass it in as a variable using a let binding, setting lib to nixpkgs.lib:

{
  description = "My first flake!";

  inputs = { ... };

  outputs = { self, nixpkgs, ... }:
    let
      lib = nixpkgs.lib;
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ ./configuration.nix ];
      };
    };
  };
}

This uses a let-in block, which allows us to pass in the definition for lib to be used when defining anything in the outputs. Note that the definition for outputs is the same as before, except we’re passing in the definition of lib as nixpkgs.lib.

Putting it All Together

At this point, your flake.nix should be like this:

{
  description = "My first flake!";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-23.05";

    # use the following for unstable:
    # nixpkgs.url = "nixpkgs/nixos-unstable";

    # or any branch you want:
    # nixpkgs.url = "nixpkgs/{BRANCH-NAME}";
  };

  outputs = { self, nixpkgs, ... }:
    let
      lib = nixpkgs.lib;
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ ./configuration.nix ];
      };
    };
  };
}

Using the Flake

Now that we have a flake, we won’t be using the same command to synchronize the configuration, now, while in the directory with your flake, you can use:

sudo nixos-rebuild switch --flake .#NAMEOFCONFIGURATION

If your hostname matches your configuration name, you can drop the last bit:

sudo nixos-rebuild switch --flake

The Flake Lock and Updating Your Flake

Run ls in your dotfiles dir and you should see a flake.lock file! As mentioned before, this is an automated method of updating sources. Whatever versions are referenced in this flake.lock is what versions the system will be built with.

You can update this by using:

nix flake update

while in the directory with the flake.

Remember: updating the flake by itself does not update the system! You must run sudo nixos-rebuild switch --flake after updating the flake to fully update the system!

You Survived?

Congratulations! You should now have a working NixOS system managed with a flake. What’s next? Find out on the next episode of NixOS Tutorial Z!

Donation Links

If you have found my work to be helpful, please consider donating via one of the following links. Thank you very much!