Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Flakes

Flakes are a Nix feature that standardizes how Nix projects declare their inputs and outputs. They solve two problems at once: pinning (every input is locked to an exact revision) and discoverability (every flake exposes a uniform schema of outputs). A flake is just a repository or directory containing a flake.nix file.

Flakes have been available behind an experimental feature flag since 2021 and remain technically experimental. Despite this status, they have been widely adopted and are now the dominant way to structure Nix projects. The “experimental” label reflects ongoing design work rather than instability in practice. There has been ongoing community debate about the pace of stabilization, but for new projects flakes are the recommended approach.

Enabling flakes

Flakes require the nix-command and flakes experimental features. Add the following to /etc/nix/nix.conf or ~/.config/nix/nix.conf:

experimental-features = nix-command flakes

On NixOS this is done declaratively:

nix.settings.experimental-features = [ "nix-command" "flakes" ];

The flake.nix structure

A flake.nix is a Nix file that returns an attribute set with two required keys:

{
  description = "A short description of the flake";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  };

  outputs = { self, nixpkgs }: {
    # outputs go here
  };
}

inputs

inputs declares the flake’s dependencies. Each input is fetched and locked automatically. The most common input is nixpkgs:

inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
};

Input URLs follow the format github:<owner>/<repo>/<branch-or-commit>. Other supported URL schemes include gitlab:, sourcehut:, git+https://, and plain path: references to local directories.

outputs

outputs is a function that receives the evaluated inputs and returns an attribute set of whatever the flake produces. The argument names must match the input names:

outputs = { self, nixpkgs }: {
  packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
  devShells.x86_64-linux.default = ...;
  nixosConfigurations.myhost = ...;
};

The output schema is not strictly enforced, but the Nix tooling recognises certain well-known attributes:

Output attributeDescription
packages.<system>.<name>Buildable packages (nix build)
devShells.<system>.<name>Development shells (nix develop)
apps.<system>.<name>Runnable programs (nix run)
checks.<system>.<name>Test derivations (nix flake check)
nixosConfigurations.<name>NixOS system configurations
nixosModules.<name>Reusable NixOS modules
overlays.<name>Nixpkgs overlays
libLibrary functions

Accessing nixpkgs from a flake

Within outputs, nixpkgs is accessed through nixpkgs.legacyPackages:

outputs = { self, nixpkgs }:
let
  pkgs = nixpkgs.legacyPackages.x86_64-linux;
in
{
  packages.x86_64-linux.mytool = pkgs.callPackage ./mytool.nix { };
};

legacyPackages exists because the full nixpkgs package set does not fit neatly into the packages output schema (which expects one derivation per attribute, whereas nixpkgs contains nested sets). It is the standard way to access nixpkgs packages from a flake output.

To pass config or overlays, import nixpkgs explicitly:

pkgs = import nixpkgs {
  system  = "x86_64-linux";
  config  = { allowUnfree = true; };
  overlays = [ self.overlays.default ];
};

flake.lock

When you first run any nix command against a flake, Nix resolves all inputs to their current revisions and writes flake.lock:

{
  "nodes": {
    "nixpkgs": {
      "locked": {
        "lastModified": 1745000000,
        "narHash": "sha256-...",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "a3a3dda3bacf61e8a39258a0ed9c924eeca8e293",
        "type": "github"
      }
    }
  }
}

The lockfile pins every input to an exact commit and hash. Commit flake.lock alongside flake.nix so that everyone using your repository gets the same nixpkgs revision.

Updating pins

# Update all inputs to their latest revisions
nix flake update

# Update a single input
nix flake update nixpkgs

# Check what changed
git diff flake.lock

Updating is an explicit, reviewable operation — a git diff on flake.lock shows exactly which commits changed.

follows

When a flake has multiple inputs that each depend on nixpkgs, you can end up with several different nixpkgs versions in your closure. The follows keyword redirects an input’s dependency to one you control:

inputs = {
  nixpkgs.url     = "github:NixOS/nixpkgs/nixpkgs-unstable";
  home-manager    = {
    url    = "github:nix-community/home-manager";
    inputs.nixpkgs.follows = "nixpkgs";  # use our nixpkgs, not home-manager's
  };
};

This ensures all inputs share a single nixpkgs version, reducing the number of packages that need to be built or downloaded.

Common issues

Pure evaluation

Flakes are evaluated in pure mode by default: access to <nixpkgs> angle brackets, builtins.currentSystem, and impure environment variables is blocked. Code that relies on these will fail under flakes and needs to be updated to receive values explicitly through function arguments.

System argument

Because flakes evaluate purely, builtins.currentSystem is unavailable. Outputs must be defined per-system explicitly, or a helper such as flake-utils.lib.eachDefaultSystem can generate the boilerplate:

inputs = {
  nixpkgs.url    = "github:NixOS/nixpkgs/nixpkgs-unstable";
  flake-utils.url = "github:numtide/flake-utils";
};

outputs = { self, nixpkgs, flake-utils }:
  flake-utils.lib.eachDefaultSystem (system:
    let pkgs = nixpkgs.legacyPackages.${system};
    in {
      packages.default = pkgs.hello;
    }
  );

Experimental status

Because flakes remain experimental, their interface could change before stabilization. In practice the core schema has been stable for several years and breaking changes are unlikely.