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:
shbsdinstall 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
shsysrc 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:
shcp /etc/resolv.conf /jail/webserver/etc/resolv.conf
Or set it manually:
shcat > /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:
shsysrc 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:
shkern.racct.enable=1
Reboot for the change to take effect. Verify:
shsysctl 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:
shwebserver { 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.
shpkg 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.
shpkg 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.
shpkg 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
- Identify services: List each Docker container and its purpose.
- Create base jail: Use ZFS clone from a FreeBSD base snapshot.
- Install software: Use
pkg installinside the jail for each service. - Configure networking: Use VNET jails for services that need network isolation.
- 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
- Set resource limits: Map Docker memory/CPU limits to rctl equivalents.
- 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:
shservice 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
shsysctl 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.