In the modern era of highly connected software, I have been trying to “offline” as many of my personal services as I can. The ideal scenario being a service running in an environment where it cannot reach other nodes on the network, or in some cases even route back to the public internet. To accomplish this I have been working with FreeBSD jails a quite a bit, creating a service per-jail in hopes of achieving high levels of isolation between them. This approach has a pretty notable problem at first glance: if you need to install software from remote sources in the jail, how do you keep it “offline”?

2021-02-14 update with more tips below

Note: if you’re already familiar with how great FreeBSD jails are, you can skip ahead

Without trying to start a flamewar, I think FreeBSD jails are basically what Linux containers aspired to be when they grew up. In short:

The jail mechanism is an implementation of FreeBSD’s OS-level virtualisation that allows system administrators to partition a FreeBSD-derived computer system into several independent mini-systems called jails, all sharing the same kernel, with very little overhead

From Wikipedia

Since FreeBSD 12, FreeBSD jails can “natively” do something which I haven’t seen done seriously in other container or virtualization stacks: provide full network isolation. VNET allows attaching an entire virtualized network stack to a jail, which allows you to delegate an actual hardware interface to a jail, after which the host no longer sees the interface at all!

VNET is the name of a technique to virtualize the network stack. The basic idea is to change global resources most notably variables into per network stack resources and have functions, sysctls, eventhandlers, etc. access and handle them in the context of the correct instance. Each (virtual) network stack is attached to a prison, with vnet0 being the unrestricted default network stack of the base system.

From vnet(9)

Using this capability you can set up entirely software-defined virtualized networks inside of FreeBSD jails for everything from network software testing (e.g. VPNs), to pre-flighting firewall changes in a simulated environment. If this sounds compelling to you, I recommend bouncing over to this blog post to learn more.

FreeBSD has also supported ZFS natively, including as the boot / partition for a number of major releases. It should therefore be no surprise that jails integrate very well with ZFS. In my setup each jail is its own file system in the main ZFS pool (zroot), which allows me to snapshot each jail independently for backup purposes.

“Excuse me, I believe I was promised some pkg(1) related content?”

Yes yes, getting to that. Since FreeBSD jails are not well appreciated by the broader systems engineering world, I wanted to take a moment to highlight some of what we’ve been missing out the past couple years while we’ve been screwing around with container orchestration engines.

pkg(1) is a binary package management tool akin to apt or yum. On modern FreeBSD installations compiling ports is no longer required, which is an incredibly welcome change to FreeBSD in recent years. Like its Linux relatives, pkg retrieves package metadata and tarballs from a remote repository by default, from the “offline jail” perspective this is a problem.

Jails however, are just running off slices of my host filesystem, e.g. /jails/postgresql which allows me to commands from the host in that file tree, for example:

freebsd# ls /jails/postgressql
.cshrc          bin             entropy         libexec         net             root            tmp
.profile        boot            etc             media           proc            sbin            usr
COPYRIGHT       dev             lib             mnt             rescue          sys             var

You should note that this looks surprisingly identical to what a default base install of FreeBSD, and that’s because it is!

Installing packages

The pkg(1) utility has a very useful flag -c which I can use for these jails:

-c <chroot path>, --chroot <chroot path>
    pkg will chroot in the <chroot path> environment.

From the jail host’s perspective, I can poke around the jail’s filesystem with ease, which makes installing packages trivial from the host’s perspective. For example:

freebsd# pkg -c /jails/postgresql install postgresql13-server

The above command will install the PostgreSQL v13 package and all its dependencies within the jail’s filesystem path:

# postgres --version
postgres (PostgreSQL) 13.1

“Try this one weird trick and your jails will never have to know anything about packages ever again!”


  • My host distribution is identical to the distribution installed in the jail, e.g. 12.2-RELEASE. If there were drift I’m sure there would be problems with using pkg(1) in this way.
  • This approach can feel goofy with configuration management, since the jail host is where all the configuration and package management happens.

Nonetheless, I was incredibly excited to discover this feature which made setting up and managing my heavily isolated vnet-based jails much easier!

2021-02-14 update: Here’s a tip passed along from a reader:

you might be interested in a tip for your jail post.

pkg -o ABI=... \
--chroot /path/to/jail \
--config /some/package/repo/FreeBSD.conf \
install -y ...

where ABI could be FreeBSD:14:aarch64 (FreeBSD current on armv8/aarch64) or a more prosaic FreeBSD:12:amd64 (amd64 architecture, FreeBSD 12).

the config file is similar to /etc/pkg/FreeBSD.conf and is acquired by pkg(8) before dropping into the chroot.

NB I usually need to mount_devfs inside the jail before & after pkg, as it now requires /dev/null - see #1763

This allows us to install packages into a directory that is not for the same FreeBSD version (and possibly even architecture!), such as a FreeBSD 13.0 armv8 nfs mount, running on a FreeBSD 14.0-current amd64 server.

There’s also pkg –rootdir … which is similar to chroot, but IIRC handles pre/post/user/group scripts differently.

In the future I hope to write more about the home lab FreeBSD set up that I have been working on over the past couple weeks. FreeBSD 12.x is by far the most exciting FreeBSD release I have used since the transition to full SMP in the 4.x -> 6.x timeframe. If you haven’t looked at FreeBSD lately, I highly recommend giving it a spin!