Migrating NixOS from nix-channels to flakes

Nuuuuuu I didn’t open my nixos laptop for like 3 years and now everything is different and my nixos-rebuild switch is busted and apparently channels aren’t a thing anymore and everyone uses something called flakes??? ;_;;;;;;;;;

I gotta fix my computer

Whining aside, I need to get my system up and running again.

Resurrection plan

Here’s the rough plan:

  1. Update my current nix-channel to something more recent (I’ll choose latest unstable) and iterate running nixos-rebuild switch and fixing config errors until the command succeeds again
  2. Identify what changes I need to make to have my system be “managed by flakes”, then make them
  3. My config files mean nothing to me!!!! Figure out what was going on with my last set up. Specifically, try and identify what I’m managing at the system-level via configuration.nix and what I’m managing at the user-level with home-manager.
  4. Strip away any configuration (programs, ricing options, etc) that I don’t understand or can’t trace back a purpose for
  5. Once configs are cleaned up, add things back in as I need them

The rest of this will mainly be about step 2.

Wait, wtf is flakes

Nix folks are migrating to using an experimental feature called “flakes”1 instead of the officially supported nix-channels to manage where things are downloaded from.

Channels were the mechanism by which you told your NixOS system which version of nixpkgs to use when running updates, and this was managed outside of your nix configurations and resolved at the time of applying your configs. This means builds technically were not hermetic2 and therefore reproducible3 using the old system because the underlying state of inputs couldn’t be guaranteed. Flakes meanwhile introduce version locking for inputs (where nixpkgs is an input) via a flake.lock file, similar to how cargo for rust/pip in python work. This makes builds actually reproducible because the exact version of your inputs are explicitly stated in your nix config files, which allows deterministically resolving the build results.

Note: flakes are still an experimental feature and nix-channels are what is officially support by stable nix (which made navigating the documentation super confusing!). There’s been some drama in the nix community around its adoption into mainline nix, but it seems like folks have largely gone the way of flakes afaict. Therefore I’m choosing to flakes in hopes of future-proofing my set up.

Another note: I’m discussing flakes vs channels here in the context of managing NixOS. I’ve yet to decipher how flakes play with (against?) nix for development, eg with a default.nix or shell.nix. Is it a full replacement? Can/should they be used together? tbd!

Actually migrating to flakes

Enable flakes

Since flakes are still an experimental feature, it needs to be switched on in your nixos configs. Add the line nix.settings.experimental-features = [ "nix-command" "flakes" ] anywhere to your configuration.nix and then rebuild your system with sudo nixos-rebuild switch.

Initial flake.nix

I grabbed a very minimal flake.nix from the thiscute.world guide as a starting point:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  description = "A simple NixOS flake";

  inputs = {
    # NixOS official package source, here using the nixos-unstable branch
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  };

  outputs = { self, nixpkgs, ... }@inputs: {
    # The host with the hostname `lolbox` will use this configuration
    nixosConfigurations.lolbox = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./configuration.nix
      ];
    };
  };
}

If you don’t know Nix language (I’m still learning!), the main takeaways about this flake.nix is that there is an input block and an output block.

Inputs represent what you want to use for your package sources and must follow the input schema4 so that they can be processed into the lockfile properly. The only input we care about right now is nixpkgs. Since nixpkgs is a git source, flakes will auto resolve the latest SHA in flake.lock since we don’t specify one.

In outputs, though flakes support many different output types, the only one we care about atm is the nixosConfiguration output type. You just need to specify your machines hostname on L11 in place of lolbox and make sure your system on L12 is correct.

Actually lets put everything in a git repo

At this point I realize that I want to be doing all of this in my .nixfiles git repo (which can be wherever) rather than /etc/nixos, so let’s move things over. My resulting directory looks something like this:

.nixfiles/
├── configuration.nix
├── flake.lock
├── flake.nix
├── common
│   └── # smaller modules... 
└── hardware
    ├── hardware-configuration.nix
    └── # smaller hardware-specific modules... 

Now I can rebuild my flakes-backed system like this:

sudo nix-rebuild switch --flake ~/.nixfiles#lolbox

Note that you’ll need to git add . your config files before rebuilds will work.

Side quests

Slightly reworking things to be “multihost-forward”

Though I don’t have multiple machines to manage just yet, I’m planning to add some nodes to my home network soon so it’d be nice to organize these configs towards that goal. My multihosted .nixfiles directory structure will look something like this:

.nixfiles/
├── flake.lock
├── flake.nix
└── hosts
    ├── lolbox           
    │    ├── default.nix  # lolbox's "configuration.nix"
    │    ├── hardware-configuration.nix
    │    └── # other hardware-specific modules...
    └── someOtherHost           
        ├── default.nix  # this host's "configuration.nix"
        ├── hardware-configuration.nix
        └── # other hardware-specific modules...

Now in flake.nix I can add multiple hosts like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

  };

  outputs = { self, nixpkgs, ... }@inputs: {
    nixosConfigurations = {
      lolbox = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./hosts/lolbox
        ];
      };
      someOtherHost = nixpkgs.libnixosSystem {
        system = "x86_64-linux";
        modules = [
          ./hosts/someOtherHost
        ];
      };
    };
  };
}

Then when rebuilding NixOS I’d specify the hostname target that I want to specifically run.

Adding home-manager as an input

I know I’ll need to set up home-manager to work with flakes as I continue reorganizing my configs, so let’s do that now. Note that there are multiple ways to do this, but I’ve decided to go the way of declaring home-manager input at system level (i.e., in my top-level flake.nix). This way my home config changes get picked up as part of running sudo nixos-rebuild switch and appear in the list of system generations rather than a separate home-manager generation list. This is nice because I don’t have to keep track of two lists, and I’ll keep it this way until the need arises to split them out.

I add a dir called home in .nixfiles to house any configs that should be managed by home-manager. I can throw configs for things like vim, i3 and other user-level programs in here. My .nixfiles dir now looks like this:

.nixfiles/
├── flake.lock
├── flake.nix
├── home              # dir for home configs
│   ├── default.nix   # entrypoint to home-manager configs
│   ├── i3
│   │   ├── config
│   │   └── default.nix
│   ├── terminal
│   │   ├── default.nix
│   │   ├── tmux.nix
│   │   └── urxvt.nix
│   └── vim.nix
└── hosts
    └── lolbox           
         └── # hardware-specific modules...

Here’s the updated flake.nix:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  description = "A very basic flake";

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

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

  outputs = { self, nixpkgs, home-manager, ... }@inputs: {

    nixosConfigurations = {
      lolbox = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        
        modules = [
          ./hosts/lolbox

          home-manager.nixosModules.home-manager 
          {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.users.tahia = import ./home;
          }
        ];
      };
    };
  };
}

In the inputs block, home-manager gets added as an input source. Then in the outputs block, we pass in the home-manager input as an argument to the output function in L11. On L20 we add home-manager to the modules list, and we specify the module dir on L24.

We got flakes

My NixOS is now managed by flakes! wooo. Theoretically it should now be safe to completely clean up any traces/usage of channels in my system. Onto step 3, 4, and a neverending step 5 :3


  1. My fave noob’s guide to flakes: https://nixos-and-flakes.thiscute.world/ ↩︎

  2. a build is “hermetic” if its unaffected by the environment (ie libraries, compiler versions, etc) it is running in; typically achieved by having the build system refer to “uniquely identifiable” inputs, e.g. a specific version of package identified by a commit SHA ↩︎

  3. a build is “reproducible” if building the same source + inputs always produces the same results. https://reproducible-builds.org/ ↩︎

  4. https://nix.dev/manual/nix/2.18/command-ref/new-cli/nix3-flake.html#flake-inputs ↩︎