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".
The output schema
nixosConfigurations, homeConfigurations, and devShells are three of a
larger set of standard output names. Nix commands know these names by convention,
so naming an output correctly is what lets a command find it. The common ones,
with the command each maps to.
| Output | What it is | Command |
|---|---|---|
packages.<system>.<name> | a buildable package | nix build .#<name> |
apps.<system>.<name> | a runnable program | nix run .#<name> |
devShells.<system>.<name> | a development environment | nix develop .#<name> |
nixosConfigurations.<host> | a NixOS system | nixos-rebuild switch --flake .#<host> |
homeConfigurations.<user> | a Home-Manager config | home-manager switch --flake .#<user> |
nixosModules.<name> | a reusable module other flakes import | imported, not run |
overlays.<name> | a package-set extension | added to nixpkgs.overlays |
formatter.<system> | the code formatter | nix fmt |
checks.<system>.<name> | a test for CI | nix flake check |
templates.<name> | a starter project | nix flake init -t <flake>#<name> |
<system> is a platform string such as x86_64-linux. The system-keyed outputs
exist once per platform you support, which is why you see x86_64-linux repeated
through a flake.
Input reference syntax
Every input is a flake reference, a short URL that tells Nix where to fetch from and which revision to take. Knowing the forms lets you pin an input exactly.
inputs = {
# a GitHub repo, default branch
nixpkgs.url = "github:NixOS/nixpkgs";
# a specific branch, tag, or commit
stable.url = "github:NixOS/nixpkgs/nixos-24.11";
pinned.url = "github:NixOS/nixpkgs/a1b2c3d4e5f6";
# any git remote over https or ssh, with an optional ref
mine.url = "git+https://example.com/me/repo?ref=main";
# a local path, handy while developing a flake
local.url = "path:/home/you/projects/thing";
# a plain tarball
tar.url = "https://example.com/src.tar.gz";
};Some sources are not flakes, meaning they have no flake.nix. Mark them with
flake = false and Nix fetches them as plain source you reference through the
input.
inputs.somePlugin = {
url = "github:author/vim-plugin";
flake = false;
};
# use it later as somePlugin.outPath, or as a derivation srcQuery parameters refine a reference. ?ref= selects a branch or tag, ?rev=
selects a commit, and ?dir= points at a flake in a subdirectory of a repo.
Inspecting a flake
Three commands tell you what a flake contains and whether it is healthy.
# list every output the flake exposes, as a tree
nix flake show
# show the inputs and their locked revisions
nix flake metadata
# evaluate all outputs and run checks, catching errors early
nix flake checknix flake show is the fastest way to see which #name targets exist on a flake
you did not write. The Commands and scripts page collects more
of these.
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.
Stamping a version with self
self is the flake itself, and it carries metadata you can read inside outputs.
This is handy for baking a commit or build date into what you ship.
outputs = { self, nixpkgs, ... }: {
# self.rev is the git commit, set only when the tree is clean
# self.dirtyRev is set instead when there are uncommitted changes
# self.lastModified is a timestamp you can format into a version string
};Reducing per-system boilerplate
Writing x86_64-linux in every output gets repetitive once you support more than
one platform. Two community inputs remove the repetition. flake-utils provides a
helper that maps a function over each system, and flake-parts splits a flake into
modules. Both are optional, and a single-system flake needs neither.
{
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = nixpkgs.legacyPackages.${system}; in {
devShells.default = pkgs.mkShell { packages = [ pkgs.jq ]; };
});
}Offering extra caches from the flake
A flake can request extra binary caches with nixConfig, so anyone who builds it
is offered the prebuilt results instead of compiling. Nix asks for confirmation
before trusting a new cache the first time.
{
nixConfig = {
extra-substituters = [ "https://nix-community.cachix.org" ];
extra-trusted-public-keys = [ "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" ];
};
}The Updating safely page explains how caches speed up builds.
Go deeper
- nix.dev flakes guide , concepts and reasoning.
- NixOS Wiki Flakes page , practical recipes.
- Nix manual nix flake reference , the command reference.