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

Pinning Nixpkgs

When you write import <nixpkgs> {}, the version of nixpkgs you get depends on whatever channel happens to be installed on the machine at the time. Two developers running the same expression on different days, or on different machines, may get different packages. Builds stop being reproducible.

Pinning solves this by recording an exact nixpkgs revision — a specific Git commit — alongside your code. Anyone who checks out your repository gets the same nixpkgs, regardless of what channels they have configured.

Avoid pkgs fetchers for pinning

It may be tempting to pin nixpkgs using a fetcher from the package set itself, such as pkgs.fetchFromGitHub. This is strongly discouraged. Package set fetchers are derivations — they are built during the build phase, not during evaluation. Using one to fetch nixpkgs means nixpkgs cannot be imported until that derivation has been built, which forces Nix to perform a build in the middle of evaluation. This is called Import From Derivation (IFD) and it significantly increases evaluation time, prevents some evaluation optimisations, and can break tooling that assumes evaluation is pure. See the Nix manual section on IFD for a full explanation.

Conceptually, you would also need to import nixpkgs to be able to use its build time fetchers, so you now just importing multiple nixpkgs’ unnecessarily.

Always use builtins fetchers or a pinning tool for nixpkgs itself. The builtins fetchers run at evaluation time and do not introduce IFD.

Pinning with fetchTarball

The simplest approach that requires no external tooling is builtins.fetchTarball. GitHub exposes a tarball of any commit, and builtins.fetchTarball verifies it against a hash:

let
  nixpkgs = builtins.fetchTarball {
    url    = "https://github.com/NixOS/nixpkgs/archive/a3a3dda3bacf61e8a39258a0ed9c924eeca8e293.tar.gz";
    sha256 = "0zb9sic985plq8mfs7sfibgbjapzqqxnqzzcsk41fnfxn0bh2qmv";
  };
  pkgs = import nixpkgs { };
in
pkgs.hello

The sha256 hash ensures the tarball has not changed. If it does not match, evaluation fails immediately. To get the hash for a new commit, set it to an empty string and let Nix report the correct value:

sha256 = "";  # Nix will print the correct hash and fail

To update the pin, replace the commit in url with the new one and update the hash.

Pinning tools

Managing fetchTarball pins by hand becomes tedious across multiple inputs. Several tools exist to automate this:

npins

npins is a minimal, file-based pinning tool. It stores pins in a npins/ directory and generates a npins/default.nix you can import:

# Initialise npins in a new project
npins init

# Add nixpkgs at a specific branch
npins add github NixOS nixpkgs --branch nixpkgs-unstable

# Update all pins to their latest commits
npins update

# Update a single pin
npins update nixpkgs
# default.nix
let
  sources = import ./npins;
  pkgs    = import sources.nixpkgs { };
in
pkgs.hello

Flakes

Flakes handle pinning natively through flake.lock, which records the exact revision of every input. This is covered in full in the next section, but if you are starting a new project and are comfortable enabling experimental features, flakes are the most integrated pinning solution available.

Choosing an approach

ApproachRequiresBest for
fetchTarballNothingSingle input, minimal dependencies
npins / niv (deprecated)The tool installedMultiple inputs, no flakes
Flakesnix with experimental featuresNew projects, full reproducibility

The key property all three share is that the pin is a file committed to your repository. Updating nixpkgs becomes an explicit, reviewable change rather than a silent side effect of nix-channel --update.