vulcanridr

FreeBSD Tribal Knowledge: Boot Enviroment Management

Wow, it has been crazy hectic in my life of late. I know the old saying that "time is what keeps everything from happening at once," but I really have felt like everything has happened at once for the last few months. And my blogging has suffered for it. So today, I am back with another FreeBSD Tribal Knowledge entry on how I personally manage my boot environments.

Boot Environments (BEs) are point-in-time bootable ZFS clones of the bulk of your system, your root filesystem, as it were, with a few omissions. A BE contains all of the base system (under /), userland (and thus, all installed packages, in /usr/local). It includes most parts of /usr and /var, yet it excludes multiple parts of the directory tree, things that you would want to preserve across changes or upgrades, like /home, /usr/ports, /usr/src, /var/{audit,crash,log,mail,tmp} and /tmp.

The default BE is named, ironically enough, "default" by the installer. The root filesystem lives in <poolname>/ROOT. The selected BE is cloned to a separate named dataset beneath ROOT. The really cool thing is that a sysadmin can reboot into an older BE, by effectively replacing the default dataset as / for the next boot. This allows you to completely roll back to an older incarnation of your system, say, in the event of a system upgrade going sideways. The freebsd-update tool creates a new BE by default, and has since, if memory serves, 13.0. Unfortunately, this functionality was never baked into pkg upgrade. So I wrote a wrapper script to do my pkg upgrades. As my the network in my homelab grew, and after I started teaching myself ansible, I wrote a playbook that does pkg upgrades en masse. The script and the playbook both create a BE called Before_pkgUpgrade before the first package gets upgraded. The old Before_pkgUpgrade is deleted and a new one (the current state of the system) is created. I do this because even pkg upgrades can go pear shaped. It gives the ability to completely roll back the system to a state prior to the upgrade. It is also a feature I longed for since I started using linux in 1995.

Of course, there exist tools that can be used to manage BEs. The default that ships with FreeBSD is called bectl. It ships with the base system, and is a binary that allows you to manipulate BEs. A similar tool is a shell program called beadm. You can do a number of operations with them to manage BEs, major functions include create, which can lone the current BE, to a different name. It can also copy an inactive BE or even a snapshot to a new BE. Other options includes list, import and export (to files), and rename.

Since they are ZFS datasets, you can have as many BEs as you can fit in the available space. Listing them gives the name, status, mountpoint, amount of space used, and creation date. For example, a bectl list on my desktop macine looks like

BE Active Mountpoint Space Created
15.0-RELEASE-p9_2026-06-15_152424 - - 1.55M 2026-06-15 15:24
Before_pkgUpgrade - - 9.25M 2026-06-15 16:38
default NR / 80.6G 2026-06-03 08:15

BE is the name of the listed environment. Active is the the state of the BEs in the list. N is the BE that is currently booted ("Now"), and R is the one that will be booted on "reboot." To change which BE will be active on the next boot, use bectl <bename>, example bectl Before_pkgUpgrade to boot onto that BE the next time the system is rebooted.

BEs can also be exported and imported to files. I have never personally needed to do this.

My workflow with BEs works as follows.

  • I keep one or two of the previous major or minor version upgrade BEs on the system. At the time of this writing, I am running 15.0p10, and I have a 15.0p9 BE that I can fall back to.
  • As stated above, I delete the prior Before_pkgUpgrade dataset, and create a new one of the current system state prior to updating packages, to leave the option of rolling back, as freebsd-updates generally further back. I usually pkg upgrade once or twice a week, which is far more often than the base system is updated.
  • I do weekly snapshots only of my <poolname>/ROOT/default dataset (the running system), and replicate that to my NAS. In researching for this article, I am also investigating the differences between doing a bectl export default > filename and making a snapshot and replicating to the NAS as I am doing now.
  • My replication scheme includes the default BE as well as /home and anything under it. What I do not include is /var/mail, /usr/ports, or /usr/src. emails to my internal inbox are generally emails from my homelab, things like output from periodic, security updates, messages from the NAS boxes, etc, so loss of them is not as big a deal, since all of my actual email is external to my network.
  • If I ever have to roll back to the Before_pkgUpgrade BE, I follow the following procedure:
  • bectl activate Before_pkgUpgrade - bectl list will show R on default (or whatever the current BE is, and N on Before_pkgUpgrade.
  • Reboot on to the older BE
  • When the system comes back up, bectl destroy default
  • Make the old BE the default, bectl rename Before_pkgUpgrade default
  • Create a new copy of the current BE. bectl create Before_pkgUpgrade

The reason for step 5 is that the ansible playbook expects to have a Before_pkgUpgrade BE to delete, and I don't necessarily like having it spit out an error, even if ignore_errors is enabled.

Note that my workflow will probably change, at some point in the future, as I am beginning to investigate replacement of freebsd-update with pkgbase. But this method has been working for me since 2019.

Thoughts? Leave a comment