Flakes

Flakes are why a Ternix config is reproducible. The same files build the same system on any machine, today or a year from now. This is the longest guide in the set because it is the concept most worth understanding, and because once it clicks you can run your whole system from a few files you commit to git. The technical claims here follow the official nix.dev flakes page.

Why flakes exist

Before flakes, nixpkgs came from a channel, a moving pointer that tracked whatever was latest. Two people building the same configuration at different times could get different package versions, and reproducing an old build was hard. A flake instead records the exact version of every dependency in a lock file. Reproducibility stops being something you remember to do and becomes the default. Flakes are also composable, so one flake can list another as an input and build on it.

In practice that means three things you can rely on.

  • Clone your config on a new laptop, run one command, and get the same system.
  • Roll back a bad update by restoring a single file.
  • Share a config that builds the same way for whoever runs it.

Anatomy of flake.nix

A flake is an attribute set with three attributes that matter. This is the flake Ternix generates for a NixOS host.

{
  description = "Ternix-generated flake";
 
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  };
 
  outputs = { self, nixpkgs, ... }: {
    nixosConfigurations.nixos-host = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [ ./configuration.nix ];
    };
  };
}
  • description is a short string summarising the flake.
  • inputs declares dependencies by URL. Here nixpkgs is pulled from the nixos-unstable branch on GitHub. Each input is fetched and pinned.
  • outputs is a function. It receives the resolved inputs and returns what the flake provides. self is the flake itself, nixpkgs is the input above, and nixosConfigurations.nixos-host is the build target you pass to nixos-rebuild --flake .#nixos-host.

The shape worth remembering is that inputs are data you depend on and outputs are a function of that data.

flake.lock

The first time you run a nix command in the flake, Nix writes flake.lock, a record of the exact revision each input resolved to. Nix generates and updates this file automatically, and if your inputs have inputs of their own, it reads their lock files too, so the entire dependency graph is pinned. A lock entry looks roughly like this (flake.lock is JSON).

{
  "nixpkgs": {
    "locked": {
      "owner": "NixOS",
      "repo": "nixpkgs",
      "rev": "a1b2c3d4...",
      "narHash": "sha256-...",
      "type": "github"
    }
  }
}

The rev pins the exact git commit, and narHash is the content hash Nix uses to verify the fetched source has not changed. Together they turn nixos-unstable (a moving branch) into a fixed, verified point. Commit this file. It is the thing that makes a build reproducible. Anyone who clones the repository gets the versions you used, not whatever happens to be current.

Your first flake update, end to end

Here is the full loop you will run whenever you want newer packages. Each step is safe and reversible.

# 1. See what your inputs are currently pinned to
nix flake metadata
 
# 2. Move every input to the latest revision matching its URL
nix flake update
 
# 3. Inspect what changed in the lock file before committing to it
git diff flake.lock
 
# 4. Build and switch into the new system
sudo nixos-rebuild switch --flake .#nixos-host
 
# 5. If something broke, undo the update and rebuild the known-good system
git checkout flake.lock
sudo nixos-rebuild switch --flake .#nixos-host

Step 5 is the safety net that makes updating low risk. The previous flake.lock is the exact set of versions that worked, so restoring it returns you to a build you have already run.

Updating a single input

Pinned does not mean frozen, and you do not have to move everything at once.

# update only nixpkgs, leave other inputs where they are
nix flake update nixpkgs

Then rebuild (Using a config). Updating one input at a time makes it obvious which change caused a regression.

Common outputs

One flake can expose several things. Ternix generates whichever you selected, and each has a command you run against it.

  • nixosConfigurations.<host>, a full NixOS system. Build it with nixos-rebuild switch --flake .#<host>.
  • homeConfigurations.<user>, a Home-Manager configuration. Build it with home-manager switch --flake .#<user>.
  • devShells.<system>.default, an environment you enter with nix develop.

The part after # is the output name. nixos-rebuild --flake .#nixos-host means "build the nixos-host system defined in the flake in the current directory".

Gotchas worth knowing early

  • Flakes only see files that git tracks. If a newly added file seems to have no effect, run git add and rebuild. This is the single most common surprise.
  • Flakes evaluate in pure mode by default, which blocks access to things like environment variables and the network during evaluation. The nix.dev docs note this "promotes a style of writing programs more likely to make them reproducible". --impure lifts the restriction, but reach for it only when you truly must, because it gives up the guarantee.
  • nix flake check evaluates your outputs and surfaces errors before you try to switch. Run it after editing flake.nix.
  • Flakes are still an experimental feature, so commands need flakes enabled. If nix flake reports an error, add this to /etc/nixos/configuration.nix (Ternix configs already include it) and rebuild.
nix.settings.experimental-features = [ "nix-command" "flakes" ];

Flakes compared to channels

ChannelsFlakes
Version sourcemutable, system widepinned in flake.lock
Reproducible by defaultnoyes
Update commandnix-channel --updatenix flake update
Depending on other codeawkwardfirst class via inputs

Advanced

inputs.<name>.follows deduplicates a transitive dependency so two inputs share one nixpkgs rather than pinning two copies. This keeps the closure (the full set of dependencies pulled in) smaller and avoids two slightly different package sets in one build.

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

A flake url can point at a branch, a tag, or a specific commit, which is how you pin hard when you need a build that never moves until you say so.

# track a branch (moves on nix flake update)
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
 
# pin to an exact commit (frozen until you change the rev)
nixpkgs.url = "github:NixOS/nixpkgs/a1b2c3d4e5f6";

You can also build a configuration without switching into it, which is useful in CI or when testing on another machine.

nix build .#nixosConfigurations.nixos-host.config.system.build.toplevel

The nix.dev page is candid that the flakes implementation still has rough edges and ongoing design debate, so expect some commands to feel experimental. That does not stop you using flakes today. The daily workflow above is stable and widely relied on.

Go deeper