FreeBSD.software
Home/Guides/FreeBSD Jails: The Complete Container Guide
review·2026-04-09·12 min read

FreeBSD Jails: The Complete Container Guide

Complete guide to FreeBSD jails: history and design, manual creation, VNET networking, rctl resource limits, ZFS integration, management tools (Bastille, iocage, pot), and migration from Docker.

FreeBSD Jails: The Complete Container Guide

FreeBSD jails are the original operating system-level virtualization technology. Introduced in FreeBSD 4.0 in 2000 -- a full thirteen years before Docker -- jails provide lightweight, isolated environments that share the host kernel while maintaining strict separation of processes, filesystems, and network resources. Every process inside a jail sees its own root filesystem, its own process tree, and optionally its own network stack, but runs on the host kernel with no hypervisor overhead.

Jails are not containers in the Docker sense. They are more secure, more mature, and operate at a lower level of abstraction. Where Docker packages applications with their dependencies, jails isolate entire operating system environments. This guide covers jail history and architecture, manual creation, VNET networking, resource limits with rctl, ZFS integration, management tools (Bastille, iocage, pot), and practical guidance for teams migrating from Docker.

History and Architecture

Origins

Poul-Henning Kamp introduced jails in FreeBSD 4.0 (March 2000) to solve a practical problem: shared hosting providers needed to isolate customers on the same physical hardware. The chroot mechanism was insufficient -- it only restricted filesystem visibility, not process visibility, network access, or system calls.

Jails extended chroot with:

  • Process isolation (a jail cannot see processes outside its boundary)
  • Network isolation (a jail is bound to specific IP addresses)
  • Filesystem isolation (the jail's root is a separate directory tree)
  • Restricted system calls (jails cannot modify the host kernel)

Architecture

A jail runs on the host kernel. There is no separate kernel, no virtualization layer, and no hardware emulation. The kernel enforces jail boundaries through checks in every relevant system call. When a process in a jail calls getpid(), it sees PID 1 for its init process, not the host's PID.

Key architectural properties:

  • Single kernel: All jails share the host kernel. Kernel modules, sysctl values, and kernel version are shared.
  • Filesystem root: Each jail has a root directory on the host filesystem. Inside the jail, this appears as /.
  • Network: Classic jails inherit an IP address from the host. VNET jails get their own network stack.
  • Security: A jailed root user cannot escape the jail. The jail cannot load kernel modules, mount filesystems (by default), or modify the host.

Manual Jail Creation

Preparing the Base System

Download and extract the FreeBSD base system into a directory:

sh
# Create the jail root directory mkdir -p /jail/webserver # Fetch and extract the base system fetch https://download.freebsd.org/releases/amd64/14.1-RELEASE/base.txz tar -xf base.txz -C /jail/webserver

Or use bsdinstall:

sh
bsdinstall jail /jail/webserver

Configuring /etc/jail.conf

FreeBSD's jail configuration lives in /etc/jail.conf:

sh
# /etc/jail.conf # Global defaults exec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown jail"; exec.clean; mount.devfs; allow.raw_sockets; # Web server jail webserver { host.hostname = "webserver.local"; ip4.addr = "em0|10.0.1.50/24"; path = "/jail/webserver"; exec.start += ""; exec.stop += ""; }

Starting the Jail

sh
sysrc jail_enable="YES" sysrc jail_list="webserver" service jail start webserver

Entering the Jail

sh
# Get a shell inside the jail jexec webserver /bin/sh # Run a command inside the jail jexec webserver pkg install nginx jexec webserver sysrc nginx_enable="YES" jexec webserver service nginx start

Listing and Managing Jails

sh
# List running jails jls # Detailed jail information jls -v # Stop a jail service jail stop webserver # Restart a jail service jail restart webserver

Jail DNS and Resolv.conf

Copy the host's DNS configuration into the jail:

sh
cp /etc/resolv.conf /jail/webserver/etc/resolv.conf

Or set it manually:

sh
cat > /jail/webserver/etc/resolv.conf << 'EOF' nameserver 10.0.1.1 nameserver 1.1.1.1 search example.com EOF

VNET: Virtual Network Stack

VNET gives each jail its own independent network stack -- its own interfaces, routing table, firewall rules, and ARP table. This is a significant upgrade from the classic shared-IP model.

Creating a VNET Jail

First, create a bridge interface on the host:

sh
# Create bridge ifconfig bridge0 create ifconfig bridge0 addm em0 ifconfig bridge0 up

Add to /etc/rc.conf for persistence:

sh
sysrc cloned_interfaces+="bridge0" sysrc ifconfig_bridge0="addm em0 up"

Configure the VNET jail in /etc/jail.conf:

sh
# /etc/jail.conf webserver { host.hostname = "webserver.local"; path = "/jail/webserver"; vnet; vnet.interface = "epair0b"; exec.prestart = "ifconfig epair0 create"; exec.prestart += "ifconfig bridge0 addm epair0a"; exec.prestart += "ifconfig epair0a up"; exec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown jail"; exec.poststop = "ifconfig bridge0 deletem epair0a"; exec.poststop += "ifconfig epair0a destroy"; mount.devfs; allow.raw_sockets; enforce_statfs = 2; }

Inside the jail, configure the network:

sh
# /jail/webserver/etc/rc.conf ifconfig_epair0b="inet 10.0.1.50 netmask 255.255.255.0" defaultrouter="10.0.1.1"

VNET gives each jail full network isolation: its own firewall, ability to run DHCP clients or VPN software, and no IP conflicts with the host.

Resource Limits with rctl

FreeBSD's rctl (Resource Limits) framework controls how much CPU, memory, disk I/O, and other resources a jail can consume.

Enabling rctl

Add to /boot/loader.conf:

sh
kern.racct.enable=1

Reboot for the change to take effect. Verify:

sh
sysctl kern.racct.enable

Setting Resource Limits

sh
# Limit jail to 2GB of RAM rctl -a jail:webserver:memoryuse:deny=2g # Limit to 50% of one CPU core rctl -a jail:webserver:pcpu:deny=50 # Limit open files rctl -a jail:webserver:openfiles:deny=10000 # Limit number of processes rctl -a jail:webserver:maxproc:deny=500 # Limit disk bandwidth (read + write, bytes/sec) rctl -a jail:webserver:readbps:throttle=50m rctl -a jail:webserver:writebps:throttle=50m # View current limits rctl -l jail:webserver # View current usage rctl -u jail:webserver # Remove a limit rctl -r jail:webserver:memoryuse

Persistent Limits via jail.conf

Add rctl rules to /etc/jail.conf:

sh
webserver { host.hostname = "webserver.local"; path = "/jail/webserver"; ip4.addr = "em0|10.0.1.50/24"; exec.prestart += "rctl -a jail:webserver:memoryuse:deny=2g"; exec.prestart += "rctl -a jail:webserver:pcpu:deny=200"; exec.prestart += "rctl -a jail:webserver:maxproc:deny=500"; exec.poststop += "rctl -r jail:webserver"; exec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown jail"; mount.devfs; }

ZFS and Jails

ZFS is the ideal filesystem for jails. It provides instant jail creation via clones, efficient snapshots for backup, and dataset-level quotas for resource control.

ZFS Dataset per Jail

sh
# Create a base dataset zfs create zroot/jails zfs create zroot/jails/base # Extract base into the dataset tar -xf base.txz -C /zroot/jails/base # Snapshot the base zfs snapshot zroot/jails/base@14.1-RELEASE # Create a jail by cloning (instant, zero disk space) zfs clone zroot/jails/base@14.1-RELEASE zroot/jails/webserver zfs clone zroot/jails/base@14.1-RELEASE zroot/jails/database zfs clone zroot/jails/base@14.1-RELEASE zroot/jails/mailserver

ZFS Quotas for Jails

sh
# Limit a jail's disk usage zfs set quota=20G zroot/jails/webserver # Set a reservation (guaranteed space) zfs set reservation=5G zroot/jails/database

Snapshot and Rollback

sh
# Snapshot a jail before changes zfs snapshot zroot/jails/webserver@pre-upgrade # Rollback if something breaks service jail stop webserver zfs rollback zroot/jails/webserver@pre-upgrade service jail start webserver

Jail Migration with ZFS Send/Receive

sh
# Snapshot and send to another host zfs snapshot zroot/jails/webserver@migrate zfs send zroot/jails/webserver@migrate | ssh newhost zfs receive zroot/jails/webserver # On the new host, configure jail.conf and start

Management Tools

Manual jail management works but does not scale. Several tools automate jail creation, networking, package management, and lifecycle operations.

Bastille

Bastille is a modern jail management tool that uses a template system for automated jail provisioning.

sh
pkg install bastille sysrc bastille_enable="YES"

Basic operations:

sh
# Bootstrap a release bastille bootstrap 14.1-RELEASE # Create a jail bastille create webserver 14.1-RELEASE 10.0.1.50 # Start/stop bastille start webserver bastille stop webserver # Enter a jail bastille console webserver # Install packages bastille pkg webserver install nginx # Apply a template (automated configuration) bastille template webserver bastillebsd/nginx # List jails bastille list # Destroy a jail bastille destroy webserver

Bastille templates are declarative configuration files that install packages, copy files, enable services, and execute commands. They are stored in Git repositories and can be shared.

iocage

iocage is a ZFS-based jail manager that heavily leverages ZFS snapshots and clones.

sh
pkg install py311-iocage iocage activate zroot iocage fetch --release 14.1-RELEASE

Basic operations:

sh
# Create a jail iocage create -n webserver -r 14.1-RELEASE ip4_addr="em0|10.0.1.50/24" # Start/stop iocage start webserver iocage stop webserver # Console access iocage console webserver # Snapshot iocage snapshot webserver # List jails iocage list # Set resource limits iocage set memoryuse=2G:deny webserver iocage set maxproc=500:deny webserver # Destroy iocage destroy webserver

iocage excels at ZFS integration. Every jail operation (create, snapshot, clone, migrate) uses ZFS primitives.

pot

pot is a jail management framework focused on simplicity and compatibility with container orchestration tools like Nomad.

sh
pkg install pot pot init

Basic operations:

sh
# Create a pot (jail) pot create -p webserver -t single -b 14.1 # Start/stop pot start webserver pot stop webserver # Run a command pot exec webserver pkg install nginx # Snapshot pot snapshot -p webserver # Clone pot clone -p webserver -P webserver-v2 # Export (for distribution) pot export -p webserver # Destroy pot destroy -p webserver

pot integrates with HashiCorp Nomad, making it suitable for orchestrated deployments across multiple FreeBSD hosts.

Tool Comparison

| Feature | Bastille | iocage | pot |

|---|---|---|---|

| Configuration | Templates (Bastillefile) | CLI flags/properties | CLI/flavours |

| ZFS integration | Good | Excellent | Good |

| VNET support | Yes | Yes | Yes |

| Networking | Manual/template | Built-in VNET | Built-in bridge |

| Orchestration | No | No | Nomad integration |

| Template/image sharing | Git repos | No standard | Image export/import |

| Learning curve | Low | Medium | Low |

| Active development | Yes | Slower | Yes |

Migration from Docker

Teams moving from Docker to FreeBSD jails face conceptual differences that matter more than technical ones.

Conceptual Mapping

| Docker Concept | FreeBSD Jail Equivalent |

|---|---|

| Container | Jail |

| Image | ZFS snapshot/clone of base system |

| Dockerfile | Bastille template or shell script |

| docker-compose | jail.conf + scripts |

| Docker Hub | No central registry (build locally or use templates) |

| Overlay filesystem | ZFS clones |

| Docker network | VNET + bridge |

| Volume mount | nullfs mount |

| Resource limits (cgroups) | rctl |

| Container orchestration | pot + Nomad |

Key Differences

No image registry: FreeBSD jails do not have a Docker Hub equivalent. You create base images from FreeBSD releases and customize them locally. Share images via ZFS send/receive or exported tarballs.

Full OS environment: A jail contains a complete FreeBSD userland, not a minimal application image. This means jails are larger (a base jail is ~300 MB vs a minimal Docker image at 5 MB) but include all system tools.

Security model: Jails are stronger isolation than Docker containers by default. A jailed root cannot escape to the host. Docker requires additional configuration (AppArmor, seccomp profiles) to approach jail-level isolation.

Package management: Inside a jail, you use pkg install to add software, just like on the host. There is no separate package format for jailed applications.

Practical Migration Steps

  1. Identify services: List each Docker container and its purpose.
  2. Create base jail: Use ZFS clone from a FreeBSD base snapshot.
  3. Install software: Use pkg install inside the jail for each service.
  4. Configure networking: Use VNET jails for services that need network isolation.
  5. Migrate data volumes: Copy data into the jail's filesystem or use nullfs mounts:
sh
# In jail.conf, mount a host directory into the jail webserver { mount.fstab = "/etc/fstab.webserver"; # ... }
sh
# /etc/fstab.webserver /data/webserver/html /jail/webserver/usr/local/www nullfs rw 0 0
  1. Set resource limits: Map Docker memory/CPU limits to rctl equivalents.
  2. Automate with templates: Create Bastille templates or shell scripts for repeatable provisioning.

You gain stronger security isolation, ZFS snapshots, boot environments, no daemon dependency, and 25+ years of maturity. You lose the Docker Hub ecosystem, cross-platform portability, and docker-compose convenience (pot + Nomad comes closest).

FAQ

Can I run Docker inside a FreeBSD jail?

No. Docker requires the Linux kernel (specifically cgroups and namespaces). Docker does not run natively on FreeBSD. You can run Docker inside a bhyve VM running Linux on FreeBSD, but not inside a jail. If you need OCI-compatible containers on FreeBSD, look at runj, which implements the OCI runtime spec using jails.

How many jails can I run on one FreeBSD host?

There is no hard limit. The practical limit depends on available RAM and disk space. A base FreeBSD jail uses about 300 MB of disk (less with ZFS clones sharing blocks) and minimal RAM when idle. Production hosts commonly run 10-50 jails. With thin provisioning via ZFS clones, disk is rarely the bottleneck.

Are jails as secure as VMs?

Jails share the host kernel, so a kernel vulnerability could theoretically allow a jail escape. VMs provide stronger isolation through hardware-assisted virtualization. However, FreeBSD jail escapes are extremely rare in practice -- the jail boundary has been hardened over 25 years. For most threat models, jails provide sufficient isolation. For hosting untrusted code, consider bhyve VMs.

How do I update jails when FreeBSD releases a security patch?

For jails based on ZFS clones of a base image:

sh
# Update the base freebsd-update -b /zroot/jails/base fetch install # Snapshot the updated base zfs snapshot zroot/jails/base@14.1-p5 # For each jail, either update in-place: freebsd-update -b /zroot/jails/webserver fetch install # Or recreate from the updated base: service jail stop webserver zfs destroy zroot/jails/webserver zfs clone zroot/jails/base@14.1-p5 zroot/jails/webserver # Re-apply jail-specific configuration service jail start webserver

Can jails run a different FreeBSD version than the host?

Jails can run the same or older userland than the host kernel. The kernel is shared, so the jail cannot run a newer version than the host. A FreeBSD 14.1 host can run jails with 14.0 or 13.3 userland but not 15.0. For running different kernel versions, use bhyve VMs.

How do I back up a jail?

With ZFS, jail backups are trivial:

sh
# Snapshot zfs snapshot zroot/jails/webserver@backup-$(date +%Y%m%d) # Send to a remote backup server zfs send zroot/jails/webserver@backup-$(date +%Y%m%d) | ssh backup zfs receive backup/jails/webserver

Without ZFS, use tar:

sh
service jail stop webserver tar czf /backups/webserver-$(date +%Y%m%d).tar.gz -C /jail webserver service jail start webserver

How do I give a jail internet access through NAT?

Enable IP forwarding on the host and configure PF for NAT:

sh
# /etc/sysctl.conf net.inet.ip.forwarding=1
sh
# /etc/pf.conf ext_if = "em0" nat on $ext_if from 10.0.99.0/24 to any -> ($ext_if) pass from 10.0.99.0/24 to any
sh
sysctl net.inet.ip.forwarding=1 pfctl -f /etc/pf.conf

Assign the jail a private IP in the 10.0.99.0/24 range and set the host as its default gateway.

Get more FreeBSD guides

Weekly tutorials, security advisories, and package updates. No spam.