8.7 KiB
Flakes
A flake is a directory containing flake.nix (and, once evaluated, flake.lock). It's a packaging convention on top of regular Nix that gives you:
- Pinned inputs via
flake.lock→ reproducible builds. - A fixed output schema → tools know where to look (
devShells.<system>.default,packages.<system>.default, etc.). - Composability → flakes depend on other flakes by URL.
- Pure evaluation → no silent dependence on
NIX_PATH, env vars, or random files.
Flakes are still marked "experimental" but are overwhelmingly the way new Nix code is written.
1. Anatomy of a flake.nix
{
description = "…"; # optional human-readable summary
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# Pattern: make a transitive input follow yours, to avoid duplicates
flake-utils.url = "github:numtide/flake-utils";
flake-utils.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, flake-utils, ... }: {
# arbitrary attrs here — but names with meaning to `nix` CLI must match the schema
};
}
Two top-level keys matter: inputs and outputs. description is cosmetic.
Inputs
Each input has a flake reference URL. Common forms:
github:owner/repo # latest default branch
github:owner/repo/branch-or-tag # specific ref
github:owner/repo/abc123 # specific commit
git+https://example.com/repo.git
git+ssh://git@example.com/repo
path:./local-flake # local path
tarball+https://…/src.tar.gz
nixpkgs # registry alias (resolves via `nix registry`)
Non-flake sources (a repo without flake.nix):
inputs.some-src = { url = "github:owner/repo"; flake = false; };
# Access raw files via self.inputs.some-src
Outputs
A function: { self, ...inputs }: <attrset>. The attrset keys are conventions the nix CLI knows about:
| Output path | What it is | Command |
|---|---|---|
devShells.<system>.<name> |
A mkShell — dev environment |
nix develop [.#<name>] |
packages.<system>.<name> |
A derivation — build target | nix build [.#<name>] |
apps.<system>.<name> |
{ type = "app"; program = …; } |
nix run [.#<name>] |
nixosConfigurations.<host> |
nixpkgs.lib.nixosSystem { … } |
nixos-rebuild switch --flake .#<host> |
homeConfigurations.<user> |
home-manager config | home-manager switch --flake .#<user> |
nixosModules.<name> |
reusable NixOS module | imported by other flakes |
overlays.<name> |
nixpkgs overlay | imported by other flakes |
formatter.<system> |
a formatter derivation | nix fmt |
checks.<system>.<name> |
derivations run by nix flake check |
nix flake check |
templates.<name> |
scaffold for nix flake init |
nix flake init -t .#<name> |
Default attrs default are picked when you omit the name: nix build ≡ nix build .#default.
.<system> is the platform triple: x86_64-linux, aarch64-linux, aarch64-darwin, x86_64-darwin.
2. The lockfile
flake.lock pins the exact commit + NAR hash of every input, transitively. Generated/updated automatically on first use or nix flake update.
- Commit it. Without it, "my flake" means different things on different machines.
- Update explicitly.
nix flake update(all inputs) ornix flake update nixpkgs(one input). - Inspect.
nix flake metadataprints the resolved URLs and revisions. followsdeduplicates: if bothflake-utilsandfoowant their ownnixpkgs,follows = "nixpkgs";forces them to use yours. Without this, you can end up with multiple nixpkgs copies and subtle version skew.
3. Multi-system support
A flake output has to enumerate systems explicitly. Two common patterns:
(a) Manual forAllSystems
outputs = { self, nixpkgs }:
let
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ];
forAllSystems = f:
nixpkgs.lib.genAttrs systems (system: f (import nixpkgs { inherit system; }));
in {
packages = forAllSystems (pkgs: { default = pkgs.hello; });
devShells = forAllSystems (pkgs: { default = pkgs.mkShell { packages = [ pkgs.jq ]; }; });
};
Zero external deps, explicit.
(b) flake-utils / flake-parts
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs { inherit system; }; in {
packages.default = pkgs.hello;
devShells.default = pkgs.mkShell { packages = [ pkgs.jq ]; };
});
Less boilerplate, but adds a dependency. For bigger projects, flake-parts is increasingly popular — it gives you a module system for flakes themselves.
4. Flake URIs: .#<name> syntax
Everywhere you point nix at a flake, the form is <flake-ref>#<attr>:
nix build . # current dir, packages.<sys>.default
nix build .#foo # packages.<sys>.foo
nix build github:owner/repo#foo # remote flake
nix develop .#ci # devShells.<sys>.ci
nix run nixpkgs#hello # pkgs.hello from the nixpkgs registry flake
nixos-rebuild switch --flake .#my-host
The <system> portion is auto-inferred from your machine — you don't spell it out in the CLI.
5. Pure evaluation — what you lose, what you gain
Flakes evaluate in pure mode:
- No reading
NIX_PATHor<nixpkgs>. - No arbitrary
builtins.getEnvor reading files outside the flake. builtins.currentTime/currentSystemare blocked.- Source is pulled from the flake's input tree, not the working directory, unless you're in
self. Files not tracked by git are invisible to the flake by default.
This last point surprises people: git add a file before nix build can see it. (nix build --impure escapes this, but then you're not reproducible.)
In exchange: anyone with your flake.nix + flake.lock gets bit-identical evaluation.
6. Common commands
nix flake init # scaffold a flake.nix in current dir
nix flake init -t github:nix-community/templates#rust
nix flake show # tree of all outputs
nix flake metadata # locked inputs + paths
nix flake check # evaluate & build all `checks` outputs
nix flake update # bump all inputs
nix flake update nixpkgs # bump one input
nix flake lock --override-input nixpkgs github:NixOS/nixpkgs/nixos-24.11
nix develop # enter devShells.<sys>.default
nix develop .#ci # named shell
nix build # build packages.<sys>.default → ./result
nix run # run apps.<sys>.default (or packages.<sys>.default)
7. Recurring patterns worth recognizing
Rust / Go / Python dev shell
devShells.default = pkgs.mkShell {
packages = [ pkgs.cargo pkgs.rustc pkgs.rust-analyzer ];
env.RUST_LOG = "debug";
shellHook = ''export FOO=bar'';
};
Package a local source tree
packages.default = pkgs.rustPlatform.buildRustPackage {
pname = "my-tool";
version = "0.1.0";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
};
Expose a NixOS module
nixosModules.default = import ./module.nix;
# Consumer does: imports = [ inputs.my-flake.nixosModules.default ];
NixOS system config as a flake
nixosConfigurations.my-laptop = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./configuration.nix ];
};
# Apply with: nixos-rebuild switch --flake .#my-laptop
Overlay as an output
overlays.default = final: prev: {
my-patched-foo = prev.foo.overrideAttrs (old: { … });
};
8. Debugging tips
nix flake show --all-systemsto see outputs for every platform, not just yours.nix eval .#packages.x86_64-linux.default.drvPathto get the.drvwithout building.nix eval --json .#devShells.x86_64-linux.default.buildInputs | jqto inspect shell deps.--show-traceon any command for full eval backtraces.nix repl .loads your flake's outputs into a REPL — great for poking at attrs.- If an input seems stuck on an old rev, check
flake.lock;nix flake update <name>fixes it.
9. When flakes are overkill
- Running a one-off tool:
nix run nixpkgs#ripgrep— no flake needed. - A tiny personal script: plain
default.nix+nix-buildis still fine. - Large monorepos with complex CI: consider
flake-partsor hybrid setups (flake for the interface, regular Nix for guts).
Flakes are a packaging interface, not a requirement. The old non-flake world still works — flakes just compose better once you have more than one Nix project.