Lesson 02 — Flakes¶
What is a Flake?¶
A flake is a self-contained, reproducible Nix project. It has:
- A
flake.nixfile 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.lockfile 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:
-
Two channels — stable (
nixos-25.05) and unstable. Unstable is instantiated separately and passed asunstableto modules that need newer packages. -
Two outputs — one NixOS host (
"nixos") and one Home Manager config ("igray"). Simple and direct. -
specialArgs— thevarsattribute set (username = "igray"; terminal = "ghostty") is passed down to all modules so they don't hardcode these values. This is the right pattern. -
External nixvim — the neovim configuration lives in a separate flake (
nixvim-config) and is pulled in as an input. Good example of composing flakes.
Wimpy's Flake — Advanced Patterns¶
vendor/nix-config-main/flake.nix
This is much more complex. Key differences from Ian's:
-
Many more inputs — SOPS secrets, Disko disk management, Catppuccin theming, custom menus, AI agent tools, and more.
-
inputs.X.followseverywhere — carefully aligning shared transitive dependencies to avoid version divergence. This matters at scale. -
Registry-driven — instead of listing each host by name, it reads from TOML files:
Thenusers = builtins.fromTOML (builtins.readFile ./lib/registry-users.toml); systems = builtins.fromTOML (builtins.readFile ./lib/registry-systems.toml);builder.mkAllNixos systemsgenerates allnixosConfigurationsautomatically. -
Cross-platform — produces
nixosConfigurations,darwinConfigurations, andhomeConfigurationsfrom 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.followsto align shared inputs — it prevents subtle version conflicts and reduces build time. flake.lockis what makes Nix reproducible. Commit it. Don't gitignore it.- The
@inputspattern (outputs = { ... }@inputs:) gives you access to the full inputs set as a single attribute, useful for passing everything down withinherit inputs.