Updating safely
Updating a NixOS machine is one of the few places where the system can stop booting, and it is also the place where NixOS protects you better than any other distribution. Every rebuild produces a new generation and leaves the previous one untouched in the boot menu, so a bad update is a reboot away from being undone rather than a recovery USB away. This page walks through a careful update loop, the special care that GPU drivers and kernels deserve, and how to recover and reclaim disk when you need to.
Throughout, the system is the Ternix output nixosConfigurations.nixos-host for x86_64-linux, and the flake lives in a git repository you control (Using a config).
What an update actually changes
A flake pins its inputs in flake.lock. Your package versions, the kernel, and the drivers all come from the nixpkgs revision recorded there, so nothing on your machine moves until that lock file moves. Updating is therefore two distinct steps. First you change flake.lock to point at newer inputs, then you build a new system from it. Until the build succeeds and you activate it, the running system is exactly as it was.
This is why an update can be reviewed like a code change before it ever touches the running system. The rest of this page leans on that fact.
The safe update loop
The loop below changes nothing on disk until a build succeeds, lets you run the new system live before you trust it, and keeps the old generation bootable the whole time.
Start by looking at what you have. nix flake metadata prints the resolved inputs and the age of the current lock, so you can see how far behind you are before changing anything.
cd ~/nixos
nix flake metadataNow move the lock forward. nix flake update refreshes every input to its latest revision and rewrites flake.lock.
nix flake updateBecause the lock file is tracked by git, the update is a reviewable diff. Read it before you build. The diff shows the old and new revisions and the dates they were locked, which is often enough to spot a jump across a release boundary.
git diff flake.lockBuild the new system without activating it. nixos-rebuild build evaluates and compiles the configuration and leaves the result in a ./result symlink. Nothing about the running system changes, and a failure here costs you nothing.
nixos-rebuild build --flake .#nixos-hostIf the build succeeds, try it live. nixos-rebuild test activates the new configuration in the running system but does not add it to the boot loader, so a reboot returns you to the current generation no matter what the new one does. Quoting the NixOS manual , test will "build the configuration and switch the running system to it, but without making it the boot default".
sudo nixos-rebuild test --flake .#nixos-hostUse the machine. Check the things this update was likely to touch, such as graphics, networking, and any service you depend on. When you are satisfied, commit to it. switch activates the configuration and makes it the boot default.
sudo nixos-rebuild switch --flake .#nixos-host
git commit -am "flake.lock: update inputs"Committing the lock file is what makes the update reproducible. Anyone who checks out that commit, including you on the same machine next week, builds the identical system.
Updating one input at a time
nix flake update with no arguments moves everything at once, which makes a later regression hard to attribute. Naming an input updates only that one. Most of your system follows nixpkgs, so updating it alone is the common case.
nix flake update nixpkgsIf a problem appears after a single-input update, you already know which input caused it. To undo just that input, restore the lock and rebuild.
git checkout flake.lock
sudo nixos-rebuild switch --flake .#nixos-hostUpdating or pinning a single package
There is no per-package version field in a NixOS configuration. A package is whatever the locked nixpkgs provides, so the normal way to get a newer version of one program is to update nixpkgs and rebuild. When you need a version that differs from what your main nixpkgs carries, pin it explicitly.
One approach adds a second nixpkgs input locked to a different revision and pulls the single package from it. Add the input in flake.nix.
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
inputs.nixpkgs-pinned.url = "github:NixOS/nixpkgs/nixos-24.11";
}Then reference that input inside the module that builds the system. The package comes from the pinned revision while everything else stays on your main nixpkgs.
{ pkgs, inputs, ... }:
let
pinned = import inputs.nixpkgs-pinned {
inherit (pkgs) system;
config.allowUnfree = true;
};
in
{
environment.systemPackages = [ pinned.someTool ];
}When you instead want a different version from the same source, an overlay overrides the package in place so every reference to it system wide uses your version. The example below points the package at a newer source tarball.
{
nixpkgs.overlays = [
(final: prev: {
someTool = prev.someTool.overrideAttrs (old: {
version = "2.5.0";
src = prev.fetchFromGitHub {
owner = "example";
repo = "sometool";
rev = "v2.5.0";
hash = "sha256-AAAA...";
};
});
})
];
}Overlays are the heavier tool because overriding version and src can break the build when newer sources need different dependencies. Reach for a pinned input first and an overlay only when you genuinely need to rebuild the package differently.
Binary caches and build speed
A substituter, also called a binary cache, lets Nix download a prebuilt result instead of compiling it locally. That is why most packages install in seconds. When nothing in a cache matches what you asked for, like a custom overlay, an override, or an unusual option combination, Nix falls back to building from source on your machine, which is slow.
Adding a community cache cuts down on local builds.
nix.settings = {
substituters = [ "https://nix-community.cachix.org" ];
trusted-public-keys = [ "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" ];
};Adding a cache at runtime as a non-root user also requires being listed in
nix.settings.trusted-users. To publish your own cache,
Cachix is the common hosted option.
GPU drivers
Graphics is the part of an update most likely to leave you staring at a black screen, so it is the part that most rewards nixos-rebuild test before you reboot. Driver and kernel changes only take full effect on a fresh boot, and test lets you confirm the system still builds and activates while keeping the known-good generation as the boot default.
Enable the graphics stack first. This option pulls in the userspace libraries and replaces the older hardware.opengl.enable name used before NixOS 24.11.
{
hardware.graphics.enable = true;
}NVIDIA
The NVIDIA driver is unfree, so the build refuses to proceed until you allow unfree packages. Select the nvidia X server driver, then configure hardware.nvidia. Pin the driver branch to a package from the kernel you are running so the module always matches the kernel, and choose the open or proprietary kernel module deliberately.
{ config, ... }:
{
nixpkgs.config.allowUnfree = true;
services.xserver.videoDrivers = [ "nvidia" ];
hardware.graphics.enable = true;
hardware.nvidia = {
modesetting.enable = true;
package = config.boot.kernelPackages.nvidiaPackages.production;
open = false;
};
}A few details matter here. hardware.nvidia.package selects the driver branch, and besides production you can use config.boot.kernelPackages.nvidiaPackages.stable or .beta when you want a newer branch, or one of the legacy_* packages for older cards. hardware.nvidia.open chooses between NVIDIA's open source kernel module and the proprietary one. The open module suits recent Turing and later GPUs, while older cards still need open = false. Set it explicitly rather than relying on a default, because the right answer depends on your hardware. modesetting.enable is needed by Wayland and by most modern setups.
After changing any of this, build and test before you reboot.
nixos-rebuild build --flake .#nixos-host
sudo nixos-rebuild test --flake .#nixos-hostIf the desktop comes back under test, run switch. If it does not, reboot and the previous generation returns untouched.
AMD and Intel
AMD and Intel graphics run on the in-tree kernel drivers plus the Mesa userspace, all of which hardware.graphics.enable already provides, so a basic setup needs nothing beyond enabling graphics. When you want extra acceleration libraries such as hardware video decoding or Vulkan, add them through hardware.graphics.extraPackages.
{ pkgs, ... }:
{
hardware.graphics.enable = true;
hardware.graphics.extraPackages = [
pkgs.vaapiVdpau
pkgs.libvdpau-va-gl
];
}Because these drivers ship with the kernel, a kernel update is also a graphics update for AMD and Intel users, which is the next thing to be careful about.
Choosing a kernel
NixOS defaults to a long-term-support kernel. You can ask for the newest released kernel instead, which is sometimes necessary for very recent hardware.
{ pkgs, ... }:
{
boot.kernelPackages = pkgs.linuxPackages_latest;
}A newer kernel is not a free upgrade. Out-of-tree modules are built against the running kernel, so a kernel bump rebuilds the NVIDIA driver and can outrun a driver branch that has not added support for that kernel yet. In-tree drivers for AMD and Intel move with the kernel too, so a graphics regression can arrive purely from a kernel change. Treat a kernel change exactly like a driver change and run nixos-rebuild test, or a full VM, before you make it the boot default.
Recovery
The point of every generation being kept is that recovery is ordinary, not an emergency.
If an update activated badly but the machine still runs, roll back to the previous generation and make it current.
sudo nixos-rebuild switch --rollbackIf the machine will not boot into the new generation at all, reboot and pick an older generation from the boot menu. Each NixOS generation is its own boot entry, so an older, working system is always one menu selection away. Booting an old generation does not delete the new one, it just starts the older system, and you can investigate from there.
To see what you can roll back to, list the generations.
sudo nix-env --list-generations --profile /nix/var/nix/profiles/systemFreeing space and garbage collection
Old generations and the store paths they reference are kept on purpose, which is why disk usage grows over time. Garbage collection removes store paths that no live generation references.
The blunt command deletes every generation except the current one and then collects everything now unreferenced.
sudo nix-collect-garbage -dWhen you would rather keep recent history, delete only generations older than a chosen age and collect after that.
sudo nix-collect-garbage --delete-older-than 30dDeduplicate the store to claw back more space by hard-linking identical files. This is safe to run any time and pairs well with garbage collection.
nix store optimiseThere is a real trade-off to keep in mind. Garbage collection deletes the very generations that rollback and the boot menu depend on, so running nix-collect-garbage -d removes your ability to roll back to anything but the current system. Run a deep clean only when the current generation is one you trust, and prefer --delete-older-than when you want a recovery window to survive.
Advanced
Test a full system in a VM
nixos-rebuild build-vm builds your configuration into a runnable QEMU virtual machine without touching the host. It is the safest possible way to check a risky kernel or driver change, because a broken boot happens inside the VM rather than on your hardware.
nixos-rebuild build-vm --flake .#nixos-host
./result/bin/run-nixos-host-vmThe VM shares your configuration but uses its own throwaway disk image, so you can confirm the system boots and the desktop starts before you bring the change to the real machine.
Unattended updates
system.autoUpgrade runs the update loop on a timer so the machine keeps itself current without you driving it.
{
system.autoUpgrade = {
enable = true;
flake = "github:youruser/nixos-config#nixos-host";
flags = [ "--print-build-logs" ];
dates = "04:00";
randomizedDelaySec = "45min";
};
}The upgrade follows whatever flake.lock is committed at the flake URL, so it
does not refresh inputs on its own. To also pull newer packages on a schedule,
run nix flake update in that repository and commit the new lock, then let the
next upgrade pick it up.
The convenience has a cost. Unattended upgrades activate without you watching, so a regression can land while you are away from the machine, and the automatic update does not run your own test step first. It suits servers and headless machines where you accept the trade for never falling behind, and it is a poorer fit for a workstation with a GPU driver that you would rather verify by hand. Rollback still works either way, so even an automatic upgrade leaves the previous generation in the boot menu.
Next
- Flakes explains inputs and the lock file that every update moves.
- Using a config covers applying a configuration and the rebuild modes in more depth.
- Commands and scripts collects the rebuild, flake, and store commands used here.
- Editing your config shows how to change options, packages, and overlays.