SSD Nodes Learn 🎉 VPS from $4.99/mo
Guides Matt ConnorBy Matt Connor · Updated 2026-08-12

FreeBSD jails vs Docker containers

Jails isolate a full FreeBSD userland; Docker ships layered images from a registry. How the two models differ on software, state, networking and limits.

FreeBSD jails vs Docker containers, in one paragraph

FreeBSD jails and Docker containers solve the same problem in two different shapes. Both run isolated userlands on one shared kernel, so neither one is a virtual machine. What differs is what goes inside. A Docker container runs one process from a layered image you pulled from a registry. A jail runs a complete FreeBSD userland: its own /etc, its own rc startup scripts, its own pkg database, and as many processes as you want. Almost every other difference on this page follows from that one.

SSD Nodes does not offer FreeBSD images. You cannot rent a FreeBSD server on this platform, and nothing below is an install guide for a machine you can buy here. This is a comparison of two isolation models, written so you can tell which one a workload actually wants, and so you can read a FreeBSD team's setup without guessing.

What a jail actually is

Jails arrived in FreeBSD 4.0 in March 2000, which makes them older than cgroups and roughly a decade older than Docker. The mechanism is one kernel call. jail(8) takes a directory tree and starts processes inside it with a jail ID attached, and the kernel then refuses a fixed set of operations for any process carrying that ID. A jailed process cannot see processes outside its jail, cannot mount or unmount filesystems, cannot load kernel modules, and cannot bind to network addresses the jail was not given. There is no separate namespace type to learn and no per-feature opt-in: the restrictions arrive as one unit, adjusted by parameters in the jail's config.

On the host, jls lists running jails and jexec web sh drops you into a shell inside the one named web.

You build a jail by putting a FreeBSD userland into a directory. The base system does that for you:

sudo bsdinstall jail /usr/local/jails/containers/web

That fetches the base distribution set for your release and runs the ordinary post-install steps, so you set a root password and pick a timezone exactly as you would on a new server. The result is a FreeBSD installation sitting in a folder. You then describe it in /etc/jail.conf:

web {
  host.hostname = "web.example.internal";
  path = "/usr/local/jails/containers/web";
  ip4.addr = "10.0.0.10";
  exec.start = "/bin/sh /etc/rc";
  exec.stop = "/bin/sh /etc/rc.shutdown";
  mount.devfs;
}

Start it, then check it:

sudo service jail start web
jls

jls should now list web with a JID, its hostname and its IP address. If the jail does not appear, run sudo jail -c web directly. It applies the same configuration in the foreground and prints the parameter it could not accept, instead of leaving the failure in the service output.

The line worth reading twice is exec.start = "/bin/sh /etc/rc". Starting a jail runs FreeBSD's normal boot script inside it, so the jail brings up every service enabled in its own /etc/rc.conf. A Docker container has no equivalent step, because it runs the image's entrypoint process and stops when that process stops.

How you get software in: images and registries vs a userland you fill

This is the difference you feel on day one.

With Docker you name software and receive it. docker pull nginx fetches a layered, content-addressed image that somebody else built and tested, and docker compose up -d starts it with its volumes and its network attached. The registry is the product. Most of the value in a Docker workflow is that thousands of projects publish a working image, which is what makes running Docker on a VPS a short job rather than a project.

FreeBSD ships no default public registry of jail images. You create an empty userland and install into it, the same way you would set up a bare server. That is more typing. It is also more transparent, because what runs in the jail is what pkg put there, out of the same package set the host uses.

Tooling makes it short. BastilleBSD is the common jail manager and it is a package:

sudo pkg install bastille
sudo sysrc bastille_enable=YES
sudo bastille setup
sudo bastille bootstrap 15.1-RELEASE

bastille setup configures networking, storage and the firewall for you. bastille bootstrap downloads a release once, and every jail you create afterwards reuses it. FreeBSD 15.1 is the current production release, out in June 2026; substitute whichever release you run.

Creating a jail is then one command, and filling it is one more:

sudo bastille create web 15.1-RELEASE 10.17.89.10/24
sudo bastille pkg web install nginx
sudo bastille service web nginx start
sudo bastille console web

bastille console web gives you a login shell inside the jail, and bastille list shows what exists on the host. To repeat a build, Bastille templates hold the steps in a file and apply them to a jail, which is the closest thing this world has to a Dockerfile. A template is replayed on each jail. Nothing arrives prebuilt.

So the honest summary is short. Docker hands you other people's builds. Jails hand you your own installs. If the software on your shortlist ships as a container image and nothing else, that settles the question before any other axis gets a vote.

State and upgrades: the part ZFS changes

Docker splits state on purpose. The container filesystem is disposable, your data lives in a named volume or a bind mount, and an upgrade is docker compose pull followed by docker compose up -d. The container is replaced, and anything you did not put in a volume is gone. That is a feature when you follow the rule and a data-loss incident when you forget it, which is why the choice between bind mounts and named volumes carries so much weight in a Compose stack.

A jail does not split state, and ZFS is the reason that works. The whole jail is one dataset:

sudo zfs snapshot zroot/jails/containers/web@pre-upgrade
sudo pkg -j web upgrade
sudo zfs rollback zroot/jails/containers/web@pre-upgrade

Check the real dataset name with zfs list before you run this; the path above is the layout the handbook uses. The snapshot takes about a second and costs almost no space until the jail's contents change. If the upgrade breaks the service, the rollback returns the entire userland to its earlier state, including the package database and the config files you edited by hand at 2am. Docker has no built-in equivalent, because its model assumes you never wanted one.

zfs clone is the other half. A clone of a snapshot is a new writable jail that shares unchanged blocks with its parent, so a staging copy of a 3 GB jail costs almost nothing on disk until you start changing it. That is how a FreeBSD admin builds a "same as production" jail to rehearse an upgrade.

The base system upgrade is separate from packages. For a jail holding its own copy of the userland:

sudo freebsd-update -b /usr/local/jails/containers/web fetch install

Thin jails avoid repeating that work. They mount one shared read-only base through nullfs and give each jail a small writable layer of its own, so you patch the base once and every jail sees the result. Bastille creates thin jails by default.

Networking: published ports vs an addressing decision

Docker decides networking for you and asks you to publish the exceptions. Containers land on a bridge, they reach each other by service name on a user-defined network, and -p 8080:80 exposes one of them to the host. Docker writes its own packet filter rules to make that happen, which is also how a published container port walks straight past ufw.

A jail makes you choose the model up front, and there are two.

Shared IP. ip4.addr = "10.0.0.10" adds that address to an existing host interface and restricts the jail to it. The jail has no network stack of its own, so it cannot run its own firewall. It also cannot really bind to every address: a jailed socket asking for 0.0.0.0 is rewritten by the kernel to the jail's own address. Two jails cannot both listen on port 80 of the same address, so you give each one an address, or you put a reverse proxy in front.

VNET. Add vnet; to the jail and it gets a full network stack: its own interfaces, its own routing table, its own firewall rules. You wire it to the host with an epair, a virtual cable with one end on each side, and put the host end on a bridge. This is the closest match to what Docker gives you, and it is the mode behind Bastille's -V and -B jail types.

Forwarding a host port into a jail is a pf redirect rule. Bastille wraps it:

sudo bastille rdr web tcp 80 80

There is no EXPOSE and no automatic publishing. Nothing reaches a jail unless its address or a redirect rule allows it. That is a slower start and a much quieter firewall.

Resource limits: cgroups vs rctl

Docker limits a container with cgroups, and the limits live where the container is defined: --memory=1g --cpus=1.5 on the command line, or the matching keys in a Compose file. If you already keep your stack in a Docker Compose file on a VPS, the limit sits beside the service it applies to and travels with it in git.

FreeBSD uses rctl, and it is a subsystem you have to switch on. Resource accounting is off by default because it costs a little on every allocation. Add the tunable to /boot/loader.conf and reboot:

kern.racct.enable=1

Then set a rule and watch it:

sudo rctl -a jail:web:vmemoryuse:deny=1g
rctl -hu jail:web

rctl -hu jail:web prints the jail's current usage in human-readable units, so you can see how close it sits to the limit before anything breaks. The deny action makes the over-limit allocation fail inside the jail, so you see the application's own allocation error rather than a kill message on the host.

Rules added with rctl -a vanish at the next reboot. FreeBSD's rctl service reloads them from /etc/rctl.conf, so write the rule into that file and enable the service:

sudo sysrc rctl_enable=YES

This is the axis where Docker is plainly more convenient. A limit in a Compose file is reviewed with the service it constrains. An rctl rule is a line in a separate file naming a jail defined somewhere else.

When the answer is a virtual machine: bhyve

A jail shares the host kernel, so some things are permanently out of reach. It cannot run a different kernel version, it cannot load a kernel module, and it cannot run Linux binaries the way a Linux container does. FreeBSD has a Linux compatibility layer, the linuxulator, but it implements a subset of Linux system calls and it is not a general answer for arbitrary Linux images.

bhyve is FreeBSD's hypervisor, and it is the right tool when you need a real machine boundary: a different operating system, a different kernel, or a tenant you would rather not share a kernel with. You pay in memory that is reserved instead of shared, and in a second kernel to patch. This is the same decision you make on Linux between containers and full virtual machines, and it is the one that decides whether you need a VPS that supports nested virtualization underneath.

The ecosystem, which is the honest reason most teams use Docker

Everything above is about the model. What decides the choice for most teams is the size of the world around each one.

Docker brings Docker Hub and GHCR, docker compose, Kubernetes when one box stops being enough, CI runners with container support already wired in, and a one-command quickstart in nearly every project's README. Jails bring the FreeBSD ports tree, which is large and carefully maintained, plus a much smaller set of ready-to-run application bundles. When a project publishes a container image and nothing else, the FreeBSD route is to read its documentation and assemble the parts yourself.

Jails earn their place on the other side of that trade. You want them when you already run ZFS and value snapshot and rollback of a whole service, when your services are FreeBSD native, when you want a full userland per tenant instead of a single process, or when you want the kernel, the packet filter, the filesystem and the documentation maintained together as one system. That last point is what people mean when they call FreeBSD coherent, and it gets more room in the wider comparison of Linux and FreeBSD as server platforms and in what FreeBSD 15 changed for server use.

One closing judgement. If your team already knows Docker, the cost of moving is real and the payoff has to be specific. Do not switch for isolation quality; the two models are close enough that your configuration matters more. Switch because you want ZFS-backed rollback of entire services, or because you are on FreeBSD already.

FAQ

Can I run Docker images on FreeBSD?

Not Linux images, and not as a supported path. FreeBSD does have OCI container support: sudo pkg install -y podman-suite installs Podman, which runs containers through ocijail, a runtime that creates real jails underneath. It needs fdescfs mounted on /dev/fd for the container monitor, and pf for container NAT (network address translation). FreeBSD-native OCI images work best. Linux images additionally require the Linux compatibility layer, and as of August 2026 the FreeBSD Podman port is still described as experimental. If your deployment is a stack of Linux images, run it on Linux.

Are FreeBSD jails more secure than Docker containers?

Both share one host kernel, so a kernel bug is a risk to both, and neither is the boundary you would pick for genuinely untrusted code. The difference is the starting point. A jail begins with a broad set of operations refused and you re-enable them one parameter at a time. A Docker container begins as root inside a set of namespaces with some capabilities dropped, and further hardening is opt-in. In practice, configuration decides more than the model: a jail running with allow.mount and allow.raw_sockets enabled is not safer than a carefully configured container.

How do I back up a jail?

Snapshot the dataset and send it. sudo zfs snapshot zroot/jails/containers/web@backup, then zfs send that snapshot to another pool or into a file you copy off the box. Because a jail keeps its whole userland in one dataset, the snapshot captures the installed packages and the data at one consistent point, along with every config file you edited by hand. That is the opposite of the Docker habit, where you back up the named volumes and the Compose file and rebuild the rest from the image.

Do I need BastilleBSD, or is the base system enough?

The base system is enough, and it is the better place to start. jail.conf, jls, jexec and service jail start cover the entire model, and once you know them you can read any FreeBSD host without learning that host's tooling first. Bastille is a convenience layer above it: it bootstraps releases, creates thin jails, applies templates and writes pf redirect rules for you. Learn the base commands first, then add Bastille when the number of jails makes the typing tedious.