Flakes in Practice
What Are Flakes?
Section titled “What Are Flakes?”Flakes are a structured, reproducible way to define Nix projects.
A flake answers three core questions explicitly:
- What are my inputs?
(nixpkgs, other flakes, local paths, Git repositories) - What do I produce?
(packages, dev shells, apps, checks) - How do I make evaluation reproducible?
At a high level, a flake turns a directory into a self-contained, content-addressed unit.
Why flakes exist
Section titled “Why flakes exist”Before flakes:
<nixpkgs>came fromNIX_PATH- Inputs were implicit
- Reproducibility depended on environment state
With flakes:
- Inputs are explicit and pinned
- Paths inside the flake are tracked automatically
- Evaluation is pure by default
- Results are cacheable and shareable
A good mental model:
A flake is a pure function from inputs → outputs.
Anatomy of a flake
Section titled “Anatomy of a flake”A minimal flake has one file:
flake.nixA typical structure looks like:
.├── flake.nix├── flake.lock├── shell.nix (optional, legacy)└── src/flake.nix→ definitionflake.lock→ pinned input versions (generated automatically)
Init Flake Template
Section titled “Init Flake Template”Step 1: Initialize a flake
Section titled “Step 1: Initialize a flake”nix flake initThis creates a starter flake.nix.
A common minimal template looks like this:
{ description = "My first flake";
inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; };
outputs = { self, nixpkgs }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; in { devShells.${system}.default = pkgs.mkShell { packages = [ pkgs.hello ]; }; };}Key ideas:
inputsdeclare where code comes fromoutputsis a function- Everything is keyed by
system
Entering a Development Shell
Section titled “Entering a Development Shell”nix developThis:
- Evaluates the flake
- Builds dependencies if needed
- Drops you into a shell
You should now have hello available:
helloModifying the Shell
Section titled “Modifying the Shell”Let’s make the shell more useful.
Add common tools
Section titled “Add common tools”devShells.${system}.default = pkgs.mkShell { packages = [ pkgs.git pkgs.ripgrep pkgs.go ];};Re-enter the shell:
exitnix developChanges are:
- reproducible
- versioned
- shared automatically
Add Packages as Outputs
Section titled “Add Packages as Outputs”Flakes are not just for shells — they can produce packages.
Add a packages output
Section titled “Add a packages output”outputs = { self, nixpkgs }:let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system};in{ packages.${system}.hello = pkgs.hello;
packages.${system}.default = pkgs.hello;
devShells.${system}.default = pkgs.mkShell { packages = [ pkgs.go ]; };};Now you can:
nix buildor:
nix build .#helloHow to Explore Outputs
Section titled “How to Explore Outputs”nix flake showThis lists:
- packages
- devShells
- apps
- checks
This is your “API surface”.
Exercises
Section titled “Exercises”These exercises are meant to be hands-on and exploratory. You are expected to read errors, search nixpkgs, and iterate.
Exercise 1: Build an open-source Go package using buildGoPackage
Section titled “Exercise 1: Build an open-source Go package using buildGoPackage”Build a Go project from source and expose it as a flake package.
- Pick a small Go project from GitHub (single binary).
- Use
pkgs.buildGoModule(preferred overbuildGoPackage). - Add it under
packages.${system}. - Build it using
nix build.
Skeleton:
packages.${system}.my-go-tool = pkgs.buildGoModule { pname = "my-go-tool"; version = "1.0.0";
src = pkgs.fetchFromGitHub { owner = "..."; repo = "..."; rev = "..."; sha256 = "..."; };
vendorHash = "...";};Exercise 2: Build a Go package from a prebuilt binary
Section titled “Exercise 2: Build a Go package from a prebuilt binary”Package a GitHub release binary instead of building from source.
- Find a GitHub release with Linux binaries.
- Download the binary using
fetchurl. - Install it using
stdenv.mkDerivation. - Expose it as a flake package.
packages.${system}.my-go-bin = pkgs.stdenv.mkDerivation { pname = "my-go-bin"; version = "1.0.0";
src = pkgs.fetchurl { url = "https://github.com/.../releases/download/..."; sha256 = "..."; };
installPhase = '' mkdir -p $out/bin cp $src $out/bin/my-go-bin chmod +x $out/bin/my-go-bin '';};Verify with:
nix run .#my-go-bin3) Package and expose a Python script that lives in your repo
Section titled “3) Package and expose a Python script that lives in your repo”Expose a Python script already present in your repository as a flake package, without generating the script via Nix.
This teaches:
- packaging local sources
- putting repo files in
$out/bin - ensuring a runtime dependency (
python3) is available
Directory structure (example)
Section titled “Directory structure (example)”Create:
.├── flake.nix├── src/│ └── hello_tool/│ ├── hello.py│ └── __init__.pyExample Python script: src/hello_tool/hello.py
Section titled “Example Python script: src/hello_tool/hello.py”#!/usr/bin/env python3
import sysfrom datetime import datetime
def main() -> int: name = sys.argv[1] if len(sys.argv) > 1 else "world" print(f"hello, {name}!") print(f"time: {datetime.now().isoformat(timespec='seconds')}") return 0
if __name__ == "__main__": raise SystemExit(main())Make it executable:
chmod +x src/hello_tool/hello.pyPackage it in flake.nix
Section titled “Package it in flake.nix”Add this output:
packages.${system}.hello-py = pkgs.stdenvNoCC.mkDerivation { pname = "hello-py"; version = "0.1.0";
src = ./src/hello_tool/hello.py; dontUnpack = true;
nativeBuildInputs = [ pkgs.makeWrapper ];
installPhase = '' mkdir -p $out/bin install -m755 $src $out/bin/hello-py
# Ensure python is available at runtime. # The script uses /usr/bin/env python3, so we wrap PATH. wrapProgram $out/bin/hello-py --prefix PATH : ${pkgs.python3}/bin '';};Build and run
Section titled “Build and run”nix build .#hello-py./result/bin/hello-py foobarKey Takeaways
Section titled “Key Takeaways”- Flakes make inputs explicit and reproducible
- Outputs are a typed interface (packages, shells, apps)
- Everything is content-addressed
flake.lockis a feature, not clutter- Flakes scale from small scripts to large systems