Skip to content

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
  '';
Here is the script that reads the 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 😅