Skip to content

Lesson 02 — Flakes

What is a Flake?

A flake is a self-contained, reproducible Nix project. It has:

  • A flake.nix file at the top level of a project folder — not at / (the filesystem root), but at the root of the project directory, e.g. ~/nixproject/flake.nix
  • A flake.lock file that pins every input to an exact commit hash

Think of it like package.json + package-lock.json for the entire system config.

Flakes are technically "experimental" but universally used in practice. All serious configs (including Ian's and Wimpy's) use them.

"Root" in Nix docs always means the top of a project folder, not /. Your system's traditional config is at /etc/nixos/configuration.nix — that's separate. Your flake config lives wherever you put the project, e.g. ~/nixproject/.

Anatomy of a flake.nix

flake.nix
├── description  — human-readable string
├── inputs       — external dependencies (other flakes you pull in)
└── outputs      — a function that receives inputs and returns configs/packages

Inputs are your dependencies — other flakes you rely on. Nix fetches them from their URLs and pins the exact commit in flake.lock.

Outputs is a function: it takes those inputs and returns things that Nix tools know how to consume. The most common outputs and what uses them:

Output key Used by Example command
nixosConfigurations.hostname nixos-rebuild nixos-rebuild switch --flake .#elitebook
homeConfigurations.username home-manager home-manager switch --flake .#marcus
devShells.system.default nix develop nix develop
packages nix build nix build .#mypackage
apps nix run nix run

The . in those commands means "the flake in the current directory". The #elitebook part picks which output to use.

inputs

inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";    # stable channel
  nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
  home-manager = {
    url = "github:nix-community/home-manager/release-25.05";
    inputs.nixpkgs.follows = "nixpkgs";   # use OUR nixpkgs, not home-manager's own pin
  };
};

inputs.nixpkgs.follows = "nixpkgs" is important — it tells home-manager to share our copy of nixpkgs rather than fetching its own. This avoids building against two different versions of nixpkgs.

outputs

outputs = { nixpkgs, home-manager, ... }@inputs:
{
  nixosConfigurations."my-hostname" = nixpkgs.lib.nixosSystem { ... };
  homeConfigurations."my-username" = home-manager.lib.homeManagerConfiguration { ... };
};

The outputs function receives all inputs and returns an attribute set. The keys are conventions that tools know how to read: - nixosConfigurations — hosts that nixos-rebuild will look in - homeConfigurations — configurations that home-manager switch will look in - packages — things nix build can build - devShells — shells that nix develop enters

flake.lock

This is auto-generated. Never edit it by hand. It records the exact git commit for every input.

{
  "nixpkgs": {
    "locked": {
      "rev": "abc123...",
      "type": "github",
      "owner": "NixOS",
      "repo": "nixpkgs"
    }
  }
}

Update it with nix flake update (updates everything) or nix flake update nixpkgs (updates one input).

Ian's Flake — A Clean Example

vendor/dotfiles-main/flake.nix

Key observations:

  1. Two channels — stable (nixos-25.05) and unstable. Unstable is instantiated separately and passed as unstable to modules that need newer packages.

  2. Two outputs — one NixOS host ("nixos") and one Home Manager config ("igray"). Simple and direct.

  3. specialArgs — the vars attribute set (username = "igray"; terminal = "ghostty") is passed down to all modules so they don't hardcode these values. This is the right pattern.

  4. External nixvim — the neovim configuration lives in a separate flake (nixvim-config) and is pulled in as an input. Good example of composing flakes.

# From vendor/dotfiles-main/flake.nix:37
let
  vars = {
    username = "igray";
    terminal = "ghostty";
  };

Wimpy's Flake — Advanced Patterns

vendor/nix-config-main/flake.nix

This is much more complex. Key differences from Ian's:

  1. Many more inputs — SOPS secrets, Disko disk management, Catppuccin theming, custom menus, AI agent tools, and more.

  2. inputs.X.follows everywhere — carefully aligning shared transitive dependencies to avoid version divergence. This matters at scale.

  3. Registry-driven — instead of listing each host by name, it reads from TOML files:

    users = builtins.fromTOML (builtins.readFile ./lib/registry-users.toml);
    systems = builtins.fromTOML (builtins.readFile ./lib/registry-systems.toml);
    
    Then builder.mkAllNixos systems generates all nixosConfigurations automatically.

  4. Cross-platform — produces nixosConfigurations, darwinConfigurations, and homeConfigurations from a single flake.

Common Flake Commands

# Evaluate the flake without building (fast check for syntax errors)
nix flake check

# Show what outputs a flake provides
nix flake show

# Update all inputs
nix flake update

# Update a single input
nix flake update nixpkgs

# Build the NixOS config for a host (without switching)
sudo nixos-rebuild build --flake .#hostname

# Switch to the new NixOS config
sudo nixos-rebuild switch --flake .#hostname --show-trace

# Build home-manager config
home-manager build --flake .#username

# Switch home-manager config
home-manager switch --flake .#username

The .# Syntax

.#hostname means: "look in the flake at . (current directory) for the output named hostname".

sudo nixos-rebuild switch --flake .#nixos → looks for nixosConfigurations.nixos in the flake at ..

Lessons Learned

  • Always use inputs.X.follows to align shared inputs — it prevents subtle version conflicts and reduces build time.
  • flake.lock is what makes Nix reproducible. Commit it. Don't gitignore it.
  • The @inputs pattern (outputs = { ... }@inputs:) gives you access to the full inputs set as a single attribute, useful for passing everything down with inherit inputs.