Every developer has lived this story: you clone a repo, run the setup script, spend two hours debugging version conflicts, discover the README assumes Ubuntu 20.04 when you’re on 22.04, and eventually get something working that’s subtly different from what CI uses. Six months later, a new team member goes through the same ritual.
Nix solves this by making environments reproducible at the package manager level. A Nix expression specifies exact versions and dependencies — not “python >= 3.11” but a cryptographic hash identifying a specific build. Every developer who applies the same expression gets byte-for-byte identical tooling, regardless of what else is on their machine, what OS they run, or when they set it up.
This guide takes you from first principles through practical use: one-off nix shell invocations, project devShells with flakes, managing your dotfiles with home-manager, and NixOS for those who want the whole system to be reproducible.
What Makes Nix Different
The Nix Store
Everything Nix installs lives in /nix/store/. Each package gets its own directory named by the hash of all its inputs — the source code, compiler version, all dependencies, build flags:
/nix/store/
├── 9xgxjh8b7ank9z4vldgqn0h20x4h2j3k-python3-3.11.8/
├── wr3l9fhv5p2k8q7xy4c6n1m0a3d9e2b1-python3-3.12.2/
├── 4k9p2qz8r7m1x3c6y0w5n8b2a1d4e7f6-nodejs-20.11.0/
├── 7m3q9z2p8r4x1c6y0w5n8b2a1d4e7f6-git-2.44.0/
└── ...
Because the path includes the hash, multiple versions of anything coexist without conflict. Python 3.11 and 3.12 are both installed in different paths. The compiler used to build each package is pinned by its hash. Nothing is ever mutated — new versions get new paths.
When you activate an environment, Nix symlinks the relevant binaries into your $PATH. When you leave the environment, the symlinks go away. The packages themselves stay cached in the store.
Purely Functional
Nix treats package builds like pure functions: same inputs → same outputs, always. There are no shared mutable directories (no /usr/local), no implicit dependencies on system libraries (unless explicitly declared), and no ordering effects. A Nix package that built successfully on your machine builds identically in CI and on your colleague’s laptop.
Three Layers
NixOS — Your entire OS declared in Nix
└── Home Manager — Your user environment (dotfiles, user packages)
└── Nix Flakes — Per-project reproducible devShells
└── Nix — The package manager at the bottom of everything
You don’t need all four layers. Most developers start with Nix as a package manager on their existing OS, add flakes for project environments, and optionally adopt home-manager for their personal setup. NixOS is for those who want to go all the way.
Installation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Linux (single-user install — simplest)
sh <(curl -L https://nixos.org/nix/install) --no-daemon
# Linux (multi-user — recommended, required for some features)
sh <(curl -L https://nixos.org/nix/install) --daemon
# macOS (multi-user required on macOS)
sh <(curl -L https://nixos.org/nix/install)
# Verify
nix --version
# nix (Nix) 2.24.x
# Enable flakes and nix-command (the modern CLI) — add to ~/.config/nix/nix.conf:
echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
|
Nix Shell: Temporary Environments
The fastest way to use Nix: drop into a shell with specific packages available, then leave and they’re gone from your $PATH.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# Temporary shell with a specific package
nix shell nixpkgs#ripgrep
rg --version # works
exit
rg # command not found — clean exit
# Multiple packages
nix shell nixpkgs#nodejs_20 nixpkgs#pnpm nixpkgs#typescript
node --version # v20.x.x
pnpm --version # 9.x.x
tsc --version # 5.x.x
# Run a command directly without entering a shell
nix run nixpkgs#cowsay -- "Nix is reproducible"
# Pin to a specific nixpkgs commit for true reproducibility
nix shell github:NixOS/nixpkgs/nixos-24.11#python312
# Useful for trying tools before committing to them
nix shell nixpkgs#htop nixpkgs#bottom nixpkgs#bandwhich
|
Flakes: The Modern Nix
Flakes are the standardized format for Nix projects. A flake.nix at the root of your repo declares inputs (pinned references to nixpkgs and other flakes) and outputs (devShells, packages, NixOS configurations, home-manager configurations).
Anatomy of a flake.nix
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
# flake.nix
{
description = "My web application development environment";
inputs = {
# Pin nixpkgs to a specific release — everyone gets the same packages
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
# flake-utils: helpers for supporting multiple systems (x86, aarch64, Darwin)
flake-utils.url = "github:numtide/flake-utils";
# Rust toolchain management
rust-overlay.url = "github:oxalica/rust-overlay";
rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; # Use our nixpkgs, not overlay's
# devenv: higher-level devShell abstraction (optional)
devenv.url = "github:cachix/devenv";
};
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default ];
};
in {
# The devShell activated by `nix develop`
devShells.default = pkgs.mkShell {
name = "my-app-dev";
# Packages available in the shell
buildInputs = with pkgs; [
# Language runtimes
nodejs_20
(pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" "rust-analyzer" ];
})
python312
# Build tools
pkg-config
openssl
# Dev tools
git
gh # GitHub CLI
direnv # Auto-activate shell on cd
just # Command runner
sqlx-cli # Database migrations
postgresql_16 # psql client
redis # redis-cli
# Formatters and linters
nixfmt-rfc-style # Nix formatter
nodePackages.prettier
ruff # Python linter
];
# Environment variables set when entering the shell
env = {
DATABASE_URL = "postgresql://localhost:5432/myapp_dev";
REDIS_URL = "redis://localhost:6379";
RUST_LOG = "debug";
RUST_BACKTRACE = "1";
};
# Shell hook: runs when you enter the shell
shellHook = ''
echo "🚀 my-app dev environment"
echo " Node: $(node --version)"
echo " Rust: $(rustc --version)"
echo " Python: $(python3 --version)"
echo ""
# Create local .env if it doesn't exist
if [ ! -f .env ]; then
cp .env.example .env
echo "Created .env from .env.example — fill in your secrets"
fi
# Ensure local postgres data dir exists
export PGDATA="$PWD/.postgres"
if [ ! -d "$PGDATA" ]; then
initdb --locale C --encoding UTF8 "$PGDATA"
echo "Initialized local PostgreSQL in .postgres/"
fi
'';
};
# You can define multiple devShells for different purposes
devShells.ci = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
python312
];
# Minimal shell for CI — no dev tools needed
};
# You can also define packages your project produces
packages.default = pkgs.buildNpmPackage {
name = "my-app";
src = ./.;
npmDepsHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
}
);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# Enter the dev shell
nix develop
# Or: run a command inside the shell without entering it
nix develop --command npm install
# Show what's in the current flake
nix flake show
# Update all inputs to latest (re-pins flake.lock)
nix flake update
# Update only nixpkgs
nix flake update nixpkgs
# Check flake validity
nix flake check
|
The flake.lock File
When you first run nix develop, Nix creates flake.lock — a file pinning every input to an exact commit SHA:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1709340113,
"narHash": "sha256-oqE7PUDpLTwTPR7xBoRIKV8e7lCQiQjhEaDuVEd9+KY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8f7de0dc72b4e22d89d940a05b71cf3e08e11c16",
"type": "github"
}
}
}
}
|
Commit flake.lock to your repo. Anyone who clones it gets exactly the same packages, forever, even years later when nixpkgs has moved on.
direnv: Automatic Shell Activation
Manually running nix develop every time you cd into a project gets old fast. direnv watches for .envrc files and automatically activates/deactivates environments as you change directories.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Install direnv
nix profile install nixpkgs#direnv
# Add to your shell (bash)
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
# Or zsh
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc
# In your project, create .envrc
echo "use flake" > .envrc
direnv allow
# Now: cd into the project → shell activates automatically
# cd out → shell deactivates
|
~/projects/my-app$ cd .
direnv: loading ~/projects/my-app/.envrc
direnv: using flake
🚀 my-app dev environment
Node: v20.11.0
Rust: rustc 1.77.0
Python: Python 3.12.2
~/projects/my-app$ cd ..
direnv: unloading
The use flake directive in .envrc evaluates the flake.nix in the current directory. For faster activation (caches the dev shell derivation), use nix-direnv:
1
2
3
4
5
|
# In your home-manager config or flake.nix
programs.direnv = {
enable = true;
nix-direnv.enable = true; # Caches devshells, much faster re-activation
};
|
Per-Language Deep Dives
Node.js / JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
nodejs_20
nodePackages.pnpm
nodePackages.typescript
nodePackages.typescript-language-server
# Use a specific version pinned in nixpkgs
# To find: nix search nixpkgs nodejs
];
shellHook = ''
# Use local node_modules/.bin in PATH (like npm scripts do)
export PATH="$PWD/node_modules/.bin:$PATH"
'';
};
|
For projects that need very specific npm package versions not in nixpkgs, you can generate a Nix expression from package-lock.json:
1
2
3
4
5
|
# Generate nix expression from package-lock.json
npx node2nix -i package.json -l package-lock.json
# Or for pnpm:
npx pnpm2nix
|
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
let
# Create a Python environment with specific packages
pythonEnv = pkgs.python312.withPackages (ps: with ps; [
# Web framework
fastapi
uvicorn
pydantic
# Database
sqlalchemy
asyncpg
alembic
# Dev tools
pytest
pytest-asyncio
black
ruff
mypy
# The packages not in nixpkgs — install with pip inside the shell
# or use a virtualenv
]);
in
devShells.default = pkgs.mkShell {
buildInputs = [
pythonEnv
pkgs.postgresql_16 # For psycopg2 build deps
];
shellHook = ''
# For packages not in nixpkgs, create a venv on top
if [ ! -d .venv ]; then
python -m venv .venv
fi
source .venv/bin/activate
# Or use uv (much faster)
if [ ! -d .venv ]; then
uv venv
fi
source .venv/bin/activate
uv pip install -r requirements.txt
'';
};
|
Rust
The rust-overlay flake gives precise control over the Rust toolchain:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
rust-overlay.url = "github:oxalica/rust-overlay";
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { nixpkgs, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default ];
};
# Stable Rust with specific components
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [
"rust-src" # For rust-analyzer
"rust-analyzer" # LSP
"clippy" # Linter
"rustfmt" # Formatter
"llvm-tools-preview" # For cargo-llvm-cov
];
targets = [
"wasm32-unknown-unknown" # If building for WASM
"x86_64-unknown-linux-musl" # Static binaries
];
};
# Or: pin to a specific nightly
# rustToolchain = pkgs.rust-bin.nightly."2024-03-01".default;
# Or: use rust-toolchain.toml in your project
# rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
in {
devShells.default = pkgs.mkShell {
buildInputs = [
rustToolchain
# Native deps commonly needed by Rust crates
pkgs.pkg-config
pkgs.openssl
pkgs.openssl.dev
# Faster linker
pkgs.mold
# Useful Cargo extensions
pkgs.cargo-watch # cargo watch -x check
pkgs.cargo-expand # Expand macros
pkgs.cargo-audit # Security audit
pkgs.cargo-nextest # Faster test runner
pkgs.cargo-llvm-cov # Coverage
];
env = {
OPENSSL_DIR = "${pkgs.openssl.dev}";
OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib";
RUST_LOG = "debug";
# Use mold as the linker (faster incremental builds)
RUSTFLAGS = "-C link-arg=-fuse-ld=mold";
};
};
}
);
}
|
Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
go_1_22
gopls # Language server
golangci-lint # Linter suite
gotools # goimports, etc.
delve # Debugger
air # Live reload
sqlc # SQL → Go type-safe queries
buf # Protocol Buffers
protoc-gen-go
protoc-gen-go-grpc
];
env = {
GOPATH = "${toString ./.}/.gopath";
GOFLAGS = "-mod=vendor";
};
shellHook = ''
export PATH="$GOPATH/bin:$PATH"
mkdir -p "$GOPATH"
'';
};
|
devenv: A Higher-Level devShell
devenv wraps Nix flakes with a friendlier API and adds built-in support for starting services (PostgreSQL, Redis, etc.) alongside your development environment:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
# devenv.nix
{ pkgs, lib, config, ... }: {
# Packages
packages = with pkgs; [
git
gh
just
];
# Language support
languages.go = {
enable = true;
version = "1.22";
};
languages.python = {
enable = true;
version = "3.12";
venv.enable = true;
venv.requirements = ./requirements.txt;
};
# Services that start automatically in the dev env
services.postgres = {
enable = true;
listen_addresses = "127.0.0.1";
port = 5432;
initialScript = ''
CREATE DATABASE myapp_dev;
CREATE USER myapp WITH PASSWORD 'devpassword';
GRANT ALL PRIVILEGES ON DATABASE myapp_dev TO myapp;
'';
};
services.redis = {
enable = true;
port = 6379;
};
# Custom scripts available in the shell
scripts.db-reset.exec = ''
psql myapp_dev -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
alembic upgrade head
echo "Database reset complete"
'';
scripts.test.exec = ''
pytest -xvs "$@"
'';
# Environment variables
env.DATABASE_URL = "postgresql://myapp:devpassword@localhost:5432/myapp_dev";
env.REDIS_URL = "redis://localhost:6379/0";
# Pre-commit hooks (runs on git commit)
pre-commit.hooks = {
ruff.enable = true;
black.enable = true;
mypy.enable = true;
};
# Shell init message
enterShell = ''
echo "Services: postgres=$(pg_isready && echo up || echo down) redis=$(redis-cli ping)"
'';
}
|
1
2
3
4
5
6
7
|
# With devenv installed:
devenv shell # Enter the shell (starts services in background)
devenv up # Start services in the foreground (shows logs)
devenv test # Run the checks defined in devenv.nix
# Services use process-compose under the hood
# Logs go to .devenv/processes.log
|
Home Manager: Declarative User Environments
Home Manager applies the Nix model to your entire user environment: packages, dotfiles, shell configuration, editor setup — all declared in Nix, reproducible across machines.
Standalone Setup
1
2
3
4
5
6
7
8
9
10
|
# Add home-manager channel
nix-channel --add https://github.com/nix-community/home-manager/archive/release-24.11.tar.gz home-manager
nix-channel --update
# Install
nix-shell '<home-manager>' -A install
# Generate initial config
home-manager init
# Creates: ~/.config/home-manager/home.nix
|
A Practical home.nix
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
|
# ~/.config/home-manager/home.nix
{ config, pkgs, ... }: {
home.username = "jdoe";
home.homeDirectory = "/home/jdoe";
home.stateVersion = "24.11";
# Enable home-manager to manage itself
programs.home-manager.enable = true;
# ─── Packages ────────────────────────────────────────────────────────────
home.packages = with pkgs; [
# CLI essentials
ripgrep
fd
bat # cat with syntax highlighting
eza # modern ls
fzf # fuzzy finder
zoxide # smart cd
delta # better git diff
dust # du alternative
duf # df alternative
bottom # system monitor
httpie # friendly curl
jq
yq
mkcert
# Dev tools
gh
git-lfs
pre-commit
just
direnv
nix-direnv
# K8s tools
kubectl
kubectx
k9s
helm
kustomize
stern # multi-pod log tailing
# Cloud CLIs
awscli2
google-cloud-sdk
# Fonts
(nerdfonts.override { fonts = [ "JetBrainsMono" "FiraCode" ]; })
];
# ─── Git ─────────────────────────────────────────────────────────────────
programs.git = {
enable = true;
userName = "Jane Doe";
userEmail = "jane@example.com";
signing = {
key = "~/.ssh/id_ed25519.pub";
signByDefault = true;
format = "ssh";
};
delta = {
enable = true;
options = {
navigate = true;
side-by-side = true;
line-numbers = true;
syntax-theme = "Catppuccin-mocha";
};
};
extraConfig = {
init.defaultBranch = "main";
pull.rebase = true;
push.autoSetupRemote = true;
rerere.enabled = true;
branch.sort = "-committerdate";
core.editor = "nvim";
};
aliases = {
st = "status -sb";
lg = "log --oneline --graph --decorate --all";
undo = "reset --soft HEAD~1";
wip = "commit -am 'WIP'";
fixup = "commit --fixup";
};
};
# ─── Shell: Zsh ──────────────────────────────────────────────────────────
programs.zsh = {
enable = true;
enableCompletion = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
history = {
size = 100000;
save = 100000;
share = true;
ignoreDups = true;
ignoreSpace = true;
};
shellAliases = {
ls = "eza --icons";
ll = "eza -la --icons --git";
lt = "eza --tree --icons --level=2";
cat = "bat";
grep = "rg";
top = "btm";
k = "kubectl";
kx = "kubectx";
kn = "kubens";
tf = "terraform";
hm = "home-manager";
};
initExtra = ''
# zoxide (smart cd)
eval "$(zoxide init zsh)"
# fzf key bindings
source ${pkgs.fzf}/share/fzf/key-bindings.zsh
source ${pkgs.fzf}/share/fzf/completion.zsh
# direnv
eval "$(direnv hook zsh)"
# Use fd as fzf's find command
export FZF_DEFAULT_COMMAND="fd --type f --hidden --follow --exclude .git"
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
# Better history search with fzf
export FZF_CTRL_R_OPTS="--sort --exact"
'';
};
programs.starship = {
enable = true;
settings = {
format = "$directory$git_branch$git_status$nix_shell$cmd_duration$line_break$character";
nix_shell = {
format = "[$symbol$name]($style) ";
symbol = "❄️ ";
};
git_status = {
ahead = "⇡$count";
behind = "⇣$count";
diverged = "⇕⇡$ahead_count⇣$behind_count";
modified = "!$count";
untracked = "?$count";
staged = "+$count";
};
};
};
# ─── Neovim ──────────────────────────────────────────────────────────────
programs.neovim = {
enable = true;
defaultEditor = true;
viAlias = true;
vimAlias = true;
plugins = with pkgs.vimPlugins; [
lazy-nvim
nvim-treesitter.withAllGrammars
telescope-nvim
nvim-lspconfig
nvim-cmp
cmp-nvim-lsp
luasnip
catppuccin-nvim
lualine-nvim
gitsigns-nvim
neo-tree-nvim
];
extraConfig = ''
lua require('config.init')
'';
};
# ─── SSH ─────────────────────────────────────────────────────────────────
programs.ssh = {
enable = true;
addKeysToAgent = "yes";
matchBlocks = {
"github.com" = {
identityFile = "~/.ssh/id_ed25519";
identitiesOnly = true;
};
"bastion.prod.*" = {
user = "ubuntu";
identityFile = "~/.ssh/id_ed25519";
forwardAgent = false;
};
};
};
# ─── Direnv ──────────────────────────────────────────────────────────────
programs.direnv = {
enable = true;
nix-direnv.enable = true;
};
# ─── File Templates ──────────────────────────────────────────────────────
# home-manager can manage arbitrary dotfiles
home.file.".config/k9s/skins/skin.yml".source = ./config/k9s-skin.yml;
home.file.".config/bat/config".text = ''
--theme="Catppuccin-mocha"
--style=numbers,changes,header
--italic-text=always
'';
}
|
1
2
3
4
5
6
7
8
9
10
|
# Apply the configuration
home-manager switch
# Roll back to the previous generation
home-manager generations
home-manager rollback
# Update home-manager and all inputs
nix-channel --update
home-manager switch --update-input home-manager
|
Home Manager as a Flake
The modern approach: manage home-manager through a flake for pinned reproducibility:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
# flake.nix — home configuration as a flake
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
home-manager = {
url = "github:nix-community/home-manager/release-24.11";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { nixpkgs, home-manager, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
homeConfigurations."jdoe@workstation" = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
modules = [ ./home.nix ];
};
# Support multiple machines with shared config
homeConfigurations."jdoe@laptop" = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages."aarch64-darwin"; # M-series Mac
modules = [
./home.nix
./home-darwin.nix # macOS-specific overrides
];
};
};
}
|
1
2
3
4
5
|
# Apply (using the flake)
home-manager switch --flake .#jdoe@workstation
# On a new machine: set up your entire environment with one command
nix run home-manager/master -- switch --flake github:jdoe/dotfiles#jdoe@workstation
|
NixOS: The Whole System
On NixOS, the entire operating system — kernel, init system, services, users, network configuration, bootloader — is declared in Nix. The nixos-rebuild switch command atomically applies changes, and every previous configuration is a bootable entry in the GRUB menu.
A Minimal NixOS Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
# /etc/nixos/configuration.nix
{ config, pkgs, ... }: {
imports = [
./hardware-configuration.nix # Auto-generated from hardware scan
./modules/desktop.nix
./modules/development.nix
];
# ─── Boot ───────────────────────────────────────────────────────────────
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
# ─── Networking ─────────────────────────────────────────────────────────
networking.hostName = "nixstation";
networking.networkmanager.enable = true;
# Firewall
networking.firewall = {
enable = true;
allowedTCPPorts = [ 22 80 443 ];
};
# ─── Time and Locale ─────────────────────────────────────────────────────
time.timeZone = "America/New_York";
i18n.defaultLocale = "en_US.UTF-8";
# ─── Users ──────────────────────────────────────────────────────────────
users.users.jdoe = {
isNormalUser = true;
extraGroups = [ "wheel" "docker" "networkmanager" "video" "audio" ];
shell = pkgs.zsh;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... jdoe@laptop"
];
};
# Passwordless sudo for wheel group
security.sudo.wheelNeedsPassword = false;
# ─── Services ───────────────────────────────────────────────────────────
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
X11Forwarding = false;
};
};
services.tailscale.enable = true;
virtualisation.docker = {
enable = true;
enableOnBoot = false; # Start on first use
};
# ─── System Packages ────────────────────────────────────────────────────
# These are available system-wide (prefer home-manager for user tools)
environment.systemPackages = with pkgs; [
vim
wget
curl
git
htop
];
programs.zsh.enable = true;
# ─── Nix Settings ───────────────────────────────────────────────────────
nix = {
settings = {
experimental-features = [ "nix-command" "flakes" ];
auto-optimise-store = true;
trusted-users = [ "@wheel" ];
# Binary caches — pre-built binaries so you don't build everything
substituters = [
"https://cache.nixos.org"
"https://nix-community.cachix.org"
"https://devenv.cachix.org"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCUSeBo="
"devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="
];
};
# Automatic garbage collection
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 30d";
};
};
system.stateVersion = "24.11";
}
|
NixOS as a Flake
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
# flake.nix — full system + home-manager configuration
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
home-manager = {
url = "github:nix-community/home-manager/release-24.11";
inputs.nixpkgs.follows = "nixpkgs";
};
# Hardware-specific optimizations
nixos-hardware.url = "github:NixOS/nixos-hardware";
};
outputs = { nixpkgs, home-manager, nixos-hardware, ... }: {
nixosConfigurations = {
# Desktop workstation
nixstation = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
nixos-hardware.nixosModules.common-cpu-amd
nixos-hardware.nixosModules.common-gpu-amd
./hosts/nixstation/configuration.nix
home-manager.nixosModules.home-manager {
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.jdoe = import ./home.nix;
}
];
};
# Home server
homeserver = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./hosts/homeserver/configuration.nix
];
};
};
};
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# Apply a NixOS configuration
sudo nixos-rebuild switch --flake .#nixstation
# Boot into the new config without switching (test first)
sudo nixos-rebuild boot --flake .#nixstation
# Roll back to the last generation
sudo nixos-rebuild switch --rollback
# List all generations (every nixos-rebuild creates one)
nix-env --list-generations --profile /nix/var/nix/profiles/system
# Build without activating (to check for errors)
nixos-rebuild build --flake .#nixstation
|
Binary Caches
Building everything from source is slow. Binary caches (substituters) serve pre-built derivations so Nix downloads binaries instead of compiling them. cache.nixos.org covers the entire nixpkgs package set.
1
2
3
4
5
6
7
8
9
10
11
|
# Check if a package has a binary in the public cache
nix path-info --store https://cache.nixos.org nixpkgs#nodejs_20
# Set up a private cache with Cachix (for your own packages)
nix-env -iA cachix -f https://cachix.org/api/v1/install
cachix authtoken <your-token>
cachix use my-org-cache
# Push your devShell's closure to the cache
nix develop --profile /tmp/dev-profile
cachix push my-org-cache /tmp/dev-profile
|
Common Patterns and Pitfalls
Searching for Packages
1
2
3
4
5
6
7
8
|
# Search nixpkgs
nix search nixpkgs python312
nix search nixpkgs#python312Packages tensorflow
# Or use the web: search.nixos.org
# Find which package provides a binary
nix-locate bin/rg # requires nix-index
|
When a Package Isn’t in nixpkgs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
# Option 1: Override an existing package's source
pkgs.somePackage.overrideAttrs (old: {
version = "1.2.3";
src = pkgs.fetchFromGitHub {
owner = "example";
repo = "somepackage";
rev = "v1.2.3";
sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
});
# Option 2: Write a simple derivation
pkgs.stdenv.mkDerivation {
pname = "my-tool";
version = "1.0.0";
src = pkgs.fetchurl {
url = "https://example.com/my-tool-1.0.0.tar.gz";
sha256 = "sha256-AAAAAAA...";
};
buildInputs = [ pkgs.openssl ];
installPhase = ''
mkdir -p $out/bin
cp my-tool $out/bin/
'';
}
# Option 3: Use pkgs.writeShellScriptBin for simple scripts
pkgs.writeShellScriptBin "my-script" ''
#!/bin/bash
echo "Hello from Nix"
''
|
Imperative Package Management (nix profile)
For one-off global installs without a flake:
1
2
3
4
5
6
7
8
9
10
11
|
# Install a package globally (user profile)
nix profile install nixpkgs#ripgrep
# List installed packages
nix profile list
# Remove a package
nix profile remove ripgrep
# Update all packages
nix profile upgrade '.*'
|
Garbage Collection
1
2
3
4
5
6
7
8
9
10
11
|
# Remove unused packages (generations keep old packages alive)
nix-collect-garbage
# Remove old generations AND collect garbage
nix-collect-garbage --delete-older-than 30d
# See what's taking space
nix path-info --all --size | sort -k2 -rn | head -20
# Optimize the store (hard-link identical files)
nix store optimise
|
The Nix Learning Curve
Nix has a genuinely steep learning curve. The language is unusual (lazy, purely functional, dynamically typed), the error messages are cryptic, and the documentation is scattered. Here’s a realistic learning path:
Week 1: nix shell and nix run
→ Try tools without installing them
→ Get comfortable with the nix CLI
Week 2: Your first flake.nix
→ Add a flake to a project you own
→ Set up direnv for automatic activation
→ Commit flake.lock to the repo
Month 2: Home Manager
→ Move your dotfiles into home.nix
→ Manage git, shell, and editor config declaratively
→ Experience your first "switch everything to a new machine in 30 minutes"
Month 3+: NixOS (optional)
→ Full system declaration
→ Atomic upgrades and rollbacks
→ The "it works the same everywhere" payoff
The investment is real, but the payoff is too: a development environment that any new team member can reproduce in minutes, CI that uses exactly the same tooling as local development, and a system you can rebuild from scratch after a hardware failure without remembering what you installed.
Quick Reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
# Explore
nix search nixpkgs <query> # Search packages
nix shell nixpkgs#<pkg> # Temp shell with package
nix run nixpkgs#<pkg> -- <args> # Run a program
# Flakes
nix flake init # Create a new flake
nix develop # Enter devShell
nix develop --command <cmd> # Run command in devShell
nix flake update # Update all inputs
nix flake show # Show flake outputs
nix flake check # Validate the flake
# Home Manager
home-manager switch # Apply home.nix
home-manager generations # List generations
home-manager rollback # Roll back one generation
# NixOS
nixos-rebuild switch # Apply and activate
nixos-rebuild switch --rollback # Roll back
nixos-rebuild build # Build without activating
# Maintenance
nix-collect-garbage -d # Delete old generations + GC
nix store optimise # Deduplicate store
nix store verify --all # Verify store integrity
|
Filed under: Developer Tooling & Workflow
Comments