Create a Boot Environment

This page is tested for Alpine, Arch Linux, Fedora and RHEL guides. Not useful for NixOS since system rollback is already managed by Nix.

With Root on ZFS, it is possible to take snapshots of existing root file systems, which is a read-only copy of that file system. A new, full-fledged file system – clones – can be then created from this snapshot. This bootable clone of the current system is then called a “Boot Environment”.

This could be useful if you are performing a major system upgrade and wish to have the option to go back to a previous state if the upgrade fails.

  1. Identify which dataset is currently mounted as root / and boot /boot:

    findmnt /
    # output
    TARGET SOURCE               FSTYPE OPTIONS
    /      rpool/archlinux/root zfs    rw,relatime,xattr,posixacl
    
    findmnt /boot
    # output
    TARGET SOURCE               FSTYPE OPTIONS
    /boot  bpool/archlinux/root zfs    rw,relatime,xattr,posixacl
    
  2. Identify your distribution in the dataset (file system) path:

    DISTRO=archlinux # or `fedora', `alma', `alpinelinux'
    
  3. Choose a name for the new boot environment:

    BE_NAME=backup
    
  4. Take snapshots of the / and /boot datasets:

    zfs snapshot rpool/$DISTRO/root@$BE_NAME
    zfs snapshot bpool/$DISTRO/root@$BE_NAME
    
  5. Create clones from read-only snapshots:

    zfs clone -o canmount=noauto \
      -o mountpoint=/ \
      rpool/$DISTRO/root@$BE_NAME \
      rpool/$DISTRO/$BE_NAME
    
    zfs clone -o canmount=noauto \
      -o mountpoint=legacy \
      bpool/$DISTRO/root@$BE_NAME \
      bpool/$DISTRO/$BE_NAME
    
  6. Mount clone and update file system table (fstab)

    mount -t zfs -o zfsutil rpool/$DISTRO/$BE_NAME /mnt
    mount -t zfs  bpool/$DISTRO/$BE_NAME /mnt/boot
    
    sed -i "s|rpool/$DISTRO/root|rpool/$DISTRO/$BE_NAME|g" /mnt/etc/fstab
    sed -i "s|bpool/$DISTRO/root|bpool/$DISTRO/$BE_NAME|g" /mnt/etc/fstab
    

    If legacy mountpoint is used, omit -o zfsutil from mount command.

  7. Create GRUB menu for new clone:

    m='/dev /proc /sys'
    for i in $m; do mount --rbind $i /mnt/$i; done
    chroot /mnt /usr/bin/env sh <<EOF
      if which grub-mkconfig; then
        grub-mkconfig -o /boot/grub.cfg
      else
        grub2-mkconfig -o /boot/grub.cfg
      fi
    EOF
    

    GRUB menu contains information on kernel version and initramfs.

  8. Unmount clone:

    umount -Rl /mnt
    
  9. Add new boot environment as GRUB menu entry:

    tee -a new_entry <<EOF
    menuentry 'ZFS Clone of ${DISTRO}: ${BE_NAME}' { configfile (hd0,gpt2)/${DISTRO}/${BE_NAME}@/grub.cfg }
    EOF
    
    find /boot/efis/ -name "grub.cfg" \
    | while read i; do
        if grep -q "${DISTRO}" $i; then
          cat $i new_entry > grub.cfg
          cp grub.cfg $i
        fi
      done
    
    rm new_entry
    
  10. After reboot, select boot environment entry from GRUB menu to boot from the clone. Press ESC inside submenu to return to the previous menu.

  11. Steps above can also be used to create a new clone from an existing snapshot.

  12. To set a boot environment as default, replace existing grub.cfg inside EFI system partition with the one from the boot environment:

    mount -t zfs  bpool/$DISTRO/$BE_NAME /mnt
    
    # backup existing grub.cfg inside EFI
    # then replace it with menu from clone
    
    mkdir -p /mnt/grub_menu_backup
    menu_counter=1
    find /boot/efis/ -name "grub.cfg" \
    | while read i; do
        if grep -q "${DISTRO}" $i; then
          cp $i /mnt/grub_menu_backup/grub_${menu_counter}.cfg
          echo $i > /mnt/grub_menu_backup/grub_${menu_counter}_path.txt
          cp /mnt/grub.cfg $i
          menu_counter=$(($menu_counter + 1))
        fi
      done
    
    umount -Rl /mnt
    
  13. To delete the boot environment, check with findmnt that the boot environment is not currently used:

    findmnt /
    findmnt /boot
    

    Set variables:

    DISTRO=archlinux
    

    Then check the origin snapshot:

    zfs get origin rpool/archlinux/backup
    # rpool/archlinux/root@backup
    zfs get origin bpool/archlinux/backup
    # bpool/archlinux/root@backup
    
    RM_BE=backup
    RM_SNAPSHOT=root@backup
    

    Finally, destroy clone (boot environment) and its origin snapshot:

    zfs destroy rpool/${DISTRO}/${RM_BE}
    zfs destroy rpool/${DISTRO}/${RM_SNAPSHOT}
    
    zfs destroy bpool/${DISTRO}/${RM_BE}
    zfs destroy bpool/${DISTRO}/${RM_SNAPSHOT}
    

    Remove GRUB entry:

    find /boot/efis/ -name "grub.cfg" \
    | while read i; do
        if grep -q "${DISTRO}/${RM_BE}@/grub.cfg" $i; then
            head -n -1 $i > grub.cfg
            cp grub.cfg $i
        fi
      done