Build and Deploy

1. Build commands

cd ~/dotfiles
nix flake check                                                      # lint the whole flake
nix build .#nixosConfigurations.<host>.config.system.build.toplevel   # build without root
sudo nixos-rebuild switch --flake .#<host>                            # apply

The first two need no root and no target machine. Run them before touching anything.

1.1. Building as a user, activating as root

On a host where the flake checkout lives in a user's home, sudo nixos-rebuild can fail to read the repository — the version-control library refuses a repository owned by another user. Splitting the two halves works:

nix build .#nixosConfigurations.<host>.config.system.build.toplevel
sudo nix-env -p /nix/var/nix/profiles/system --set ./result
sudo ./result/bin/switch-to-configuration switch

1.2. Proving a change is a no-op

For a refactor that should change nothing, compare derivation paths. This is pure evaluation — no building, no root, and it works for hosts other than the one you are sitting at:

nix eval --raw .#nixosConfigurations.<host>.config.system.build.toplevel.drvPath

Identical before and after means provably inert. This is the right tool for namespace renames, module moves and formatting sweeps.

1.3. Do not build other hosts' closures carelessly

Building a graphical host's toplevel on a small or virtualised machine can source-compile large packages that are not in a binary cache, exhaust memory and take the environment down with it. Use the drvPath comparison above when all you need is a comparison.

2. Updating

NixOS has no apt upgrade. An update moves the flake's input pins and rebuilds. Everything pinned by the flake moves together — the language toolchains, the editor and its packages, the compositor. A new generation is created, so the blast radius of a bad update is one rollback.

2.1. The whole update, in order

cd ~/dotfiles
nix flake check                                                     # 1
nix flake update                                                    # 2
nix flake check                                                     # 3
nix build .#nixosConfigurations.<host>.config.system.build.toplevel  # 4
sudo nixos-rebuild switch --flake .#<host>                           # 5
git add flake.lock && git commit -m "flake.lock: bump all inputs"    # 6

The order carries the meaning:

  • Step 1 is the baseline. A check is only evidence about an update if it also passed before the update; running it once, after, cannot tell "broken by the pin" from "broken already".
  • Step 2 only rewrites flake.lock. Nothing on the machine changes yet.
  • Step 4 builds the new system closure as your own user — no root, no activation, and no effect on the running machine. It is where a broken pin shows up as a failed build instead of a failed boot.
  • Step 6 is not optional. How a machine is rebuilt is decided by the pins its checkout names, and that is flake.lock. An uncommitted lock means the next checkout of the repository reverts the machine to the old pins with no message. A pin bump is a change like any other and belongs in history.

2.2. One input

nix flake update <input>
nix flake check
sudo nixos-rebuild switch --flake .#<host>

Older instructions spell this nix flake lock --update-input <input>. That is now a deprecated alias: Nix 2.34 prints a warning and says to use nix flake update <input>. Both do the same thing today.

2.3. What updates what

Tool Pinned by Moved by
Python, Node.js, CLI tools nixpkgs nix flake update nixpkgs
Emacs and its packages nixpkgs + emacs-overlay nix flake update nixpkgs emacs-overlay
ibm-plex (the font) nixpkgs-unstable nix flake update nixpkgs-unstable
EWM the ewm input nix flake update ewm
Home Manager modules the home-manager input nix flake update home-manager
The distribution itself the emanix input see below

The base is stable nixos-26.05 with Home Manager's release-26.05. Packages that outrun it are taken one by one from nixpkgs-unstable; a consumer that wants a package from unstable (a coding-agent CLI, say) supplies it itself.

One thing is not in the lock: the agent extensions and skills under ~/.pi/ are runtime state, not Nix packages. They update through that tool, not through a rebuild.

2.4. Updating the distribution

Emanix is an input of the consuming flake, so it is updated like input and in two steps, in that order:

# in the emanix checkout
nix flake check
nix flake update                    # the distro's own pins, for its own checks
git commit -am "flake.lock: bump"
git push

# in the consuming flake
nix flake update emanix
nix flake check

The consumer follows both nixpkgs and nixpkgs-unstable into the distribution, so the two locks are not independent: bumping nixpkgs in the consumer moves the distribution's nixpkgs too. Bumping the distribution's own lock keeps its standalone checks — the ones that run in its repository — on the same revision as the machines it builds.

If the distribution's change renames or moves a module, the consumer names that path and must change with it. This is the one step that is easy to forget: the consumer's comments and checks contain paths from the distribution, so a move there without a change here breaks the consumer.

3. Rolling back

sudo nixos-rebuild switch --rollback

Every rebuild creates a generation and every generation is a target. The bootloader also lists them, which is the escape hatch when a change breaks the graphical session and you cannot get a shell.

A bad update has two halves and they are rolled back differently. The generation rollback above restores the old closure while flake.lock still names the new pins — so the next rebuild produces the broken system again. To roll back the pins as well:

git checkout -- flake.lock
sudo nixos-rebuild switch --flake .#<host>

That works only before the next nix flake update overwrites the file. Once the new pins are committed, rolling back means reverting that commit.

3.1. What rollback does not cover

Rollback restores the system closure. It does not restore anything the system does not own:

  • Files symlinked out of the store for live editing are not generation-pinned — a rollback leaves them at whatever the working tree holds. See emanix.src.liveElisp in Options.
  • Data is data. Rollback is not a backup.
  • Anything a service mutated at runtime — a database, a cache — stays mutated.

4. Garbage collection

Collection runs daily and deletes generations older than three days (--delete-older-than 3d), and the store is auto-optimised.

There is also a mid-build emergency valve: when free space falls below a floor, Nix collects during the build until a ceiling is available again. That valve is the real safety net — a timer only helps between builds, and it is during a large build that a machine actually runs out of room.

Manual collection, when needed:

sudo nix-collect-garbage -d          # delete old generations, then collect
nix store gc                         # collect only what nothing references

4.1. On a virtualised host

A virtual disk image is a high-water mark: it grows and does not shrink when files are deleted inside the guest. Collecting garbage frees space to the guest but not to the host, so image growth needs a separate, deliberate compaction step. Plan for it rather than being surprised by it.

5. Formatting and linting

nix fmt                              # format the tree
nix develop                          # shell with the Nix language server and linters

The dev shell carries a Nix language server, the formatter, a dead-code finder and a linter — the same tools CI would run, available before you commit.