# How to Set Up Boot Environments on FreeBSD
Every system administrator has lived through the nightmare: you run a major upgrade, something breaks, and now you are staring at a system that will not boot. On FreeBSD, boot environments eliminate this risk entirely. With a single command, you can snapshot your entire running system, perform any upgrade or change you want, and roll back in seconds if anything goes wrong.
This guide covers everything you need to know about FreeBSD boot environments, from basic bectl commands to automated pre-upgrade scripts and advanced workflows involving jails and kernel upgrades.
What Are Boot Environments?
A boot environment (BE) is a bootable clone of your root filesystem stored on ZFS. Because ZFS supports copy-on-write snapshots and clones natively, creating a boot environment is nearly instantaneous and consumes no additional disk space at creation time. Only blocks that diverge between the active environment and the clone consume extra space over time.
Each boot environment is a complete, independent copy of your root filesystem. You can:
- **Create** a new BE before any risky operation
- **Activate** a different BE to boot into on the next reboot
- **Roll back** to a previous BE if an upgrade fails
- **Mount** an inactive BE to inspect or modify its contents
- **Destroy** old BEs to reclaim disk space
Think of boot environments as save points in a video game. You save before the boss fight, and if you lose, you reload. The "boss fight" here is a major FreeBSD version upgrade, a kernel change, or an untested package installation.
Boot environments operate at the ZFS dataset level. When you create a BE, FreeBSD snapshots the root dataset (typically zroot/ROOT/default) and creates a clone. The bootloader knows how to enumerate these clones and present them as selectable options at boot time.
Prerequisites
Before you can use boot environments, your system needs two things:
1. ZFS Root Filesystem
Your FreeBSD installation must use ZFS as the root filesystem. If you installed FreeBSD 13.0 or later using the default installer options, you already have ZFS root. This has been the default since FreeBSD 13.
To verify your root filesystem is on ZFS:
$ zfs list | grep ROOT
zroot/ROOT 4.12G 108G 96K none
zroot/ROOT/default 4.12G 108G 4.12G /
If you see output like the above, you are ready. The zroot/ROOT/default dataset mounted at / confirms ZFS root. For a deeper dive into ZFS on FreeBSD, see our [ZFS guide](/blog/zfs-freebsd-guide/).
2. The bectl Utility
The bectl command is included in the FreeBSD base system starting with FreeBSD 11.2. No additional packages are required. Verify it is available:
$ which bectl
/sbin/bectl
If you are running FreeBSD 11.1 or earlier, you can use the beadm port from sysutils, but upgrading to a supported FreeBSD release is strongly recommended.
bectl Basics
The bectl command is your primary interface for managing boot environments. Here are the essential operations.
Listing Boot Environments
# bectl list
BE Active Mountpoint Space Created
default NR / 4.12G 2025-09-15 10:22
The columns tell you:
- **BE**: The name of the boot environment
- **Active**: N means active now (running), R means activated for next reboot, NR means both
- **Mountpoint**: Where the BE is currently mounted
- **Space**: Disk space unique to this BE (space that would be freed if destroyed)
- **Created**: When the BE was created
For more detail, use the -a flag to include snapshots:
# bectl list -a
BE Active Mountpoint Space Created
default NR / 4.12G 2025-09-15 10:22
default@snap1 - - 212M 2026-01-10 14:30
default@snap2 - - 45.3M 2026-03-01 09:15
Creating a Boot Environment
# bectl create pre-upgrade-14.2
That is it. The operation completes in under a second regardless of how large your root filesystem is, because ZFS clones share all existing blocks with the source dataset.
You can also create a BE from a specific snapshot:
# bectl create -e default@snap1 restored-from-snap1
Activating a Boot Environment
To set a BE as the one to boot into on next reboot:
# bectl activate pre-upgrade-14.2
Successfully activated boot environment pre-upgrade-14.2
Verify the activation:
# bectl list
BE Active Mountpoint Space Created
default N / 4.12G 2025-09-15 10:22
pre-upgrade-14.2 R - 12K 2026-03-29 08:00
The R on pre-upgrade-14.2 confirms it will be used on next reboot. The N on default shows it is still the currently running environment.
Destroying a Boot Environment
# bectl destroy pre-upgrade-14.2
Are you sure you want to destroy 'pre-upgrade-14.2'? (y/N): y
You cannot destroy the currently active BE. To skip the confirmation prompt, use -F:
# bectl destroy -F old-environment
Mounting and Unmounting a Boot Environment
You can mount an inactive BE to inspect or modify its files without rebooting:
# bectl mount pre-upgrade-14.2 /mnt
Successfully mounted pre-upgrade-14.2 at /mnt
Now you can browse /mnt as if it were the root of that environment. This is useful for recovering configuration files or checking what changed. When done:
# bectl umount pre-upgrade-14.2
Workflow: Safe System Upgrades
This is the most common use case for boot environments. Before upgrading FreeBSD, create a BE so you can roll back if the upgrade fails. For detailed upgrade procedures, see our [FreeBSD update guide](/blog/freebsd-update-guide/).
Step 1: Create a Pre-Upgrade Boot Environment
# bectl create pre-14.2-upgrade
Step 2: Run the Upgrade
# freebsd-update fetch
# freebsd-update install
# reboot
# freebsd-update install
Step 3: Test the System
After rebooting into the upgraded system, verify that everything works:
# uname -r
14.2-RELEASE
# service -e # Check running services
# pkg check -d -a # Verify package dependencies
Step 4: Roll Back if Needed
If the upgrade caused problems, rolling back takes seconds:
# bectl activate pre-14.2-upgrade
# reboot
You are now back on the exact system you had before the upgrade. Nothing was lost.
Step 5: Clean Up on Success
If the upgrade went well, remove the old BE after you are confident:
# bectl destroy pre-14.2-upgrade
Do not rush this step. Keep the old BE around for a week or two until you are certain the upgrade is stable.
Workflow: Testing New Packages
Boot environments are not just for OS upgrades. They are equally valuable for testing new software installations.
Step 1: Create a Testing Environment
# bectl create pkg-test
# bectl mount pkg-test /mnt
Step 2: Install Packages into the BE
You can use pkg with a custom root to install packages into the mounted BE:
# pkg -r /mnt install -y nginx py311-django
Step 3: Boot into the Test Environment
# bectl umount pkg-test
# bectl activate pkg-test
# reboot
Step 4: Evaluate and Decide
If the packages work well, keep using this BE. If not:
# bectl activate default
# reboot
# bectl destroy pkg-test
This approach is particularly valuable when testing major package upgrades (like PostgreSQL major version bumps) that could break your application stack.
Workflow: Kernel Upgrades
Kernel upgrades carry more risk than userland changes. Boot environments handle them gracefully because the kernel lives within the root filesystem on ZFS.
# bectl create pre-kernel-update
# cd /usr/src
# make buildkernel KERNCONF=MYKERNEL
# make installkernel KERNCONF=MYKERNEL
# reboot
If the new kernel panics or causes hardware issues, select the pre-kernel-update boot environment from the boot menu (covered in the next section) and you are back to a working kernel immediately.
For custom kernel builds, the BE captures both /boot/kernel and the entire root filesystem state, so you get a complete rollback of the kernel and any related configuration changes.
Boot Menu Integration
FreeBSD's bootloader natively supports boot environments. When your system starts, you can select which BE to boot.
Accessing the Boot Menu
During boot, when you see the FreeBSD bootloader, press **Escape** (for the legacy BIOS bootloader) or look for the boot environment menu option. On systems using the lua-based bootloader (default since FreeBSD 13), you will see:
______ ____ _____ _____
| ____| | _ \ / ____| __ \
| |___ _ __ ___ ___ | |_) | (___ | | | |
| ___| '__/ _ \/ _ \| _ < \___ \| | | |
| | | | | __/ __/| |_) |____) | |__| |
|_| |_| \___|\___||____/|_____/|_____/
1. Boot Multi user [Enter]
2. Boot Single user
3. Escape to loader prompt
4. Reboot
5. Cons: Video
Options:
6. Kernel: default/kernel (1 of 2)
7. Boot Environments... <<<---
Select option **7** to see available boot environments:
Boot Environments:
1. default (NR)
2. pre-14.2-upgrade
3. pkg-test
4. Back to main menu
Select the number corresponding to the BE you want, then boot as normal. This is your emergency escape hatch when the activated BE will not boot properly.
Setting the Default Boot Environment from the Loader
You can also set the default BE from the loader prompt:
OK set currdev=zfs:zroot/ROOT/pre-14.2-upgrade:
OK boot
Renaming and Managing Boot Environments
Renaming a BE
# bectl rename pre-14.2-upgrade stable-14.1
Use descriptive names that tell you what state the BE captures. A good naming convention:
- pre- for pre-upgrade snapshots
- stable- for known-good states
- test- for experimental changes
- Date-based: 2026-03-29-pre-update
Exporting and Importing BEs
You can export a BE as a ZFS stream for backup or transfer:
# bectl export default > /backup/default-be.zfs
And import it on the same or another system:
# bectl import newbe /backup/default-be.zfs
This is useful for creating golden images or migrating system states between machines.
Space Usage and Cleanup
Boot environments use copy-on-write, so they start at virtually zero overhead. Space consumption grows only as blocks diverge between environments.
Checking Space Usage
# bectl list
BE Active Mountpoint Space Created
default NR / 4.12G 2025-09-15 10:22
pre-14.2-upgrade - - 1.87G 2026-01-15 08:00
stable-14.1 - - 892M 2026-02-20 14:30
test-nginx - - 45.3M 2026-03-25 11:00
The **Space** column shows the unique space for each BE. This is the space that would be reclaimed if you destroyed that BE.
Cleanup Strategy
Keep your BE list tidy. A reasonable policy:
1. Always keep the current active BE
2. Keep one known-good fallback BE
3. Destroy test BEs after validation (within days)
4. Destroy pre-upgrade BEs after confirming stability (within two weeks)
# bectl destroy -F test-nginx
# bectl destroy -F pre-14.2-upgrade
Checking Total Pool Usage
To understand overall ZFS pool consumption:
# zpool list
NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT
zroot 50G 12.4G 37.6G - - 8% 24% 1.00x ONLINE -
If your pool is getting full, destroying old BEs is one of the fastest ways to reclaim space.
Automating BE Creation Before Updates
Manually creating boot environments before every update gets tedious. Here is a script that automates the process.
Pre-Update Boot Environment Script
Create the file /usr/local/sbin/safe-update.sh:
sh
#!/bin/sh
# safe-update.sh - Create a boot environment before running freebsd-update
# Usage: safe-update.sh [fetch|install]
set -e
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BE_NAME="pre-update-${TIMESTAMP}"
LOGFILE="/var/log/safe-update.log"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "${LOGFILE}"
}
# Ensure we are running as root
if [ "$(id -u)" -ne 0 ]; then
echo "Error: This script must be run as root."
exit 1
fi
# Verify ZFS root
if ! zfs list zroot/ROOT > /dev/null 2>&1; then
echo "Error: ZFS root dataset not found. Boot environments require ZFS root."
exit 1
fi
# Create the boot environment
log "Creating boot environment: ${BE_NAME}"
if bectl create "${BE_NAME}"; then
log "Boot environment ${BE_NAME} created successfully."
else
log "Failed to create boot environment. Aborting update."
exit 1
fi
# Run freebsd-update
ACTION="${1:-fetch}"
case "${ACTION}" in
fetch)
log "Running: freebsd-update fetch"
freebsd-update fetch
log "Fetch complete. Review updates, then run: safe-update.sh install"
;;
install)
log "Running: freebsd-update install"
freebsd-update install
log "Install complete. Reboot when ready."
log "To roll back: bectl activate ${BE_NAME} && reboot"
;;
upgrade)
if [ -z "$2" ]; then
echo "Usage: safe-update.sh upgrade "
echo "Example: safe-update.sh upgrade 14.2-RELEASE"
exit 1
fi
log "Running: freebsd-update -r $2 upgrade"
freebsd-update -r "$2" upgrade
log "Upgrade prepared. Run: freebsd-update install && reboot"
log "To roll back: bectl activate ${BE_NAME} && reboot"
;;
*)
echo "Usage: safe-update.sh [fetch|install|upgrade ]"
exit 1
;;
esac
log "Rollback BE available: ${BE_NAME}"
# Clean up old pre-update BEs (keep last 5)
log "Checking for old pre-update boot environments..."
OLD_BES=$(bectl list -H | awk '{print $1}' | grep '^pre-update-' | sort | head -n -5)
for OLD_BE in ${OLD_BES}; do
log "Destroying old BE: ${OLD_BE}"
bectl destroy -F "${OLD_BE}"
done
log "Done."
Make it executable:
# chmod +x /usr/local/sbin/safe-update.sh
Usage
# safe-update.sh fetch # Create BE, then fetch updates
# safe-update.sh install # Create BE, then install updates
# safe-update.sh upgrade 14.2-RELEASE # Create BE, then start major upgrade
Automating with cron
You can schedule automatic update checks with BE creation via cron:
# crontab -e
0 3 * * 0 /usr/local/sbin/safe-update.sh fetch >> /var/log/safe-update.log 2>&1
This runs every Sunday at 3 AM, creating a safety BE and fetching available updates. You still review and install manually.
Boot Environments and Jails
If you run FreeBSD jails, boot environments introduce a complication: jails typically live on separate ZFS datasets (e.g., zroot/jails/webserver), which are not part of the root BE. This means rolling back a BE does not roll back your jails.
The Synchronization Problem
Consider this scenario:
1. You create a BE and upgrade the host from 14.1 to 14.2
2. You also upgrade packages inside your jails
3. Something breaks, so you roll back the host BE
4. Your jails are still running upgraded packages against a 14.1 host
Solution 1: Manual Jail Snapshots
Snapshot your jail datasets alongside the BE creation:
# bectl create pre-upgrade
# zfs snapshot -r zroot/jails@pre-upgrade
To roll back both:
# bectl activate pre-upgrade
# zfs rollback -r zroot/jails/webserver@pre-upgrade
# zfs rollback -r zroot/jails/database@pre-upgrade
# reboot
Solution 2: Include Jails in Your Automation Script
Extend the safe-update.sh script to snapshot jail datasets:
sh
# Add after BE creation in safe-update.sh
JAIL_DATASETS=$(zfs list -H -o name | grep '^zroot/jails/')
for DS in ${JAIL_DATASETS}; do
log "Snapshotting jail dataset: ${DS}@${BE_NAME}"
zfs snapshot "${DS}@${BE_NAME}"
done
Solution 3: Thick Jails on Root
For maximum rollback safety, some administrators create jails as datasets under zroot/ROOT/default/jails. This way, jails are included in the boot environment automatically. The tradeoff is larger BEs and more space consumption during divergence.
Comparison with Linux Approaches
Linux users coming to FreeBSD may be familiar with snapshot-based rollback tools. Here is how FreeBSD boot environments compare.
Snapper (openSUSE, SUSE Linux Enterprise)
Snapper creates Btrfs snapshots and integrates with the GRUB bootloader. It is the closest Linux equivalent to FreeBSD boot environments.
| Feature | FreeBSD bectl | Linux Snapper |
|---------|--------------|---------------|
| Filesystem | ZFS | Btrfs |
| Boot integration | Native loader support | GRUB plugin |
| Snapshot granularity | Full root clone | Subvolume snapshot |
| Pre/post hooks | Manual or scripted | Built-in with zypper |
| Rollback method | bectl activate + reboot | snapper rollback + reboot |
| Complexity | Simple, single tool | Multiple config files |
Timeshift (Ubuntu, Mint)
Timeshift creates Btrfs or rsync-based snapshots primarily for desktop use. It is simpler than Snapper but lacks boot-time integration. You must boot from a live USB to restore if the system will not start. FreeBSD boot environments solve this problem at the bootloader level.
NixOS Generations
NixOS takes a fundamentally different approach by building entire system configurations declaratively. Each configuration change creates a new "generation" selectable at boot. This is conceptually similar to boot environments but operates at the package manager level rather than the filesystem level. FreeBSD boot environments are filesystem-agnostic in what they capture -- everything on the root dataset is included, whether it is a config file, a compiled binary, or a log file.
Why FreeBSD's Approach Stands Out
FreeBSD boot environments benefit from ZFS's maturity and reliability. ZFS has decades of production use, checksums all data, and handles clones with minimal overhead. The tight integration between the FreeBSD bootloader, ZFS, and bectl creates a seamless experience that Linux distributions have been trying to replicate with varying degrees of success.
Frequently Asked Questions
Can I create a boot environment on a UFS filesystem?
No. Boot environments require ZFS. If your system uses UFS, you would need to reinstall FreeBSD with ZFS root to use boot environments. Since FreeBSD 13, ZFS has been the default filesystem in the installer, so newer installations typically have this already. For guidance on setting up FreeBSD, see our [FreeBSD VPS setup](/blog/freebsd-vps-setup/) guide.
How much disk space does a boot environment use?
At creation time, virtually zero. A newly created BE is a ZFS clone that shares all data blocks with its source. Space consumption grows only as files change in either the source or the clone. A BE from before a major OS upgrade typically uses 1-3 GB of unique space. A BE from before a small package change might use only a few megabytes.
Can I have more than two boot environments?
Yes. There is no hard limit on the number of boot environments. Practically, the limit is your available disk space. The boot menu will list all available BEs. However, keeping dozens of BEs around is poor hygiene. Stick to 3-5 at most and clean up regularly.
What happens if I run out of disk space with too many BEs?
ZFS will start refusing writes when the pool is full, which can make the system unstable. Monitor your pool usage with zpool list and destroy unnecessary BEs before space becomes critical. The safe-update.sh script above includes automatic cleanup of old BEs for this reason.
Can I use boot environments with encrypted ZFS?
Yes. Boot environments work with ZFS native encryption (available since FreeBSD 13). The BEs inherit the encryption properties of the parent dataset. You will need to enter your encryption passphrase at boot time before the BE selection menu appears.
Do boot environments back up my data directories?
Only if those directories are part of the root dataset. Typically, /home, /var, and /tmp are separate ZFS datasets and are not included in boot environments. This is actually desirable: your user data persists regardless of which BE you boot into. If you need to snapshot data directories, use zfs snapshot separately.
Can I boot into a different BE from a remote/headless server?
Yes. Use bectl activate via SSH and then reboot. The system will boot into the activated BE without any manual intervention at the console. If the new BE fails to boot, you may need out-of-band console access (IPMI, serial console, or hosting provider's VNC console) to select a different BE from the boot menu.
Summary
FreeBSD boot environments are one of the most practical features available to system administrators. They turn risky operations like major version upgrades, kernel replacements, and large-scale package changes into reversible actions. The workflow is straightforward:
1. Create a boot environment before any change
2. Make your changes
3. Test thoroughly
4. Keep the changes or roll back instantly
With bectl built into the base system and ZFS as the default root filesystem, every modern FreeBSD installation has this capability out of the box. There is no excuse for running upgrades without a safety net.
Start using boot environments today. The first time you roll back a failed upgrade in under a minute instead of spending hours rebuilding a system, you will never go back.