Migrating from NixOS channels to Flakes
In a recent episode of Linux Unplugged, Chris remarked that he wanted to move his NixOS configuration to using Flakes. Brent mentioned that his brother, a new NixOS convert, made the jump rather painlessly.
Indeed, some relearning is required before you can wrap your head around flakes, and can be hard to convince yourself that it's worth it to move from something that works to something you have to invest time into – especially when you're not entirely sold on the benefits.
So, why bother?
If you've been around the block with nixos-unstable
, you're probably familiar with broken channel
updates… your system just won't build due to a bug in some package or module somewhere. If you
want to revert back, you have to do some spelunking and channel surgery to get back to a clean
working state (or just boot back to a previous generation depending on what was broken), which kinda
sucks.
Worse yet, imagine that your functional config hops in a time machine and needs to be applied to a fresh machine. How do you get back to that chewy fudge brownie of a system you once had? Well, assuming you can't borrow the time machine your config used, you're going to have to resort to git repo archaeology and guesswork.
It ain't great. Ask me how I know.
Flakes are a chewy fudge brownie
Flakes enable some pretty killer scenarios, but the most basic (and to my mind most important) is
that the state of nixpkgs
is recorded along with the config via the flake.lock
file. Your meticulous
hand-crafted setup can, like a fine wine, be uncorked at some future date with all of the package
versions intact.
Need to stand up a CI VM like it used to be back in good ol' 2022? Flakes got you.
Ugh, I don't have the ti-
Look, it's really not that hard to start. I'll even throw in some free advice1 for you at the end!
Enable flake support in your current config
Add this to your config and do your nixos-rebuild
routine:
nix.settings.experimental-features = [ "nix-command" "flakes" ];
Start a new git repo
Use GitHub, Gitlab, or whatever if you like… or just do something local-only:
mkdir ~/nixos-conf cd ~/nixos-conf git init
Create your flake.nix
You can start completely from scratch if you like:
nix flake init
…but that's not really why you're here. Instead of staring at a mostly-empty nano
window (I see
you), you can start from a more fully-fledged template. I recommend Misterio77's minimal starter config.
nix flake init -t github:misterio77/nix-starter-config#minimal
The template is pretty thorough, but you'll have to fill out some details appropriate to your
scenario. The template helpfully marks the couple of changes that need to be made with FIXME
and TODO
so
crack open your favorite editor, search for FIXME
and TODO
and get to fixin'. A more detailed treatment of
this can be found at the starter config link mentioned above.
Personally, I don't like managing home-manager
separately from the broader system, so I followed
Misterio77's advice to merge it into my system configs.
Copy in your existing config
cp /etc/nixos/{configuration,hardware-configuration}.nix ~/nixos-conf/nixos
Getting nix
to see your changes
Common gotcha: flakes tooling sees the world through git
. If a file isn't being tracked,
nixos-rebuild
won't reference it.
git add --all
Update your flake.lock
This is roughly equivalent to updating your channel. I'll explain a bit more later.
nix flake update
Time for a test build
nixos-rebuild build --flake .#your-hostname
Note: You can safely ignore any warnings about your build being "dirty" - this just means you have
changes you haven't committed in git
yet.
This test build, if successful, will produce a symlink named result
in the directory you ran the
command from. You can safely rm
this symlink. You might consider adding result
to your .gitignore
(optional).
Drumroll please
sudo nixos-rebuild switch --flake .#your-hostname
Make it permanent
Don't want to lose a good thing, right?
git commit -a -m "first post!" git push # assumes you've got a remote to push to
Next steps
Okay, so now you're running a flake-based install. What now?
Ongoing updates and upgrades
How do you run this thing? Updating/upgrading looks something like this (should look familiar):
- Make edits to the
configuration.nix
in your repo (e.g. to add a new package) nix flake update
<- if you want to pull more recent versions of thingsgit commit -a -m "BAM!" && git push
sudo nixos-rebuild switch --flake .#your-hostname
So what exactly does nix flake update
do?
When you run nix flake update
, each input defined in your flake.nix
is examined and updated.
inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; home-manager.url = "github:nix-community/home-manager"; home-manager.inputs.nixpkgs.follows = "nixpkgs"; };
This defines an input called nixpkgs
backed by a GitHub URL (if you prefer the long form, you can
say "https://github.com/nixos/nixpkgs/tree/nixos-unstable"
). It refers to the nixos-unstable
branch
of the nixpkgs
project in the nixos
organization. The nixos-unstable
branch moves over time as PRs
get merged in the broader project, so how does Nix know what point in time we're supposed to be
using? That's what the flake.lock
is for.
{ "nodes": { "nixpkgs": { "locked": { "lastModified": 1720957393, "narHash": "sha256-oedh2RwpjEa+TNxhg5Je9Ch6d3W1NKi7DbRO1ziHemA=", "owner": "NixOS", "repo": "nixpkgs", "rev": "693bc46d169f5af9c992095736e82c3488bf7dbb", "type": "github" }, "original": { "owner": "NixOS", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } } } }
Flake super powers
If you've decided to use a public GitHub or Gitlab repo as a remote, you can reference your flake(s) directly from other machines. This means that to reinstall your machine completely from scratch, you can boot a vanilla NixOS install image, do your partitioning as normal, then magic:
sudo nixos-install --flake github:YourGithubUsername/YourFlakeRepo#your-hostname
You don't have to clone your repo or futz about pointing curl
at some random URL – you just install
straight from your repo.
Managing multiple systems with one flake
It's easy to adapt your new setup to be useful on more than one machine. Take a look at this section
in your flake.nix
:
nixosConfigurations = { your-hostname = nixpkgs.lib.nixosSystem { specialArgs = {inherit inputs outputs;}; modules = [./nixos/configuration.nix]; }; };
If you had two identical machines that are identically partitioned, you could possibly get away with
doing something like this (depending on how your hardware-configuration.nix
is written):
nixosConfigurations = { your-hostname = nixpkgs.lib.nixosSystem { specialArgs = {inherit inputs outputs;}; modules = [./nixos/configuration.nix]; }; your-second-hostname = nixpkgs.lib.nixosSystem { specialArgs = {inherit inputs outputs;}; modules = [./nixos/configuration.nix]; }; };
But of course, you don't have two identical machines2. You probably have some set of things you
want to share across machines (e.g. installing/configuring your favorite editor) and some things that are
decidedly machine-specific (e.g. networking.hostId
). Here's how to get to the "shared stuff is
shared; machine stuff is split" promised land:
cd ~/nixos-conf # or wherever your repo root is mkdir -p nixos/{your-hostname,your-second-hostname,common} mv nixos/configuration.nix nixos/your-hostname/default.nix mv nixos/hardware-configuration.nix nixos/your-hostname
Note: default.nix
has a special meaning to nix
– if some .nix
expression references a directory
without specifying a filename, default.nix
will be used automatically if it exists.
Create a new file, nixos/common/default.nix
, and pull common config items out of your existing
system's config.
{ inputs, lib, config, pkgs, ... }: { services.emacs.enable = true; # other config you want to share goes here }
Now you reference this common config in each system you want to share with.
nixos/your-hostname/default.nix
:
{ inputs, lib, config, pkgs, ... }: { imports = [ ../common ]; # the rest of your system config goes here }
Repeat this exercise for each system.
Finally, update your flake.nix
:
nixosConfigurations = { your-hostname = nixpkgs.lib.nixosSystem { specialArgs = {inherit inputs outputs;}; modules = [./nixos/your-hostname]; }; your-second-hostname = nixpkgs.lib.nixosSystem { specialArgs = {inherit inputs outputs;}; modules = [./nixos/your-second-hostname]; }; };
Don't forget to add your new files to git
and/or commit them! As noted above, this is a pretty
common gotcha that folks run into when first starting out with flakes. If you're ever seeing error
messages indicating that a file doesn't exist, but you know it does, in all likelihood you've
forgotten to git add
it.
Why do flakes behave this way? Well, part of the point of flakes is to avoid depending on any state
that can vary at build time without being detected – we want that reproducibility. The Nix language
can reference environment variables and arbitrary files, and can be influenced by other outside
state. By limiting the view of the world down to what's in the git
repo, we can greatly improve our
ability to define software that builds exactly the same across machines and in different
environments.
Anyways, now that you've gotten everything in git
, you can manage both machines from the same flake
as you would normally (except, of course, you specify the host name when you nixos-rebuild
).
The End Result
To recap, here's the directory structure you should end up with:
git_repo_root - flake.nix - flake.lock - nixos/ - common/default.nix - your-hostname/ - default.nix - hardware-configuration.nix - your-second-hostname/ - default.nix - hardware-configuration.nix - home-manager/home.nix
And a rough sketch of the files should look something like this:
{ description = "NixOS"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; # or stable, if that's your bag home-manager.url = "github:nix-community/home-manager"; # or pin to a release if you like home-manager.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = { self, nixpkgs, home-manager, ...}@inputs: let inherit (self) outputs; in { nixosConfigurations = { your-hostname = nixpkgs.lib.nixosSystem { specialArgs = {inherit inputs outputs;}; modules = [./nixos/your-hostname]; }; your-second-hostname = nixpkgs.lib.nixosSystem { specialArgs = {inherit inputs outputs;}; modules = [./nixos/your-second-hostname]; }; }; # as noted further up, I prefer merging the home-manager module directly into the host configuration }; }
<autogenerated-file />
{ inputs, lib, config, pkgs, ... }: { imports = [ inputs.home-manager.nixosModules.home-manager ]; # assuming you want home-manager set up the same for every system home-manager = { backupFileExtension = "back"; useGlobalPkgs = true; useUserPackages = true; extraSpecialArgs = { inherit inputs; }; users.yourUserName = import ../../home-manager/home.nix; }; # other config you want to share goes here services.emacs.enable = true; }
{ inputs, lib, config, pkgs, ... }: { imports = [ ./hardware-configuration.nix ../common ]; networking.hostName = "your-hostname"; # all of your config specific to your-hostname }
{ inputs, lib, config, pkgs, ... }: { imports = [ ./hardware-configuration.nix ../common ]; networking.hostName = "your-second-hostname"; # all of your config specific to your-second-hostname }
{ inputs, lib, config, pkgs, ... }: { home = { username = "your-username"; homeDirectory = "/home/your-username"; }; # whatever other home-manager config you like }