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 ];
};
};
}descriptionis a short string summarising the flake.inputsdeclares dependencies by URL. Herenixpkgsis pulled from thenixos-unstablebranch on GitHub. Each input is fetched and pinned.outputsis a function. It receives the resolved inputs and returns what the flake provides.selfis the flake itself,nixpkgsis the input above, andnixosConfigurations.nixos-hostis the build target you pass tonixos-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-hostStep 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 nixpkgsThen 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 withnixos-rebuild switch --flake .#<host>.homeConfigurations.<user>, a Home-Manager configuration. Build it withhome-manager switch --flake .#<user>.devShells.<system>.default, an environment you enter withnix 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 addand 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".
--impurelifts the restriction, but reach for it only when you truly must, because it gives up the guarantee. nix flake checkevaluates your outputs and surfaces errors before you try to switch. Run it after editingflake.nix.- Flakes are still an experimental feature, so commands need flakes enabled. If
nix flakereports 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
| Channels | Flakes | |
|---|---|---|
| Version source | mutable, system wide | pinned in flake.lock |
| Reproducible by default | no | yes |
| Update command | nix-channel --update | nix flake update |
| Depending on other code | awkward | first 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.toplevelThe 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
- nix.dev flakes guide , concepts and reasoning.
- NixOS Wiki Flakes page , practical recipes.
- Nix manual nix flake reference , the command reference.