<-- All Articles

FreeBSD 15.1-RELEASE shipped on 16 June 2026, the second release cut from stable/15. For anyone running packaged base, the 15.0 to 15.1 hop is the first real test of whether pkgbase holds up under production conditions. Dan Langille published a clean walkthrough of doing exactly this on a single jail, and it is worth reading. This is the version you need when you have forty of them.

The difference is not technical difficulty. A single-jail pkgbase upgrade is four commands and takes ninety seconds. The difference is blast radius, ordering, and the fact that in a fleet you will discover you have three different kinds of jail and only remembered two of them. What follows is the runbook.

Pre-Flight: Find Out What You Actually Have

Do not start from your inventory spreadsheet. Start from the machine. The first thing to establish is which jails are on 15.0 and which are already something else, because in most shops that has drifted:

# Every running jail and the userland it reports
for j in $(jls -N name); do
    printf '%-24s %s\n' "$j" "$(jexec "$j" freebsd-version -u)"
done

# The host kernel, which governs everything below it
freebsd-version -k

The second question matters more: is each jail pkgbase-managed or freebsd-update-managed? These are completely different upgrade paths, and mixing them up is how you end up with a half-converted userland. A pkgbase jail has the base system registered in its own pkg database; a freebsd-update jail does not:

for j in $(jls -N name); do
    if pkg -j "$j" info -e FreeBSD-runtime 2>/dev/null; then
        echo "$j: pkgbase"
    else
        echo "$j: freebsd-update or tarball-extracted"
    fi
done

Anything in the second bucket does not belong in this runbook. Convert it to pkgbase deliberately, on its own change ticket, or upgrade it with freebsd-update the old way. Do not attempt an in-place conversion during a minor-version hop.

One piece of good news: the ABI does not change across a minor release. A 15.0 jail and a 15.1 jail are both FreeBSD:15:amd64, which is why third-party packages survive the hop untouched and why this upgrade is genuinely cheap compared to a major-version move.

Finally, check space. Budget roughly 1.5 GB of package churn per jail plus whatever your snapshots hold onto for the length of the change window. If the pool is already past 80 percent used you have a fragmentation problem before you have an upgrade problem -- see ZFS Tuning for Production FreeBSD Servers for why that threshold matters.

zpool list -o name,size,alloc,free,cap,frag
zfs list -o name,used,avail -r zroot/jails

The Safety Net Goes In First

Nothing gets upgraded until rollback is provably in place. On the host, that is a boot environment. In the jails, that is a ZFS snapshot per dataset.

# Host: clone the current system into a named BE
bectl create -e default 15.0-preupgrade-20260726
bectl list -H

# Jails: one recursive snapshot, one name, one thing to roll back to
zfs snapshot -r zroot/jails@pre-15.1-20260726
zfs list -t snapshot -r zroot/jails

Use a recursive snapshot on the parent dataset rather than looping over children individually. It gives you a single consistent snapshot name across every jail, which makes the rollback script trivial and makes cleanup a one-liner three weeks later.

Note that bectl in 15.1 gained a -E flag to create an empty boot environment without cloning the running system. That is useful for building a fresh install into a BE. It is not what you want here -- you want the clone.

Two caveats worth saying out loud. First, a ZFS rollback discards everything written to that dataset since the snapshot. For a stateful jail running a database, take the snapshot immediately before the upgrade and keep the window short, or dump the data separately. Second, a recursive snapshot is consistent within a pool, not across pools. If a jail has a second dataset mounted in from elsewhere, snapshot that too.

The Upgrade Sequence

Order matters: host first, jails second. FreeBSD supports an older userland running on a newer kernel, not the reverse. Upgrade the host to 15.1, reboot into it, confirm it is healthy, and only then start on the jails. Jails share the host kernel, so the host is the hard dependency for everything else.

The mechanism is the base repository. On 15.x the shipped definition lives at /etc/pkg/FreeBSD-base.conf and resolves base_release_${VERSION_MINOR}. To move a system from 15.0 to 15.1, drop an override into the local repo directory pinning the minor explicitly:

# /usr/local/etc/pkg/repos/FreeBSD-base.conf
FreeBSD-base {
    url = "pkg+https://pkg.freebsd.org/${ABI}/base_release_1";
    mirror_type = "srv";
    signature_type = "fingerprints";
    fingerprints = "/usr/share/keys/pkg";
    enabled = yes;
}

Inside a jail, that path is relative to the jail root -- for example /usr/local/jails/web01/usr/local/etc/pkg/repos/FreeBSD-base.conf. Write it there, then drive pkg from the host with -j against a running jail, or -r against a stopped jail's root directory:

# Refresh the base catalogue, forcing a full fetch
pkg -j web01 update -f -r FreeBSD-base

# Review what is about to change, then apply
pkg -j web01 upgrade -n -r FreeBSD-base
pkg -j web01 upgrade -r FreeBSD-base

# The two packages split out in 15.1 -- install explicitly
pkg -j web01 install FreeBSD-pam FreeBSD-zstd

service jail restart web01

Always run the dry run. On a fleet you are looking for the jail where the plan is forty packages instead of the two hundred everywhere else -- that is the one with a hand-patched base someone forgot to mention.

Post-Upgrade Verification

Do not close the ticket on the basis of a version string. Check the version, then check the things that quietly did not land:

jexec web01 freebsd-version -u          # expect 15.1-RELEASE

# Did the split packages actually arrive?
pkg -j web01 info -e FreeBSD-pam  && echo "pam ok"
pkg -j web01 info -e FreeBSD-zstd && echo "zstd ok"

# Missing files, broken dependencies, stale linkage
pkg -j web01 check -d -a
pkg -j web01 audit -F

# Config files pkg refused to overwrite
find /usr/local/jails/web01/etc -name '*.pkgnew' -o -name '*.pkgsave'

Anything that turns up as .pkgnew is a config file you had modified and pkg declined to clobber. Merge those by hand. There will be fewer than you expect on a minor hop, and they will cluster in /etc/rc.d, /etc/ssh, and /etc/pam.d.

Then restart services and watch them come back, rather than assuming they did. If your metrics pipeline is in place this is where it earns its keep -- process counts, listener sockets, and error rates per jail, as covered in the FreeBSD Server Monitoring Guide.

What Actually Breaks

OpenPAM and Zstandard are now separate packages. In 15.1 they split out as FreeBSD-pam and FreeBSD-zstd. A naive upgrade can leave them behind, and the failure mode is nasty: PAM missing means sshd and login stop authenticating. Verify both are present before you drop your existing session, and keep a second shell open on the host while you work.

installworld is blocked on package-based systems. This is the biggest trap for anyone with source-upgrade muscle memory. If your Ansible roles, jail build scripts, or a colleague's fingers reach for make installworld against a pkgbase root, it refuses. That is correct behaviour, not a bug -- but grep your automation for it now rather than at 2 a.m.

The default shell changed from csh to sh. Release images now give root and the freebsd user /bin/sh. Existing jails keep their master.passwd, so an upgrade will not change them -- but any jail you rebuild from a 15.1 image will differ from its neighbours. Automation carrying setenv or csh redirection syntax breaks silently against the new default. Check with jexec web01 getent passwd root and standardise deliberately rather than discovering the split later.

pwd(1) now defaults to logical mode. Per POSIX, it behaves as pwd -L. Any script that changes directory through a symlink and then parses pwd output now gets the logical path rather than the resolved one. Where a script depends on the physical path, make it say pwd -P explicitly.

Worth noting on the storage side: OpenZFS is 2.4.2 in 15.1. New pool feature flags are not enabled automatically, and you should not enable them until every host that might import the pool is on 15.1. That includes your backup target.

Rollback, Concretely

For a jail: stop it, roll the dataset, start it. The -r on rollback destroys any snapshots taken after the target, which is what you want mid-incident:

service jail stop web01
zfs rollback -r zroot/jails/web01@pre-15.1-20260726
service jail start web01
jexec web01 freebsd-version -u          # expect 15.0-RELEASE again

For the host, activate the pre-upgrade boot environment and reboot. Use -t for a one-shot activation that reverts on the following boot, so a rollback that itself goes badly does not strand you:

bectl activate -t 15.0-preupgrade-20260726
shutdown -r now

Rehearse this on the canary before you touch anything that matters. A rollback procedure you have never executed is a hypothesis, not a safety net.

Sequencing the Fleet

Pick a canary that is representative rather than trivial: one that authenticates users over SSH so PAM gets exercised, one that links something against zstd, and one that runs a real service under real load. An idle jail proves nothing. Let it soak for 48 hours before the next batch -- PAM and linkage failures show up on the next login or the next log rotation, not in the first five minutes.

Then batch five to ten jails at a time, grouped by service tier, and never put both halves of an HA pair in the same batch. Automate the mechanical parts: snapshot creation, repo config placement, the update and upgrade calls, and the verification script. Do not automate two things -- the config file merge, and the decision to proceed to the next batch. Both need a human who has actually read the last batch's logs.

Time the whole exercise against the ports tree. Running a base upgrade while ports are in flux gives you two moving variables at once; the ports freeze is the natural upgrade window precisely because third-party packages hold still. And if you have not yet decided whether 15.1 justifies the change ticket at all, start with what changed in 15.1 and who should upgrade.

The takeaway: pkgbase makes the minor-version hop mechanically boring, which is exactly the point. The risk has moved out of the upgrade command and into the process around it -- knowing which jails are genuinely pkgbase, having rollback in place before you start, and catching the split packages before they lock you out of a box. Get those three right and 15.1 is a Tuesday afternoon. Skip them and it is a Tuesday night.

Planning a fleet-wide 15.1 upgrade?

We run FreeBSD upgrade programs for shops with dozens to hundreds of jails: pkgbase migration, rollback design, canary sequencing, and the automation that makes the next minor release boring.

Book a Free 30-Min Review