How to Set Up FreeBSD Jails: Complete Guide 2026
FreeBSD jails are the original operating-system-level virtualization. They shipped in FreeBSD 4.0 in March 2000 -- a full thirteen years before Docker existed. While the container world on Linux reinvents isolation primitives every few years, jails have been stable, production-tested, and incrementally improved for over two decades.
A jail gives you an isolated userland with its own root filesystem, process tree, network stack, and user accounts, running on the host kernel with near-zero overhead. No hypervisor. No emulation layer. No container runtime daemon that can crash and take all your workloads down with it.
This guide covers everything from creating your first jail by hand to managing fleets with Bastille. Every command is real. Every output block reflects what you will see on a FreeBSD 14.x system.
Table of Contents
- What Are Jails and Why They Matter
- Manual Jail Creation Step by Step
- jail.conf Walkthrough
- Starting, Stopping, and Managing Jails
- Networking: Inherited IP vs VNET
- ZFS and Jails
- Resource Limits with rctl
- Bastille Jail Manager
- Practical Example: Running NGINX in a Jail
- Updating and Maintaining Jails
- Security Considerations
- FAQ
What Are Jails and Why They Matter
A FreeBSD jail is an isolated environment that shares the host kernel but has its own:
- Root filesystem. Each jail has a complete directory tree. Processes inside the jail cannot see or access files outside it.
- Process space. A
ps auxinside a jail only shows that jail's processes. The host and other jails are invisible. - Network identity. A jail can have its own IP address or, with VNET, its own full network stack including routing tables and firewall rules.
- Users and groups. Root inside a jail is not root on the host. Privilege escalation within a jail does not grant host access.
Jails vs Docker
Docker containers on Linux depend on a layered stack of kernel features -- namespaces, cgroups, seccomp, AppArmor or SELinux -- bolted together by a userland daemon. The isolation boundary is complex and the attack surface is wide. Docker requires a daemon running as root. If that daemon crashes, every container goes down.
Jails are a single, audited kernel subsystem. There is no daemon. The jail boundary is enforced directly by the FreeBSD kernel, has been reviewed by security researchers for over two decades, and has a track record of very few privilege-escalation vulnerabilities.
For a detailed comparison, see our FreeBSD jails vs Docker analysis.
A Brief History
- 2000 -- FreeBSD 4.0 introduces jails (Poul-Henning Kamp's design).
- 2008 -- Hierarchical jails land in FreeBSD 8.0, allowing jails inside jails.
- 2012 -- VNET (virtual network stack) becomes stable, giving each jail its own full networking.
- 2014 --
jail.confreplaces the old rc.conf-based configuration. - 2020+ -- Integration with ZFS, RCTL resource limits, and modern jail managers like Bastille matures.
Manual Jail Creation Step by Step
We will build a jail from scratch on a FreeBSD 14.2-RELEASE host. This teaches you what every jail manager does under the hood.
Step 1: Create the Jail Directory
Pick a location for your jails. /usr/local/jails is a common convention:
shmkdir -p /usr/local/jails/containers/webserver
Step 2: Fetch the FreeBSD Base System
Download and extract the base system into the jail directory:
shfetch https://download.FreeBSD.org/releases/amd64/14.2-RELEASE/base.txz tar -xf base.txz -C /usr/local/jails/containers/webserver
This gives you a minimal FreeBSD userland -- around 350 MB -- with all the standard tools, libraries, and configuration files.
Step 3: Configure DNS Resolution
Copy the host's DNS configuration into the jail:
shcp /etc/resolv.conf /usr/local/jails/containers/webserver/etc/resolv.conf
Step 4: Set the Jail's Root Password
shchroot /usr/local/jails/containers/webserver passwd root
Step 5: Create the jail.conf Entry
Edit /etc/jail.conf:
confwebserver { host.hostname = "webserver.jail"; ip4.addr = "192.168.1.50"; interface = "em0"; path = "/usr/local/jails/containers/webserver"; exec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown"; mount.devfs; allow.raw_sockets; }
Step 6: Enable and Start
Add to /etc/rc.conf:
shjail_enable="YES" jail_list="webserver"
Start the jail:
shservice jail start webserver
You now have a running, isolated FreeBSD environment.
jail.conf Walkthrough
The /etc/jail.conf file is where all jail configuration lives. Understanding its parameters is essential. Here is a comprehensive example with every commonly used option:
conf# Global defaults applied to all jails exec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown"; exec.clean; mount.devfs; allow.raw_sockets; exec.consolelog = "/var/log/jail_${name}_console.log"; webserver { # Identity host.hostname = "webserver.jail"; path = "/usr/local/jails/containers/webserver"; osrelease = "FreeBSD 14.2-RELEASE"; # Networking (inherited IP mode) ip4.addr = "em0|192.168.1.50/24"; ip6 = "disable"; # Permissions allow.set_hostname = 0; allow.sysvipc = 0; allow.raw_sockets = 1; allow.chflags = 0; allow.mount = 0; allow.mount.devfs = 0; allow.mount.nullfs = 0; allow.mount.procfs = 0; allow.mount.zfs = 0; allow.quotas = 0; allow.socket_af = 0; # Execution hooks exec.prestart = ""; exec.poststart = ""; exec.prestop = ""; exec.poststop = ""; # Mount points mount.devfs; mount.fstab = "/etc/fstab.webserver"; # Boot behavior exec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown jail"; # Security securelevel = 2; devfs_ruleset = 4; enforce_statfs = 2; children.max = 0; persist; }
Key Parameters Explained
path -- The root filesystem of the jail. Every file access inside the jail is relative to this directory.
host.hostname -- The hostname visible inside the jail. Does not need to resolve in DNS.
ip4.addr -- The IPv4 address assigned to the jail. The format interface|address/mask binds the IP as an alias on the given interface. With VNET jails, this parameter is not used (you configure networking inside the jail instead).
exec.start / exec.stop -- Commands run inside the jail when it starts and stops. The defaults invoke /etc/rc to start services defined in the jail's own /etc/rc.conf.
mount.devfs -- Mounts a devfs inside the jail so that /dev is populated. Without this, most software will not function.
devfs_ruleset -- Controls which devices are visible inside the jail. Ruleset 4 is the default restricted set. Never use ruleset 0 (unrestricted) in production.
securelevel -- Sets the kernel securelevel inside the jail. Level 2 prevents modification of system flags and direct disk writes.
enforce_statfs -- Controls what mount point information is visible. Value 2 (the default) hides all mount points outside the jail.
children.max -- Maximum number of child jails. Set to 0 to prevent nested jails.
allow.raw_sockets -- Permits raw socket access, needed for ping and traceroute inside the jail.
persist -- Keeps the jail running even if all processes inside it exit.
Starting, Stopping, and Managing Jails
Basic Operations
sh# Start all jails in jail_list service jail start # Start a specific jail service jail start webserver # Stop a specific jail service jail stop webserver # Restart a jail service jail restart webserver
Listing Running Jails
shjls
Output:
shellJID IP Address Hostname Path 1 192.168.1.50 webserver.jail /usr/local/jails/containers/webserver 2 192.168.1.51 database.jail /usr/local/jails/containers/database
For detailed information:
shjls -v
Or query specific fields:
shjls jid name host.hostname ip4.addr path
Executing Commands Inside a Jail
sh# Open a shell inside jail ID 1 jexec 1 /bin/sh # Or use the jail name jexec webserver /bin/sh # Run a single command jexec webserver pkg info
Viewing Jail Processes from the Host
shps -aux -J 1
This shows all processes belonging to jail ID 1, with the jail ID column visible.
Console Logging
With exec.consolelog set in jail.conf, all console output during jail start and stop is logged:
shtail -f /var/log/jail_webserver_console.log
Networking: Inherited IP vs VNET
FreeBSD jails support two networking models. Choosing correctly is critical.
Inherited IP (Simple Mode)
In inherited IP mode, the jail borrows an IP address from the host's network stack. The jail shares the host's routing table and firewall. This is the simplest setup and works for most single-service jails.
confwebserver { ip4.addr = "em0|192.168.1.50/24"; # ... }
The host adds 192.168.1.50 as an alias on em0. The jail can only bind to that address. It cannot modify routes, cannot run its own firewall, and cannot see other interfaces.
Pros: Simple. No extra configuration. Low overhead.
Cons: No per-jail firewall. No per-jail routing. All jails share the host's network stack.
VNET (Full Network Stack)
VNET gives each jail its own complete network stack -- interfaces, routing table, ARP table, and firewall. This is necessary when jails need to run DHCP clients, VPN software, or their own PF rules.
VNET requires a bridge and an epair (virtual ethernet pair).
Step 1: Create the Bridge on the Host
sh# /etc/rc.conf on the host cloned_interfaces="bridge0" ifconfig_bridge0="inet 192.168.100.1/24 up"
Step 2: Configure jail.conf for VNET
confwebserver { host.hostname = "webserver.jail"; path = "/usr/local/jails/containers/webserver"; vnet; vnet.interface = "epair0b"; exec.prestart = "ifconfig epair0 create up"; exec.prestart += "ifconfig bridge0 addm epair0a"; 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; devfs_ruleset = 4; }
Step 3: Configure Networking Inside the Jail
In the jail's /etc/rc.conf:
shifconfig_epair0b="inet 192.168.100.10/24" defaultrouter="192.168.100.1"
How It Works
The epair device creates a virtual ethernet cable with two ends: epair0a and epair0b. The a end stays on the host and is added to the bridge. The b end is passed into the jail via vnet.interface. The jail sees epair0b as its network interface and configures it normally.
This gives the jail full control over its own networking. It can run PF, set up routes, use ifconfig, and even run a DHCP client.
NAT for VNET Jails
If your VNET jails need internet access through the host, enable NAT with PF on the host:
conf# /etc/pf.conf on the host ext_if = "em0" jail_net = "192.168.100.0/24" nat on $ext_if from $jail_net to any -> ($ext_if) pass from $jail_net to any
Enable IP forwarding:
shsysctl net.inet.ip.forwarding=1
Make it persistent in /etc/sysctl.conf:
shellnet.inet.ip.forwarding=1
ZFS and Jails
ZFS and jails are a natural combination. ZFS datasets give each jail its own filesystem with independent snapshots, quotas, and compression settings. For a deep dive into ZFS itself, see our ZFS guide.
One Dataset Per Jail
shzfs create zpool/jails zfs create zpool/jails/webserver
Set the jail's path to the dataset mount point:
confwebserver { path = "/zpool/jails/webserver"; # ... }
Now you can snapshot, clone, and send/receive entire jails:
sh# Snapshot a jail zfs snapshot zpool/jails/webserver@2026-03-29 # Roll back to a snapshot zfs rollback zpool/jails/webserver@2026-03-29 # Send a jail to another machine zfs send zpool/jails/webserver@2026-03-29 | ssh backup-host zfs receive backup/jails/webserver
Thin Jails with ZFS Clones
Instead of extracting a full base.txz for every jail (350 MB each), create one base dataset and clone it:
sh# Create and populate the base zfs create zpool/jails/base tar -xf base.txz -C /zpool/jails/base # Snapshot the base zfs snapshot zpool/jails/base@14.2-RELEASE # Clone for each jail -- near-instant, near-zero disk usage zfs clone zpool/jails/base@14.2-RELEASE zpool/jails/webserver zfs clone zpool/jails/base@14.2-RELEASE zpool/jails/database zfs clone zpool/jails/base@14.2-RELEASE zpool/jails/mailserver
Each clone starts as a zero-byte copy-on-write reference to the base snapshot. Disk usage only grows as files inside the jail diverge from the base. With 20 jails, you might use 350 MB for the base plus a few MB per jail instead of 7 GB.
Quotas
Limit how much disk space a jail can consume:
shzfs set quota=10G zpool/jails/webserver zfs set reservation=2G zpool/jails/webserver
Compression
Enable compression per jail dataset:
shzfs set compression=zstd zpool/jails/webserver
Resource Limits with rctl
FreeBSD's rctl framework lets you cap CPU, memory, and other resources per jail. This prevents a runaway process in one jail from starving the host or other jails.
Enable rctl
Add to /boot/loader.conf:
shellkern.racct.enable=1
Reboot. Then verify:
shsysctl kern.racct.enable
shellkern.racct.enable: 1
Set Resource Limits
sh# Limit jail to 2 GB of RAM rctl -a jail:webserver:memoryuse:deny=2G # Limit to 50% of one CPU core rctl -a jail:webserver:pcpu:deny=50 # Limit to 200 processes rctl -a jail:webserver:maxproc:deny=200 # Limit open files rctl -a jail:webserver:openfiles:deny=4096
View Current Limits
shrctl -l jail:webserver
shelljail:webserver:memoryuse:deny=2147483648 jail:webserver:pcpu:deny=50 jail:webserver:maxproc:deny=200 jail:webserver:openfiles:deny=4096
View Current Usage
shrctl -u jail:webserver
shellcputime=312 datasize=847249408 stacksize=8388608 coredumpsize=0 memoryuse=524288000 memorylocked=0 maxproc=47 openfiles=213 vmemoryuse=1249902592 pseudoterminals=3 swapuse=0 nthr=98 msgqqueued=0 msgqsize=0 nmsgq=0 nsem=0 nsemop=0 nshm=0 shmsize=0 wallclock=28847 pcpu=8 readbps=0 writebps=0 readiops=0 writeiops=0
Make Limits Persistent
Add rules to /etc/rctl.conf so they survive reboots:
shelljail:webserver:memoryuse:deny=2G jail:webserver:pcpu:deny=50 jail:webserver:maxproc:deny=200 jail:webserver:openfiles:deny=4096
Bastille Jail Manager
While manual jail creation teaches fundamentals, managing dozens of jails by hand is tedious. Bastille is the leading modern jail manager for FreeBSD. It automates creation, networking, templates, and lifecycle management.
Install Bastille
shpkg install bastille
Enable it:
shsysrc bastille_enable="YES"
Bootstrap a Release
Before creating jails, download a FreeBSD release:
shbastille bootstrap 14.2-RELEASE update
This fetches and extracts the base system and applies the latest security patches. Bastille stores releases in /usr/local/bastille/releases/.
Create a Jail
shbastille create webserver 14.2-RELEASE 192.168.1.50
That single command does everything we did manually: creates the directory structure, extracts the base, configures the IP address, and generates the jail.conf entry.
For a VNET jail:
shbastille create -V webserver 14.2-RELEASE em0
Basic Bastille Operations
sh# List all jails bastille list # Start a jail bastille start webserver # Stop a jail bastille stop webserver # Open a console bastille console webserver # Run a command in a jail bastille cmd webserver pkg update # Install a package bastille pkg webserver install nginx # View jail logs bastille logs webserver # Destroy a jail bastille destroy webserver
Bastille Templates
Templates are Bastille's automation layer. They define packages, services, and configuration files to apply to a jail. A template is a directory with a Bastillefile:
shell# Bastillefile for a web server jail PKG nginx PKG php84 PKG php84-extensions SYSRC nginx_enable=YES SYSRC php_fpm_enable=YES SERVICE nginx start SERVICE php-fpm start CP usr/local/etc/nginx/nginx.conf TEMPLATE usr/local/etc/nginx/vhosts/default.conf
Apply a template:
shbastille template webserver myuser/webserver-template
Bastille templates can be hosted as Git repositories and applied directly from URLs, making jail provisioning reproducible and version-controlled.
Bastille with ZFS
Bastille detects ZFS automatically. If your jail storage sits on a ZFS dataset, Bastille creates a child dataset per jail and uses ZFS clones for fast provisioning:
shsysrc -f /usr/local/etc/bastille/bastille.conf bastille_zfs_enable="YES" sysrc -f /usr/local/etc/bastille/bastille.conf bastille_zfs_zpool="zpool"
Practical Example: Running NGINX in a Jail
Let us put everything together and deploy an NGINX web server in a jail with VNET networking and ZFS storage.
Create the Jail Infrastructure
sh# Create ZFS datasets zfs create zpool/jails zfs create zpool/jails/nginx-web # Fetch and extract the base system fetch https://download.FreeBSD.org/releases/amd64/14.2-RELEASE/base.txz tar -xf base.txz -C /zpool/jails/nginx-web # Copy DNS config cp /etc/resolv.conf /zpool/jails/nginx-web/etc/resolv.conf
Configure the Jail
/etc/jail.conf:
confexec.start = "/bin/sh /etc/rc"; exec.stop = "/bin/sh /etc/rc.shutdown jail"; exec.clean; mount.devfs; devfs_ruleset = 4; nginx-web { host.hostname = "nginx-web.jail"; path = "/zpool/jails/nginx-web"; vnet; vnet.interface = "epair1b"; exec.prestart = "ifconfig epair1 create up"; exec.prestart += "ifconfig bridge0 addm epair1a"; exec.poststop = "ifconfig bridge0 deletem epair1a"; exec.poststop += "ifconfig epair1a destroy"; }
Configure Networking Inside the Jail
Write to /zpool/jails/nginx-web/etc/rc.conf:
shifconfig_epair1b="inet 192.168.100.20/24" defaultrouter="192.168.100.1"
Start the Jail and Install NGINX
sh# Enable and start sysrc jail_enable="YES" sysrc jail_list+="nginx-web" service jail start nginx-web # Enter the jail jexec nginx-web /bin/sh # Inside the jail: pkg install -y nginx sysrc nginx_enable="YES" service nginx start
Verify
From the host:
shcurl http://192.168.100.20
You should see the default NGINX welcome page.
From outside the network, set up port forwarding with PF on the host:
conf# /etc/pf.conf ext_if = "em0" rdr on $ext_if proto tcp from any to ($ext_if) port 80 -> 192.168.100.20 port 80 rdr on $ext_if proto tcp from any to ($ext_if) port 443 -> 192.168.100.20 port 443
For a production NGINX configuration inside the jail, see our NGINX setup guide.
Set Resource Limits
shrctl -a jail:nginx-web:memoryuse:deny=1G rctl -a jail:nginx-web:maxproc:deny=100 rctl -a jail:nginx-web:openfiles:deny=8192
Snapshot the Working State
shzfs snapshot zpool/jails/nginx-web@working-nginx
You now have a fully isolated, resource-limited, snapshotable web server.
Updating and Maintaining Jails
Update the Base System in a Jail
Use freebsd-update inside the jail:
shjexec webserver freebsd-update fetch install
Or from the host, targeting the jail's filesystem:
shfreebsd-update -b /usr/local/jails/containers/webserver fetch install
Update Packages in a Jail
shjexec webserver pkg update jexec webserver pkg upgrade -y
Bulk Updates with Bastille
sh# Update all jails at once bastille cmd ALL pkg update bastille cmd ALL pkg upgrade -y # Apply freebsd-update to all jails bastille cmd ALL freebsd-update fetch install
Upgrading Jails to a New FreeBSD Release
When the host is upgraded (e.g., from 14.1 to 14.2), the jails need to follow:
shfreebsd-update -b /usr/local/jails/containers/webserver -r 14.2-RELEASE upgrade freebsd-update -b /usr/local/jails/containers/webserver install
Always snapshot before upgrading:
shzfs snapshot zpool/jails/webserver@pre-upgrade-14.2
If anything breaks, roll back in seconds:
shservice jail stop webserver zfs rollback zpool/jails/webserver@pre-upgrade-14.2 service jail start webserver
Security Considerations
Jails are a strong isolation boundary, but they are not magic. Follow these practices to maximize security.
Minimize the Jail's Contents
Do not copy the full base system if you only need a few binaries. Strip down the jail to the minimum required files. Fewer files means fewer potential vulnerabilities.
Use devfs Rulesets
Never use devfs_ruleset = 0 (unrestricted). The default ruleset 4 hides most devices. Create custom rulesets for jails that need specific devices:
sh# /etc/devfs.rules [devfsrules_jail_custom=10] add include $devfsrules_hide_all add include $devfsrules_unhide_basic add include $devfsrules_unhide_login add path 'bpf*' unhide
Then reference it in jail.conf:
confwebserver { devfs_ruleset = 10; }
Restrict Permissions Aggressively
Start with the most restrictive settings and open only what is needed:
confallow.set_hostname = 0; allow.sysvipc = 0; allow.raw_sockets = 0; allow.chflags = 0; allow.mount = 0; allow.quotas = 0; allow.socket_af = 0; children.max = 0; enforce_statfs = 2;
Only enable allow.raw_sockets if the jail actually needs ping. Only enable allow.mount if the jail needs to mount filesystems.
Set a Securelevel
confsecurelevel = 2;
Securelevel 2 prevents the jail from modifying system flags on files and from writing directly to mounted disk devices.
Use rctl to Prevent Resource Abuse
Without resource limits, a compromised jail can fork-bomb the host or exhaust all available memory. Always set maxproc, memoryuse, and openfiles limits.
Run Firewalls on the Host
Even with VNET jails that can run their own PF rules, maintain a host-level firewall as the outer defense layer. Defense in depth. See our FreeBSD hardening guide for a complete PF configuration.
Keep Jails Updated
An unpatched jail is an unpatched attack surface. Automate freebsd-update and pkg upgrade for all jails. Use cron jobs or Bastille's bulk command feature.
Audit Jail Access
Log all jexec sessions. Monitor jail console logs. Consider enabling FreeBSD's audit framework (auditd) on the host to track process creation and file access across jail boundaries.
FAQ
How many jails can I run on one server?
There is no hard limit built into FreeBSD. The practical limit depends on your hardware. Each jail consumes memory for its running processes and disk space for its filesystem. Lightweight jails (DNS resolver, cron runner) might use 30-50 MB of RAM. A jail running PostgreSQL might use several gigabytes. Servers with 64 GB of RAM routinely run 50-100 jails. With ZFS thin provisioning, disk overhead per jail is minimal.
Can I run Docker inside a FreeBSD jail?
No. Docker requires the Linux kernel. FreeBSD jails run on the FreeBSD kernel. However, you can run Linux containers inside a FreeBSD bhyve virtual machine, or use the Linux compatibility layer (linuxulator) for individual Linux binaries. For containerization on FreeBSD, jails are the native and superior solution.
How do jails compare to bhyve virtual machines?
Jails share the host kernel and have near-zero overhead. They start in under a second. Bhyve is a full hypervisor that runs separate kernels -- it can run Linux, Windows, or other FreeBSD versions, but each VM consumes dedicated CPU and memory and takes longer to boot. Use jails when you are running FreeBSD workloads that need isolation. Use bhyve when you need a different operating system or full kernel-level isolation.
Can jails communicate with each other?
Yes. If jails are on the same network (via shared bridge in VNET mode or shared host interface in inherited IP mode), they can communicate over TCP/IP like any networked hosts. You can also use nullfs mounts to share specific directories between jails and the host. For database-backed web applications, it is common to run the web server and database in separate jails that communicate over a private bridge network.
How do I back up a jail?
The best approach depends on your storage setup. With ZFS, use zfs snapshot and zfs send:
shzfs snapshot zpool/jails/webserver@backup-2026-03-29 zfs send zpool/jails/webserver@backup-2026-03-29 | gzip > /backups/webserver-2026-03-29.zfs.gz
Without ZFS, use tar:
shservice jail stop webserver tar -czf /backups/webserver-2026-03-29.tar.gz -C /usr/local/jails/containers/webserver . service jail start webserver
ZFS snapshots are atomic and can be taken while the jail is running.
Is it safe to allow allow.raw_sockets in a jail?
Enabling raw sockets allows tools like ping, traceroute, and packet capture inside the jail. The security risk is that a compromised jail could craft arbitrary packets. For most production jails, this risk is acceptable -- the jail is still confined to its assigned IP address. If the jail does not need network diagnostic tools, leave it disabled.
How do I give a jail access to a specific host directory?
Use a nullfs mount. Add an entry to the jail's fstab file:
/etc/fstab.webserver:
shell/data/shared /usr/local/jails/containers/webserver/mnt/shared nullfs ro 0 0
And in jail.conf:
confwebserver { mount.fstab = "/etc/fstab.webserver"; # ... }
Use ro (read-only) unless the jail genuinely needs write access. This follows the principle of least privilege.
Summary
FreeBSD jails give you production-grade, kernel-enforced process isolation with none of the complexity of Linux container runtimes. Manual creation takes five minutes and teaches you exactly what happens. ZFS integration gives you instant clones, snapshots, and send/receive for migration. VNET gives jails full network independence. Bastille automates the lifecycle.
Start with one jail isolating a service you are already running on bare metal. Move NGINX, PostgreSQL, or a DNS resolver into its own jail. Once you see how clean the separation is and how little overhead it adds, you will never run services directly on the host again.
For the next steps in securing your FreeBSD infrastructure, read our server hardening checklist. For storage architecture, see the ZFS guide.