Build and Deploy
Table of Contents
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.liveElispin 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.