NixOS Generations | ZFS Snapshots
NixOS system generations are immutable snapshots of your system's state, created each time you rebuild your system configuration using nixos-rebuild switch
. They are like magic when you need to rollback a system after a upgrade that maybe breaks something, you're testing a new display and window manager, or bumping software versions.
But don't be lured into a false sense of security with these generations. You can, and will get bitten rolling back to a previous state, and are left with a system that only rolls back specific aspects of itself. This is where I lean on ZFS snapshots and configure them to run as a pre-hook
to nixos-rebuild
to prevent my system from becoming unusable after a generation rollback.
NixOS Generations
Although generations
can save you from a lot of reversing what you did there are plenty of areas where you can still be left with a broken system. nix.oci-containers
is a great example of something that is upgraded when you nixos-rebuild switch
a system, and all to often a container gets version bumped when you didn't want that, and now the application is crashing. Reverting the generation doesn't solve the problem since the data directory for the container is often in a separate location that isn't included in the system generation. When you revert
what you did and reboot
the system, your container is back to the old version but with files left on disc from the newer image and config file changes that took place, now your container falls over.
Generations are great, but they have their limitations. Config files that get rewritten during package upgrades are often in locations that are outside of the snapshotting scope and yes, you can revert the package upgrade by booting into an old generation, but config files have changed and you need to figure out how to fix those 🤔
Sometimes it's not even critical breaking changes. Say you wanted to check out the newest KDE release and are running GNOME. You adjust your config files accordingly, rebuild, and login to a KDE session 😎 Once you are done testing you revert those changes and hop back into your GNOME setup. But wait, every KDE local file was left in your home
directory, including overwriting some of your GNOME files, now your desktop no longer functions the way it did 💢
ZFS Snapshots
I remember when I discovered ZFS and the very first thought I had was:
Why would you ever use another filesystem?!
Years later I deploy it on every system I manage, besides the client owned devices that have other technologies incorporated into their business flow. But honestly, if I never have to rsync
data over a WAN
connection again, I will be a happy camper 😅
There are two ways of snapshotting ZFS datasets:
1) Manually - You issue the zfs snapshot -r rpool/dataset_to_backup@$(date +%m%d%Y-%H%M)
this will create a time stamped snapshot within .zfs/snapshot
. You can view these snapshots with zfs list -t snapshot
2) Automatically - By far my preferred method. You can use tools like sanoid
to automate this process, create systemd timer
, or even cron
job that bad boi. However you accomplish this don't forget to back these up remotely in case your box is ever taken out or your storage array fails.
Secret Sauce🍯
Here is where we combine the power of zfs snapshot
/w the magic of generations
.
pre-hook
Like everything in Nix land you can attack this problem in multiple ways since nixos-rebuild
doesn't actually have a pre-hook
function.
Shell alias / function
nixos-rebuild() {
echo "Running pre-hook..."
# Add your pre-hook commands here
# Example: Take ZFS snapshot
zfs snapshot -r your_desired_dataset@nixos-rebuild-$(date +%Y-%m-%d-%H:%M:%S)
# Run the actual nixos-rebuild command
command nixos-rebuild "$@"
}
Wrapper Script (make executable and call the script instead)
#!/usr/bin/env bash
echo "Executing pre-hook..."
# Your pre-hook commands here
# Example: Take ZFS snapshot
zfs snapshot -r your_desired_dataset@nixos-rebuild-$(date +%Y-%m-%d-%H:%M:%S)
# Execute the actual nixos-rebuild
exec /run/current-system/sw/bin/nixos-rebuild "$@"
Script within your config
{ config, pkgs, ... }:
{
environment.systemPackages = with pkgs; [
(pkgs.writeShellScriptBin "nixos-rebuild-hooked" ''
echo "Running pre-rebuild hook..."
# Your pre-hook logic here
${pkgs.zfs}/bin/zfs snapshot -r your_desired_dataset@nixos-rebuild-$(date +%Y-%m-%d-%H:%M:%S)
# Run nixos-rebuild
exec ${config.system.build.nixos-rebuild}/bin/nixos-rebuild "$@"
'')
];
}
What if your system does auto snapshots with a tool like sanoid
which is my preferred method. Rather than manually defining the datasets I want to include, I read the sanoid
config to return the datasets that are managed, and roll those into the pre-hook snapshot function.
Sanoid
Here is a basic zfs
and sanoid
configuration
Note: When I use the default Nix method of sanoid
templates and dataset, it keeps throwing errors that I have no valid config in /etc/sanoid.conf
. As a stop gap I am using environment.etc
to just write out a default config.
environment.systemPackages = with pkgs; [
zfs
sanoid
];
services = {
zfs = {
autoScrub = {
enable = true;
interval = "monthly";
};
autoSnapshot = {
enable = false; # Let sanoid handle snapshots
};
};
sanoid = {
enable = true;
};
};
environment.etc."sanoid/sanoid.conf".text = ''
[template_backup]
hourly = 20
daily = 10
monthly = 3
autoprune = yes
autosnap = yes
[usbzfs/home]
use_template = backup
recursive = yes
[usbzfs/media]
use_template = backup
recursive = yes
[usbzfs/docker]
use_template = backup
recursive = yes
'';
sanoid
config to determine which datasets to snapshot, it adds the current NixOS generation to the snapshot name, and then runs nixos-rebuild "$@"
so you can append flags to the command.
{ config, pkgs, ... }:
{
environment.systemPackages = with pkgs; [
(pkgs.writeShellScriptBin "nixos-rebuild-hooked" ''
echo "Running pre-rebuild hook..."
# Get current system generation once
CURRENT_GEN=$(ls -la /nix/var/nix/profiles/system | grep -o 'system-[0-9]*-link' | tail -1)
echo "Current generation: $CURRENT_GEN"
# Get list of datasets from sanoid configuration
echo "Getting datasets from sanoid configuration..."
DATASETS=$(sudo sanoid --verbose --debug --configdir=/etc/sanoid | grep Filesystem | awk '{print $2}')
if [ -z "$DATASETS" ]; then
echo "No datasets found in sanoid configuration"
exit 1
fi
echo "Found datasets:"
echo "$DATASETS"
snapshot_with_prefix() {
local dataset="$1"
local timestamp=$(date +%Y-%m-%d_%H-%M-%S)
local snapshot_name="$dataset@''${CURRENT_GEN}_$timestamp"
echo "Creating snapshot: $snapshot_name"
sudo zfs snapshot -r "$snapshot_name"
}
# Loop through each dataset and create snapshots
while IFS= read -r dataset; do
if [ -n "$dataset" ]; then
snapshot_with_prefix "$dataset"
fi
done <<< "$DATASETS"
echo "Pre-rebuild snapshots complete"
# Run nixos-rebuild
exec /run/current-system/sw/bin/nixos-rebuild "$@"
'')
];
}
The default naming convention of sanoid
is dataset@autosnap_timestamp
:
usbzfs/media@2025-07-27_09-20-16
This snapshot script follows the same naming convention but it reads the current NixOS generation state and adds that to the naming:
usbzfs/media@system-45-link_2025-07-27_09-22-41
This makes it very easy when you need to rollback your NixOS system to find which datasets corelate to your NixOS generation prior to the build that may have caused issues.
If there is a problem you can list your snapshots and use a tool like grep
to grab all the relevant backups zfs list -t snapshot | grep "system-45"
Running rebuild with hook
If you are like me and use flakes to manage your systems you can simply run nixos-rebuild-hooked switch --flake .
Your datasets get snapshotted, your rebuild takes place, and you now have a simple to locate ZFS snapshot just before the system rebuilds and a new generation is set to boot.
Note: You could alter this script and remove the last line exec /run/current-system/sw/bin/nixos-rebuild "$@"
and use this tool as a snapshot function and run your nixos-rebuild
separately. But if you forget to run before you rebuild the system it's a useless function 😅