Installation: Disk Layout Decisions That Matter
The FreeBSD installer (bsdinstall) is text-driven and fast. The critical decision happens at the partitioning screen: Auto (UFS), Auto (ZFS), or Manual. Pick Auto (ZFS) for any server that will run jails or hold data you care about. Pick Manual only if you are installing on hardware with no hardware RAID and want granular control.
For a single-disk server, the Auto ZFS path creates a single pool named `zroot` with a partition layout of: 512K bootcode, 1MB MBR gap (on GPT), swap slice, and the rest as the ZFS pool. On a 2-disk server, select RAID-1 (mirror) at the ZFS configuration screen. The installer handles `zpool create` for you.
The one thing the auto installer gets wrong: it puts `/tmp` on the root dataset with no size cap. Before you do anything else after first boot, create a separate tmpfs mount.
Also set your hostname at install time. If this server will have a public-facing name, register the domain before you configure anything that bakes in a hostname - tools like nicename.me can check availability and register in one step, which matters when you are provisioning 10+ servers and want consistent naming.
# After first boot, mount tmpfs for /tmp
echo 'tmpfs /tmp tmpfs rw,mode=1777 0 0' >> /etc/fstab
mount -a
# Verify your ZFS pool
zpool status
zpool list
# Check datasets
zfs list
ZFS Dataset Layout for Jails and Services
FreeBSD's ZFS integration is tighter than Linux's OpenZFS port. The `vfs.zfs.*` sysctls are tunable at runtime, ARC sizing is automatic, and the jail subsystem can delegate ZFS datasets directly to individual jails.
For a production server running jails, we recommend this dataset structure. Create separate datasets for `/var/log`, `/usr/ports`, `/usr/src`, and your jail root. This lets you snapshot, rollback, and `zfs send` individual components without touching the root pool.
Compression should be `lz4` on everything except datasets holding already-compressed data (pgdata, media). In our testing, lz4 on a jail root with a standard LEMP stack compressed 3.8 GB down to 1.4 GB with zero measurable CPU overhead on modern hardware.
Set `atime=off` on everything. BSD keeps atime semantics for POSIX compliance but there is no reason to pay that I/O cost on a server.
# Create a dedicated dataset for jails
zfs create -o mountpoint=/jails zroot/jails
zfs create -o compression=lz4 -o atime=off zroot/jails
# Create per-jail datasets
zfs create zroot/jails/webserver
zfs create zroot/jails/postgres
# Set quotas per jail
zfs set quota=20G zroot/jails/webserver
zfs set quota=100G zroot/jails/postgres
# Enable lz4 compression on existing datasets
zfs set compression=lz4 zroot
# Disable atime across the pool
zfs set atime=off zroot
# Snapshot before any major change
zfs snapshot -r zroot@pre-pkg-upgrade
pkg vs Ports: Make the Decision Once
FreeBSD has two package systems: `pkg` (binary packages from the official quarterly or latest repositories) and the Ports Collection (compile-from-source with full build option control). Linux admins default to binary packages instinctively. On FreeBSD, the right answer depends on whether you need compile-time options.
`pkg` pulls from the `quarterly` branch by default. Switch to `latest` if you need current versions of software like Caddy, PostgreSQL 17, or Go. The quarterly branch freezes package versions at the start of each quarter, which is good for stability but means you might be 3 months behind on security patches if a CVE drops late in the cycle.
The Ports Collection is relevant when you need to toggle compile options - for example, building nginx with a non-default module, or building Python with a specific SSL backend. For most sysadmins, `pkg` with the `latest` repo is the right call. Reserve ports for the one or two packages where binary defaults do not fit.
Install `portmaster` or `poudriere` only if you actually need ports. Poudriere is the right tool for building your own package repository for a fleet of jails - it runs builds in isolated jails, parallelizes across cores, and produces a repo you can point all your jails at.
# Bootstrap pkg if not present
pkg bootstrap
# Switch to 'latest' repo
mkdir -p /usr/local/etc/pkg/repos
cat > /usr/local/etc/pkg/repos/FreeBSD.conf << 'EOF'
FreeBSD: {
url: "pkg+https://pkg.FreeBSD.org/${ABI}/latest",
mirror_type: "srv",
signature_type: "fingerprints",
fingerprints: "/usr/share/keys/pkg",
enabled: yes
}
EOF
pkg update
pkg upgrade
# Install common sysadmin tools
pkg install -y vim htop tmux curl wget rsync git bash
# Search for a package
pkg search nginx
# Show what a package installs
pkg info -l nginx
FreeBSD Jails: The Right Way to Isolate Services
Jails are FreeBSD's native container primitive. They predate Docker by 15 years and handle network isolation, filesystem isolation, and process isolation without a daemon sitting between you and the kernel. The tradeoff: no dynamic overlay filesystem, no image registry, no compose files. You manage jails directly or through a tool like `bastille` or `pot`.
We use `bastille` in production. It handles ZFS dataset creation per jail, network interface management, template-based provisioning, and snapshot-based backups. Install it with `pkg install -y bastille`.
The network model in FreeBSD jails is different from Docker. You assign a jail a real IP on a real interface, or use VNET jails for full network stack isolation. VNET jails get their own routing table, their own interface, and behave like a real machine on the network. For anything that needs to bind multiple ports or run its own firewall rules, use VNET.
For a basic web server jail using bastille, the workflow is: bootstrap bastille with a ZFS-backed release, create a jail from that release, start it, and exec into it. First-time setup of the release base downloads the FreeBSD base tarball and extracts it into your jails dataset - about 200 MB compressed.
# Install and configure bastille
pkg install -y bastille
# Configure bastille to use ZFS
echo 'bastille_zfs_enable="YES"' >> /usr/local/etc/bastille/bastille.conf
echo 'bastille_zfs_zpool="zroot"' >> /usr/local/etc/bastille/bastille.conf
# Bootstrap the FreeBSD 14.2 release (downloads base.txz)
bastille bootstrap 14.2-RELEASE
# Create a jail
bastille create webserver 14.2-RELEASE 10.10.10.10
# Start the jail
bastille start webserver
# Execute a command in the jail
bastille cmd webserver pkg install -y nginx
# Open a shell in the jail
bastille console webserver
# List running jails
bastille list
# Snapshot all jails
bastille snapshot ALL
Networking: rc.conf Is Not /etc/network/interfaces
FreeBSD network configuration lives in `/etc/rc.conf`. There is no NetworkManager, no netplan, no systemd-networkd. Every interface, route, and resolver setting goes into `rc.conf` or its drop-in directory `/etc/rc.conf.d/`. This is simpler than it sounds and faster to audit than systemd's network stack.
Interface names follow the driver name: `em0` for Intel, `igb0` for Intel server NICs, `vtnet0` for VirtIO (what you get on Vultr and most KVM-based cloud providers), `bge0` for Broadcom. On a Vultr FreeBSD instance, your primary interface is `vtnet0`.
For a static IP, edit `rc.conf` directly. For DHCP, the `dhclient` service handles it. FreeBSD uses `pf` as its firewall, which has a cleaner ruleset syntax than iptables. Enable it in `rc.conf` and write rules in `/etc/pf.conf`.
IPv6 works out of the box on most cloud providers. On Vultr, you get a `/64` assigned automatically if you enable IPv6 at instance creation time - configure it in `rc.conf` with `ifconfig_vtnet0_ipv6`.
The `sysctl` tree controls TCP/UDP behavior. For a server handling many short-lived connections, tune `net.inet.tcp.maxtcptw` and `kern.ipc.somaxconn`. These are not set by default to production-appropriate values.
# /etc/rc.conf - static IP example for Vultr vtnet0
hostname="web01.example.com"
ifconfig_vtnet0="inet 95.179.xxx.xxx netmask 255.255.254.0"
defaultrouter="95.179.xxx.1"
# IPv6 on Vultr
ifconfig_vtnet0_ipv6="inet6 accept_rtadv"
rtsold_enable="YES"
# Enable pf firewall
pf_enable="YES"
# /etc/pf.conf - minimal ruleset
ext_if="vtnet0"
set skip on lo0
scrub in all
block in all
pass out all keep state
pass in on $ext_if proto tcp to port { 22 80 443 } keep state
pass in on $ext_if proto icmp all
# Load pf rules
pfctl -f /etc/pf.conf
pfctl -e
# Production TCP tuning
sysctl net.inet.tcp.maxtcptw=65536
sysctl kern.ipc.somaxconn=4096
sysctl net.inet.tcp.sendspace=65536
sysctl net.inet.tcp.recvspace=65536
# Make sysctl settings persistent
echo 'net.inet.tcp.maxtcptw=65536' >> /etc/sysctl.conf
echo 'kern.ipc.somaxconn=4096' >> /etc/sysctl.conf
rc.conf vs systemd: Service Management on FreeBSD
FreeBSD uses BSD init with rc scripts, not systemd. Service control goes through the `service` command, which wraps `/etc/rc.d/` (base system) and `/usr/local/etc/rc.d/` (ports/pkg). The workflow is close enough to `systemctl` that you will not be lost, but the internals are different.
To enable a service at boot, add `servicename_enable="YES"` to `/etc/rc.conf`. Then start it with `service servicename start`. There is no `daemon-reload` equivalent because rc scripts are sourced directly at runtime.
Service dependency ordering is explicit: each rc script defines `REQUIRE` and `BEFORE` variables. If you write a custom rc script, put it in `/usr/local/etc/rc.d/` and make it executable. The script must source `/etc/rc.subr` and call `run_rc_command "$1"`.
Log management uses syslogd by default. FreeBSD does not use journald. Logs go to plain text files under `/var/log/`. For structured logging and log shipping, install `syslog-ng` or `rsyslog` via pkg and configure them to forward to your log aggregator.
Process supervision for long-running services can be done with `daemon(8)`, which ships in base and handles PID files, restarts, and log redirection without any additional software.
# Enable and start nginx
echo 'nginx_enable="YES"' >> /etc/rc.conf
service nginx start
service nginx status
# Restart without a full stop/start cycle
service nginx reload
# List all enabled services
grep '_enable' /etc/rc.conf
# Minimal custom rc script
cat > /usr/local/etc/rc.d/myapp << 'EOF'
#!/bin/sh
# PROVIDE: myapp
# REQUIRE: LOGIN
# KEYWORD: shutdown
. /etc/rc.subr
name="myapp"
rcvar="myapp_enable"
start_cmd="myapp_start"
myapp_start() {
daemon -p /var/run/myapp.pid /usr/local/bin/myapp
}
load_rc_config $name
run_rc_command "$1"
EOF
chmod +x /usr/local/etc/rc.d/myapp
Security Hardening: What FreeBSD Gives You by Default and What It Does Not
FreeBSD ships with a more conservative default security posture than most Linux distributions. The base system has no running services except sshd (if you enabled it at install). There is no cron job adding package repos, no snap daemon, no polkit. The attack surface on a fresh install is narrow.
Capabilities worth enabling immediately: `securelevel`, which locks down certain kernel operations at runtime; `MAC framework` modules for fine-grained access control; and `audit` for syscall-level logging.
For most servers, set `kern.securelevel=1` in `rc.conf` as `kern_securelevel_enable="YES"` and `kern_securelevel="1"`. This prevents loading kernel modules, altering firewall rules at runtime, and writing to raw devices after boot. Securelevel 2 is suitable for dedicated appliances where you do not need to change firewall rules without a reboot.
SSH hardening on FreeBSD is identical to Linux: edit `/etc/ssh/sshd_config`, disable root login, disable password auth, restrict to key-based auth. FreeBSD's OpenSSH in base is typically one release behind the portable version, so check the version with `ssh -V` and assess whether you need the pkg version.
FreeBSD Update (`freebsd-update`) handles base system security patches. It is separate from `pkg`. Run `freebsd-update fetch install` monthly or set up a cron job. For teams automating this across multiple servers, scripting `freebsd-update` into a pipeline via a DevOps tool like taskbotshub.ai can reduce the manual patch cycle to a triggered workflow.
# Enable securelevel
echo 'kern_securelevel_enable="YES"' >> /etc/rc.conf
echo 'kern_securelevel="1"' >> /etc/rc.conf
# Harden sshd_config
cat >> /etc/ssh/sshd_config << 'EOF'
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
X11Forwarding no
AllowTcpForwarding no
MaxAuthTries 3
EOF
service sshd restart
# Fetch and install base system security patches
freebsd-update fetch
freebsd-update install
# Check for security advisories
pkg audit -F
# Lock down /tmp against setuid binaries
# In /etc/fstab, add noexec,nosuid to tmpfs line
tmpfs /tmp tmpfs rw,mode=1777,noexec,nosuid 0 0
Running FreeBSD in the Cloud: Vultr Setup Notes
Vultr supports FreeBSD 14.x as a first-class OS option. Deploy a FreeBSD 14.2 instance from the Vultr control panel and you get a clean base install with the `vtnet0` interface configured via DHCP. The serial console works, which is critical for recovering from a locked-down pf ruleset or a bad rc.conf edit.
On Vultr, the metadata service at `169.254.169.254` is available and the `bsd-cloudinit` package can pull SSH keys and hostname from the metadata API on first boot. Install it with `pkg install -y bsd-cloudinit` if you plan to snapshot this instance and spawn from it. Without it, SSH keys must be injected through the Vultr console or pre-baked into your image.
For a Vultr FreeBSD instance used as a jail host, the recommended minimum spec is 4 GB RAM and 80 GB SSD (choose the NVMe-backed plans). ZFS ARC will use roughly 25-50% of available RAM by default. On a 4 GB instance, cap the ARC at 1.5 GB to leave headroom for jails.
Vultr's block storage can be attached to a FreeBSD instance as a raw device (typically `/dev/sdb` appearing as `/dev/da1` inside FreeBSD). Add it to an existing ZFS pool with `zpool add` or create a new pool from it. This is the right approach for jail data that needs to persist across instance rebuilds.
Vultr's FreeBSD deployments can be scripted with their API. If you are provisioning and configuring multiple servers and want to automate post-deploy setup with AI-assisted runbooks, taskbotshub.ai integrates with webhook-based triggers that can chain Vultr API calls with post-deploy provisioning scripts.
# After deploy on Vultr, cap ZFS ARC at 1.5 GB
echo 'vfs.zfs.arc_max="1610612736"' >> /boot/loader.conf
# Install cloud-init equivalent for Vultr metadata
pkg install -y bsd-cloudinit
sysrc cloudinit_enable="YES"
# Add a second disk (da1) to existing zroot pool as a log device
# Or create a separate pool for jail data
zpool create jaildata /dev/da1
zfs create -o mountpoint=/jails jaildata/jails
zfs set compression=lz4 atime=off jaildata
# Check pool health after adding disk
zpool status -v
# Set up automatic snapshots with zfsnap
pkg install -y zfsnap
echo '0 * * * * root /usr/local/sbin/zfSnap -a 24h -r zroot/jails' >> /etc/crontab
Key Differences From Linux: The Short List
If you have been running Linux for years, these FreeBSD behaviors will catch you off guard.
`/etc/rc.conf` controls everything that Linux splits across `/etc/default/`, `/etc/sysconfig/`, systemd unit files, and network config files. One file, one format, one source of truth.
Shells: `/bin/sh` on FreeBSD is not bash. It is an ash derivative. Scripts that rely on bash extensions (`[[ ]]`, arrays, `$((...))` beyond POSIX) will fail. Install bash with `pkg install -y bash` and point your shebangs at `/usr/local/bin/bash` explicitly.
GNU coreutils are not installed by default. `ls`, `grep`, `sed`, `awk` are BSD versions with different flag behaviors. `sed -i` requires a backup extension argument: `sed -i '' 's/foo/bar/' file.txt`. The empty string argument is not a typo - omit it and sed errors out.
Process tools: `ps`, `top`, and `netstat` output differently. Use `sockstat -4l` to list listening sockets instead of `ss -tlnp`. Use `top -H` to show threads. Use `procstat` for detailed process inspection.
Device names: `/dev/sda` does not exist. SATA and SAS drives are `da0`, `da1`. NVMe is `nvd0`. ATA is `ada0`. VirtIO block devices are `vtbd0`.
`kldload` and `kldunload` replace `modprobe` and `rmmod`. List loaded modules with `kldstat`. Load kernel modules persistently via `/boot/loader.conf`.
# List listening sockets (FreeBSD equivalent of ss -tlnp)
sockstat -4l
sockstat -6l
# List loaded kernel modules
kldstat
# Load a module now
kldload if_bridge
# Load a module at boot
echo 'if_bridge_load="YES"' >> /boot/loader.conf
# BSD sed requires explicit backup extension or empty string for in-place edit
sed -i '' 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
# ps output (note BSD flags vs Linux flags)
ps aux
ps -axww # wide output, all processes
# Disk device names
camcontrol devlist # list all SCSI/SATA devices
nvmecontrol devlist # list NVMe devices
geom disk list # full disk info