Day 16 — Packages & programs

Updated

July 30, 2026

Day 16 — Packages & programs

Stage II · ~4h
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 day exists

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.


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.


Theory 2 — programs.* modules

Many packages have first-class modules:

programs.git.enable = true;
programs.zsh.enable = true;
programs.tmux.enable = true;
# programs.neovim.enable = true; # if present

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

# search options (host with nixos, or online search.nixos.org)
man configuration.nix  # huge
# or
nixos-option programs.git 2>/dev/null

Online: 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 (Day 8/28), not systemPackages
Desktop app packages or specialized modules
Always-on service services.* (Day 17)

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

Theory 5 — Overlays and pins (awareness only)

You might see:

nixpkgs.overlays = [];

or pinned packages. Stage III/V cover overlays properly. Today: 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. Only enable when you understand licensing implications for your lab.


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 III moves dotfiles and user CLIs to HM; today system layer only.


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 ripgrep

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, …):

  1. Remove from systemPackages if redundant
  2. Enable programs.<name>
  3. Rebuild
  4. Note extra behavior (completions, config files)

Lab 3 — Anti-profile audit

nix profile list 2>/dev/null || true
# nix-env -q 2>/dev/null || true

If tools exist only imperatively, either:

  • migrate into flake, or
  • remove them

Document the end state: flake owns system tools.


Lab 4 — Size awareness

nix path-info -Sh /run/current-system
# optional deeper:
nix path-info -rS /run/current-system | sort -n | tail

Write one sentence on the cost of your package list.


Lab 5 — Split comments in config

Organize packages with comments:

environment.systemPackages = with pkgs; [
  # editors
  vim
  # net
  curl
  # observability
  htop
];

Commit as readable policy, not a dump.



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 — nix-shell vs system packages vs flakes

Want Tool
Permanent host CLI environment.systemPackages / programs.*
Project toolchain nix develop / direnv (later)
One-off try nix shell nixpkgs#foo / nix run

Do not promote every experiment into the system closure.


Worked example D — Conditional unfree single package

{
  nixpkgs.config.allowUnfreePredicate = pkg:
    builtins.elem (pkgs.lib.getName pkg) [ "vscode" ]; # example name only
}

Narrower than global allowUnfree = true when you must pull one unfree tool onto a mostly free lab.


Lab 6 — programs.* option archaeology

# on the lab host
nixos-option programs.zsh 2>/dev/null | head -40
# or use search.nixos.org with channel 26.05

List 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.

Common gotchas

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

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

Commit

cd ~/nixos-config
git add .
git commit -m "day16: systemPackages and programs modules"

Tomorrow

Day 17 — systemd via Nix. One custom service + timer generated from modules—units as code.