August 25, 2025

Running common services on FreeBSD is simple. But sometimes you want to run several services on the same OS instance and have each service safely ‘contained’ away from one another. A web service isn’t a lot of use unless it’s presented to the internet, but that also opens up the possibility of a security compromise. Naturally, we don’t want that. In the instance of a security bug arising and a service being compromised, it would be better to minimise the system’s exposure, wouldn’t it?

Enter jails. The lightweight answer to containerisation, that’s been around for a long time before Docker became trendy.

Setting up a Jail

As with everything in the tech industry, there are at least half a dozen different ways to achieve the same outcome. There are lots of tools for creating and managing jails on FreeBSD, but my preference is to keep it simple, so I opt for using the tools we’re given in a default installation.

The handbook has excellent documentation on jails, and I mostly stick to it, with only minor deviations.

I’m starting from the base virtual machine created in our recent YouTube video, because the following setup uses ZFS.

This then is my process for setting up jails. If you have five minutes, let’s go over it.

Configuring the host

Enable jails:

sysrc jail_enable="YES" && sysrc jail_parallel_start="YES"

Then create some base zfs filesystems:

zfs create -o mountpoint=/jails zroot/jails
zfs create zroot/jails/media
zfs create zroot/jails/_base

The handbook has extra mounts, which you may choose to use too. I’m OK with the slightly simpler approach of a ‘base’, or template, filesystem, which I snapshot for new jails. We’ll get to that in a moment.

Creating a template jail

Download the FreeBSD userland for our template:

fetch https://download.freebsd.org/ftp/releases/$(uname -m)/$(freebsd-version)/base.txz -o /jails/media/$(freebsd-version)-base.txz

Extract the files, patch the template, and create a snapshot:

tar -xf /jails/media/$(freebsd-version)-base.txz -C /jails/_base/ --unlink
cp /etc/resolv.conf /jails/_base/etc/resolv.conf
cp /etc/localtime /jails/_base/etc/localtime
freebsd-update -b /jails/_base/ fetch install
pkg -c /jails/_base install -y pkg python zsh
zfs snapshot zroot/jails/_base@$(freebsd-version)-$(date +%d-%b-%y)

We now have a ‘template’ jail on which to base new ones. I don’t run this jail, and more often than not I’ll add common packages to the template so every subsequent clone has them too. Populate with your favourite tools 🙂 Something to note: if you’re going to use Ansible to manage packages in a jail you’ll need to bootstrap pkg in it too. In the past, I’ve not done this to keep things simple, instead using pkg -j JAILNAME install BLAH from the host to install things. I suppose this is a philosophical choice — do you see the jail as an extended chroot, or a miniature virtual machine?

Nearly done now. We’ll need an /etc/jail.conf — I choose to have a ‘global’ file, with options that apply to all jails, then create jail-specific files in /etc/jail.conf.d/:

/etc/jail.conf template:

# https://man.freebsd.org/jail.conf
# https://man.freebsd.org/jail
#
ip4 = inherit;
ip6 = inherit;

# defaults to apply to all jails
exec.start = "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown jail";
exec.clean;

# filesystem
mount.devfs;
devfs_ruleset=4;
enforce_statfs=1;
securelevel=2;
#allow.mount.zfs;
#allow.mount;

# disable all the shiny things
allow.mount.nodevfs;
allow.mount.nofdescfs;
allow.mount.nolinprocfs;
allow.mount.nonullfs;
allow.mount.noprocfs;
allow.mount.notmpfs;
allow.nochflags;

# but these are always useful
allow.raw_sockets;
allow.reserved_ports;
allow.sysvipc=1;

# misc
allow.nomlock;
allow.noquotas;
allow.noread_msgbuf;
allow.noset_hostname;
allow.nosocket_af;
allow.nosysvipc;
sysvmsg=disable;
sysvsem=disable;
children.max=0;
.include "/etc/jail.conf.d/*.conf";

Creating new jails

Whenever I need a new jail then, it’s just a case of zfs cloning the template, creating a configuration file in /etc/jail.conf.d/ and adding the new jail name to jail_list in /etc/rc.conf. I have a simple shell script wrapper to do this:

#!/bin/sh
#
set -eo pipefail

SNAP=${2:-$(zfs list -t snapshot -H -o name | grep "jails/_base" | cut -f3 -d/)}

if [ -z $1 ]; then
    echo "pass new jail name"
    exit 1
else
    new=$1
fi

zfs clone zroot/jails/${SNAP} zroot/jails/${new}

test -d /etc/jail.conf.d || mkdir -p /etc/jail.conf.d
cat > /etc/jail.conf.d/${new}.conf <<EOF
${new} {
    host.hostname = "${new}.jail";
    path = "/jails/${new}";
}
EOF

sysrc jail_list+=${new}

Scaling Jails

This has been a simple introduction to getting jails up and running quickly. The handbook documentation has further details, including how to manage resource limits (handy for restricting memory and CPU, for example). If you were to take this further, scaling many jails, you might like to look at some of the tools listed in the handbook, or checkout community Ansible plays.

As we usually do with blog posts, we’ve put some resources used in this post in our GitHub repository, and made a video version:

Get in touch

We’re dropping new posts — and videos — for technical topics regularly. So make sure you’re subscribed to the YouTube channel and following this feed in your favorite RSS reader. There’s also the newsletter, if you’d like to receive updates by email.

We’d like this content series to be interactive too — so what would you like to see us cover? What FreeBSD questions can we help you tackle?  Get in touch with your ideas.