Bootstrapping a multi-host NixOS configuration using flakes and home manager
I’ve fallen in love with the idea of Nix and I have decided to take the plunge. I hate how messy and bit rotten my machines can feel after a few months of use, especially with my addiction to setting up development evironments, so I deliberately never take full system backups (everything important is already in the cloud). The ability to cleanly blitz and start again at any point, knowing exactly what was installed and why, and without having to worry about losing subtle tweaks I’ve made, is very attractive.
My plan is to ditch my mess of a Ubuntu partition and shift my daily driver to NixOS. Then create a similarly configured WSL version for when I need/want to be in Windows.
Browsing various dotfile repos gave me a glimpse into what is possible with a flake+home manager set up, but I found the number of techniques a bit overwhelming. So I tried to condence what I could find into the simplest thing that just works.
This is the flake.nix I ended up with:
# ~/dotfiles/flake.nix
{
  description = "NixOS configuration and home-manager configurations";
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-22.05";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };
  outputs = { home-manager, nixpkgs, ...}:
  {
    nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./hosts/nixos/configuration.nix
        home-manager.nixosModules.home-manager {
          home-manager.useUserPackages = true;
          home-manager.users.return12 = {
            imports = [
              ./home/default.nix
            ];
          };
        }
      ];
    };
  };
}
Here hosts/nixos/configuration.nix contains the configuration.nix
created from a fresh install, and home/default.nix holds the packages
and programs I want available to my user account.
# ~/dotfiles/home/default.nix 
{ pkgs, ... }:
{
  imports = [
    # Individual home manager program configurations, e.g.:
    # ./direnv.nix
    # ./zsh.nix
    # ./starship.nix
    # ./neovim.nix
    # ./git.nix
    # ./taskwarrior.nix
  ];
  home.packages = with pkgs; [
    htop
    ack
  ];
  home.stateVersion = "22.05";
  home.sessionVariables = {
    EDITOR = "vim";
  };
}
Once this skeleton was in place it was easy enough to add a second
nixConfigurations entry for the WSL host and start refactoring to extract
commonly used pieces or to separate config when the needs differed. It’s a bit
clunky but I’m hoping the smells I uncover guide me to a much more idiomatic
solution.
These are the steps I followed to get there…
Install NixOS
I decided the that a simple, and non-destructive, first step to bootstrapping my NixOS install would be to install inside a VM. Once happy, I can give up my current Ubuntu partition and install there for real with (hopefully) portable configuration and minimal fuss.
Note: I had to do this under Linux. For some reason booting the NixOS installer failed consistently in Windows
I opted for the graphical installer. The blurb for the download says it will install Plasma but you can opt for no desktop. I decided to go with this because I want to pick my own window manager, etc (I have r/unixporn pretentions).
Test a simple configuration change
Once installed, and rebooted (unmount installation media), and logged in you’ll
be dropped into your barebones instal. Your OS configuration can be found at
/etc/nixos.
configuration.nix contains details on system-wide configuration (hostname and
the user account you setup during installation) and installed packages. It
should be pretty empty but contains some hints on how to go about installing
more. hardware-configuration.nix is automatically generated based on hardward
detected. You can mostly leave it alone.
To prove everything is working as expected make an edit to configuration.nix
sudo nano /etc/nixos/configuration.nix  # File is owned by root so you'll need sudo 
Find the line that lists the installed system packages and uncomment vim.
environmend.systemPackages = with pkgs; [
  vim
];
Once saved you can apply the new configuration.
sudo nix-rebuild switch 
You’ll see a bunch of output which will reward you with the ability to launch vim.
You can stop here and build out your system by editing configuration.nix if
you like.
Enable flake support
Flakes support is still experimental so needs to be explicitly enabled. Add
this to configuration.nix just after the imports definition.
# Enable nix flakes
nix.package = pkgs.nixFlakes;
nix.extraOptions = ''
  experimental-features = nix-command flakes
'';
Then apply with sudo nixos-rebuild switch as before.
Set up your skeleton in version control
# Create homes for our host and home manager configs
mkdir -p ~/dotfiles/hosts/nixos ~/dotfiles/home
# Copy across the existing, auto-generated configuration so they're under
# version control
cp /etc/nixos/configuration.nix ~/dotfiles/hosts/nixos/
cp /etc/nixos/hardware-configuration ~/dotfiles/hosts/nixos/
# Create your multi-host set up. Use the above example changing user/host name
# when needed
vi ~/dotfiles/flake.nix
# Create your default home manager set up. Home manager itself is installed
# system(s)-wide with flake.nix
# Add separate nix files for whichever programs you want and add them to the
# imports list. I recommend setting up a basic git install so you don't need the
# nix-shell (below) in future.
# See https://github.com/jammus/dotfiles/blob/455f08/home/git.nix for a minimal
# git setup.
vi ~/dotfiles/home/default.nix
# Add to version control. Git is required for flakes to work but as we haven't
# yet installed it for real we need to create a temporary shell with it inside
nix-shell -p git 
cd ~/dotfiles
git init && git add .
You can now apply the config from your dotfiles directory with:
nixos-rebuild switch --upgrade --flake '.#' --use-remote-sudo
This will use the nixConfigurations that matches your current hostname. If you
want to be specific (or your hostname doesn’t currently match what’s defined)
you can use:
nixos-rebuild switch --upgrade --flake '.#hostname' --use-remote-sudo
That’s it!
Commit your changes and push somewhere safe. You can then follow similar steps
when adding a new host to your line up. I’d recommend create a home/gui.nix
file to host specific config (window manager, vscode, etc) for desktop machines
e.g.:
# ~/dotfiles/flake.nix
{
  description = "NixOS configuration and home-manager configurations";
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-22.05";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };
  outputs = { home-manager, nixpkgs, ...}:
  {
    nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./hosts/nixos/configuration.nix
        home-manager.nixosModules.home-manager {
          home-manager.useUserPackages = true;
          home-manager.users.return12 = {
            imports = [
              ./home/default.nix
            ];
          };
        }
      ];
    };
    nixosConfigurations.nixos-dekstop = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        ./hosts/nixos-desktop/configuration.nix
        home-manager.nixosModules.home-manager {
          home-manager.useUserPackages = true;
          home-manager.users.return12 = {
            imports = [
              ./home/default.nix
              ./home/gui.nix
            ];
          };
        }
      ];
    };
  };
}
This copy/pasted, one nixosConfigurations per host method will soon get ugly and is
crying out for a refactor. Once you need to do it, jump in.
Potential errors
Some errors I encountered when test driving the above (mostly from missing steps out):
error: creating symlinkduring anixos-rebuildYou need root to apply the system configuration. Prependsudoor add the--use-remote-sudoswitch.error: getting status of '<path>/flake.nix: No such file or directoryFlakes need to be under versionn control, Make sure you have your repository initialised and have added all files references by your config.error: failed to extract archive (Write failed)I got this failuire when nix tried to update flake.lock during a rebuild. It seemed to be in an inconsistent state but addingsudoto the command unstuck me.