Packages and programs
Packages and programs
Goal: Install software declaratively on the host—know when to use environment.systemPackages versus programs.* modules, and keep ad-hoc profiles from becoming the real SoT.
Why this chapter matters
New NixOS users nix-env / nix profile install a zoo, then wonder why rebuilds do not reproduce the machine. Declarative packages are how the host stays honest.
Ops motivation: reproducible admin tooling is part of incident response. The box you SSH into should carry jq and friends because the flake says so—not because someone remembered to install them once.
Theory 1 — environment.systemPackages
environment.systemPackages = with pkgs; [
vim
git
curl
htop
jq
];| Property | Meaning |
|---|---|
| On PATH system-wide | For all users (typical) |
| Closure in system gen | Rolled back with system |
| No service config | Just binaries/libs |
This is the blunt instrument—and it is correct for many CLI tools.
Organization pattern
environment.systemPackages = with pkgs; [
# editors
vim
# net
curl
# observability
htop
];Readable policy beats an unsorted dump.
Theory 2 — programs.* modules
Many packages have first-class modules:
programs.git.enable = true;
programs.zsh.enable = true;
programs.tmux.enable = true;Modules may:
- Install the package
- Drop config snippets
- Set shells in
/etc/shells
- Enable related services
Prefer programs.* when you need integration, not only a binary.
Discovery
man configuration.nix # huge
nixos-option programs.git 2>/dev/nullOnline: https://search.nixos.org/options (select 26.05 when available).
Theory 3 — Decision table
| Need | Prefer |
|---|---|
| Just a CLI on PATH | environment.systemPackages |
| Shell as login shell + vendor config | programs.zsh / programs.fish / programs.bash |
| Git system-wide with config hooks | programs.git and/or HM later |
| Language toolchains for one project | devShell, not systemPackages |
| Desktop app | packages or specialized modules |
| Always-on service | services.* / systemd chapter |
Theory 4 — Closure discipline (light)
Every package on the system increases:
- Download size
- Eval/build time
- Attack surface
- Cognitive load
| Smell | Fix |
|---|---|
| Entire language toolchains system-wide | devShell / HM |
| Three editors “just in case” | Pick one for system |
Random nix profile duplicates |
Remove; declare in flake |
| Debug tools forever | Keep a small admin set |
nix path-info -Sh /run/current-system
nix path-info -rS /run/current-system | sort -n | tailTheory 5 — Overlays and pins (awareness only)
You might see:
nixpkgs.overlays = [ … ];or pinned packages. Later parts cover overlays properly. Here: stick to nixpkgs 26.05 attribute names from your lock.
Theory 6 — Unfree and allow flags
Some packages need:
nixpkgs.config.allowUnfree = true;Or per-package predicates:
nixpkgs.config.allowUnfreePredicate = pkg:
builtins.elem (pkgs.lib.getName pkg) [ "vscode" ]; # example onlyOnly enable when you understand licensing implications for your lab. Narrow predicates beat global allow when possible.
Theory 7 — System vs user packages
| Layer | Mechanism |
|---|---|
| System | environment.systemPackages, programs.* |
| User (later) | Home Manager home.packages |
| Project | nix develop shells |
| Imperative | nix profile — minimize |
Stage-shaped later work moves dotfiles and user CLIs to HM; this chapter is system layer only.
Theory 8 — environment.defaultPackages trap
NixOS may install a small default package set. Know it exists so you do not double-declare or fight removals:
# advanced / intentional only
# environment.defaultPackages = [ ];Prefer adding what you need over emptying defaults on day one unless you understand the fallout (e.g. nano/perl expectations in scripts).
Theory 9 — Package name ≠ binary name
| pkgs attr | Binary might be |
|---|---|
ripgrep |
rg |
neovim |
nvim |
| some wrappers | different meta.mainProgram |
nix search nixpkgs ripgrep
command -v rgTheory 10 — Layers interaction with generations
Adding/removing system packages creates new generations. Rollback brings the old package set back—another reason not to rely on imperative profiles for “real” tools.
Worked examples bank
Example A — Baseline admin tools
{ pkgs, ... }: {
environment.systemPackages = with pkgs; [
vim
git
curl
wget
htop
tmux
jq
ripgrep
tree
];
}Example B — programs integration
{
programs.zsh.enable = true;
users.users.alice.shell = pkgs.zsh;
}Example C — Search package attr
nix search nixpkgs ripgrepExample D — Conditional unfree single package
{
nixpkgs.config.allowUnfreePredicate = pkg:
builtins.elem (pkgs.lib.getName pkg) [ "vscode" ];
}Example E — Avoid double install
If programs.git.enable = true already installs git, do not also list git unless you have a reason—collapse to one path.
Lab 1 — Declare your daily CLI set
List 8–15 tools you truly want on the lab host. Add them via environment.systemPackages. Rebuild switch. Verify each:
command -v rg
command -v jq
# …Lab 2 — Convert one package to programs.*
Pick something with a module (git, zsh, tmux, …):
- Remove from
systemPackagesif redundant
- Enable
programs.<name>
- Rebuild
- Note extra behavior (completions, config files)
Lab 3 — Anti-profile audit
nix profile list 2>/dev/null || trueIf tools exist only imperatively, either migrate into flake or remove them. Document: flake owns system tools.
Lab 4 — Size awareness
nix path-info -Sh /run/current-system
nix path-info -rS /run/current-system | sort -n | tailWrite one sentence on the cost of your package list.
Lab 5 — Split comments in config
Organize packages with section comments. Commit as readable policy.
Lab 6 — programs.* option archaeology
nixos-option programs.zsh 2>/dev/null | head -40List three options under a programs.* module that are not just “install the binary.”
Lab 7 — Remove one redundant package
Find a package present both via systemPackages and a programs.* enable (or profile). Collapse to one path. Rebuild. Confirm still on PATH.
Exercises
- Build a “minimal rescue” package set vs “comfortable admin” set; compare sizes.
- Move a language toolchain out of systemPackages into a project devShell.
- Enable
programs.tmuxand note config paths created.
- Document unfree policy for the lab in README.
- Find three packages where attr ≠ binary name.
- Use
nix-treeor path-info to find heavy dependencies (if available).
- Prove rollback restores a removed package.
- Write a PR-style commit message for package policy changes.
- Avoid
with pkgs;for a section; use explicitpkgs.and compare readability.
- List tools that should never be system-wide on a shared host.
- Prepare HM split: which packages will leave system later?
- Search options for a program you use; enable one non-obvious toggle.
Common pitfalls
| Symptom | Theory |
|---|---|
| Package name ≠ binary name | Check pkgs attr vs meta.mainProgram |
| Unfree errors | allowUnfree policy |
| Tool missing after rebuild | Not in config; was profile-only |
| Giant system closure | Too many packages / wrong layer |
| Shell not changing | users.users.*.shell not set |
| Double-declared tools | programs + systemPackages |
| Emptying defaultPackages casually | Surprising script breakage |
Checkpoint
- Daily tools declared in flake
- At least one
programs.*used
- Imperative profile not required for lab tools
- Rough system path size noted
- Config comments organized
- Layering (system vs project) explained
Further depth (this book)
| Topic | Chapter / part |
|---|---|
| Custom services using pkgs | systemd via Nix |
| Module options | Modules as a consumer |
| Home packages | Home Manager chapters |
| Overlays / packaging | Packaging part |