How to Automate FreeBSD Installation with bsdinstall
Manually installing FreeBSD on one server is straightforward. Manually installing it on 10, 50, or 200 servers is a waste of time. FreeBSD's bsdinstall supports fully automated, scripted installations that require zero human interaction: boot the media, walk away, come back to a configured system.
This guide covers every aspect of automated FreeBSD installation: scripted bsdinstall with installerconfig, ZFS-on-root automation, custom install media creation, post-installation configuration, and PXE network booting for large-scale deployments.
How bsdinstall Automation Works
When FreeBSD boots from install media, the installer checks for a file named installerconfig in the root of the install media (/etc/installerconfig on the ramdisk). If found, bsdinstall executes it non-interactively instead of launching the menu-driven installer.
The installerconfig file has two sections:
- Preamble -- Environment variables that configure partitioning, network, and distribution selection.
- Scripting section -- Shell commands that run inside a
chrootof the installed system after base extraction.
The two sections are separated by a line containing only #!/bin/sh.
Basic Automated Installation
Minimal installerconfig
sh# /etc/installerconfig # Partitioning PARTITIONS=ada0 DISTRIBUTIONS="kernel.txz base.txz" #!/bin/sh # Set timezone tzsetup -s UTC # Set hostname sysrc hostname="freebsd-auto" # Enable SSH sysrc sshd_enable="YES" # Enable DHCP on first interface sysrc ifconfig_DEFAULT="DHCP" # Set root password (hash generated with: echo 'password' | openssl passwd -6 -stdin) echo '$6$rounds=5000$saltsalt$hashedpassword' | pw usermod root -H 0 # Enable NTP sysrc ntpd_enable="YES" sysrc ntpd_sync_on_start="YES" # Reboot when done reboot
This installs FreeBSD on ada0 with UFS, configures DHCP networking, enables SSH, and reboots.
ZFS-on-Root Automated Installation
ZFS is the recommended filesystem for FreeBSD servers. Automating a ZFS-on-root installation requires the ZFSBOOT_* variables.
Single-Disk ZFS
sh# /etc/installerconfig PARTITIONS=ada0 DISTRIBUTIONS="kernel.txz base.txz" export ZFSBOOT_VDEV_TYPE=stripe export ZFSBOOT_DISKS="ada0" export ZFSBOOT_POOL_NAME="zroot" export ZFSBOOT_BEROOT_NAME="ROOT" export ZFSBOOT_BOOTFS_NAME="default" export ZFSBOOT_DATASETS=" /ROOT mountpoint=none /ROOT/default mountpoint=/ /tmp mountpoint=/tmp exec=on setuid=off /usr mountpoint=/usr canmount=off /usr/home mountpoint=/usr/home /usr/ports mountpoint=/usr/ports setuid=off /usr/src mountpoint=/usr/src /var mountpoint=/var canmount=off /var/audit mountpoint=/var/audit exec=off setuid=off /var/crash mountpoint=/var/crash exec=off setuid=off /var/log mountpoint=/var/log exec=off setuid=off /var/mail mountpoint=/var/mail exec=off setuid=off /var/tmp mountpoint=/var/tmp setuid=off " export ZFSBOOT_SWAP_SIZE="4g" export ZFSBOOT_SWAP_ENCRYPTION="YES" export nonInteractive="YES" #!/bin/sh # Timezone tzsetup -s UTC # Hostname sysrc hostname="zfs-server01" # Networking sysrc ifconfig_em0="inet 192.168.1.100 netmask 255.255.255.0" sysrc defaultrouter="192.168.1.1" # DNS cat > /etc/resolv.conf << 'DNS' nameserver 1.1.1.1 nameserver 1.0.0.1 DNS # SSH sysrc sshd_enable="YES" # NTP sysrc ntpd_enable="YES" sysrc ntpd_sync_on_start="YES" # ZFS tuning cat >> /boot/loader.conf << 'LOADER' zfs_load="YES" vfs.zfs.arc_max="2147483648" LOADER sysrc zfs_enable="YES" # Set root password echo 'YourSecurePassword' | pw usermod root -h 0 # Create admin user pw useradd admin -m -G wheel -s /bin/sh echo 'AdminPassword123' | pw usermod admin -h 0 # Install pkg and basic tools env ASSUME_ALWAYS_YES=yes pkg bootstrap pkg install -y sudo bash tmux # Configure sudo for wheel group echo '%wheel ALL=(ALL:ALL) ALL' >> /usr/local/etc/sudoers reboot
Mirror (RAID-1) ZFS
For production servers, use a mirrored vdev:
shexport ZFSBOOT_VDEV_TYPE=mirror export ZFSBOOT_DISKS="ada0 ada1" export ZFSBOOT_POOL_NAME="zroot" export ZFSBOOT_SWAP_SIZE="4g" export ZFSBOOT_SWAP_ENCRYPTION="YES" export nonInteractive="YES"
RAID-Z1 ZFS (Three Disks)
shexport ZFSBOOT_VDEV_TYPE=raidz1 export ZFSBOOT_DISKS="ada0 ada1 ada2" export ZFSBOOT_POOL_NAME="zroot" export ZFSBOOT_SWAP_SIZE="4g" export nonInteractive="YES"
RAID-Z2 ZFS (Four+ Disks)
shexport ZFSBOOT_VDEV_TYPE=raidz2 export ZFSBOOT_DISKS="ada0 ada1 ada2 ada3" export ZFSBOOT_POOL_NAME="zroot" export ZFSBOOT_SWAP_SIZE="8g" export nonInteractive="YES"
Creating Custom Install Media
To deploy your installerconfig on install media, create a custom ISO or USB image.
Custom USB Image
sh# Download the FreeBSD memstick image fetch https://download.freebsd.org/releases/amd64/14.2-RELEASE/FreeBSD-14.2-RELEASE-amd64-memstick.img # Attach the image as a memory disk mdconfig -a -t vnode -f FreeBSD-14.2-RELEASE-amd64-memstick.img -u 1 # Mount the filesystem mount /dev/md1s2a /mnt # Copy your installerconfig cp installerconfig /mnt/etc/installerconfig # Unmount and detach umount /mnt mdconfig -d -u 1 # Write to USB drive (replace da0 with your USB device) dd if=FreeBSD-14.2-RELEASE-amd64-memstick.img of=/dev/da0 bs=1m conv=sync status=progress
Custom ISO Image
For environments that boot from ISO (virtual machines, IPMI virtual media):
sh# Extract the ISO mkdir /tmp/iso-work tar xf FreeBSD-14.2-RELEASE-amd64-disc1.iso -C /tmp/iso-work # Add the installerconfig cp installerconfig /tmp/iso-work/etc/installerconfig # Recreate the ISO pkg install cdrtools mkisofs -b boot/cdboot -no-emul-boot -r -J \ -V "FreeBSD_Install" \ -o /tmp/FreeBSD-14.2-custom.iso \ /tmp/iso-work
Post-Installation Configuration Script
For complex post-installation setup, use a separate script referenced from installerconfig:
sh# In installerconfig, after #!/bin/sh: fetch -o /tmp/post-install.sh http://deploy.internal/post-install.sh sh /tmp/post-install.sh
Example Post-Installation Script
sh#!/bin/sh # post-install.sh -- called from installerconfig set -e # ============================================ # Package installation # ============================================ env ASSUME_ALWAYS_YES=yes pkg bootstrap pkg install -y \ sudo bash tmux vim-console \ py311-salt-minion \ node_exporter \ rsync # ============================================ # User configuration # ============================================ pw useradd deploy -m -G wheel -s /usr/local/bin/bash mkdir -p /home/deploy/.ssh chmod 700 /home/deploy/.ssh # Fetch authorized keys from deployment server fetch -o /home/deploy/.ssh/authorized_keys http://deploy.internal/keys/deploy.pub chmod 600 /home/deploy/.ssh/authorized_keys chown -R deploy:deploy /home/deploy/.ssh # Passwordless sudo for deploy user echo 'deploy ALL=(ALL:ALL) NOPASSWD: ALL' > /usr/local/etc/sudoers.d/deploy chmod 440 /usr/local/etc/sudoers.d/deploy # ============================================ # SSH hardening # ============================================ cat >> /etc/ssh/sshd_config << 'SSHD' PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no UseDNS no MaxAuthTries 3 LoginGraceTime 30 SSHD # ============================================ # Firewall (PF) # ============================================ cat > /etc/pf.conf << 'PF' ext_if = "em0" set skip on lo0 set block-policy drop scrub in all block all pass out all keep state pass in on $ext_if proto tcp to port { 22 } keep state pass in on $ext_if proto icmp PF sysrc pf_enable="YES" # ============================================ # Sysctl tuning # ============================================ cat > /etc/sysctl.conf << 'SYSCTL' kern.ipc.somaxconn=4096 net.inet.tcp.msl=5000 net.inet.tcp.fast_finwait2_recycle=1 security.bsd.see_other_uids=0 security.bsd.see_other_gids=0 security.bsd.unprivileged_read_msgbuf=0 security.bsd.unprivileged_proc_debug=0 SYSCTL # ============================================ # Salt Minion (configuration management) # ============================================ cat > /usr/local/etc/salt/minion << 'SALT' master: salt.internal id: freebsd-auto SALT sysrc salt_minion_enable="YES" # ============================================ # Node Exporter (Prometheus metrics) # ============================================ sysrc node_exporter_enable="YES" # ============================================ # Periodic maintenance # ============================================ cat >> /etc/periodic.conf << 'PERIODIC' daily_clean_tmps_enable="YES" daily_status_security_enable="YES" daily_status_mail_rejects_enable="YES" weekly_locate_enable="YES" PERIODIC echo "Post-installation complete."
PXE Network Boot Installation
For large-scale deployments, PXE booting eliminates the need for physical media.
DHCP Server Configuration
Configure your DHCP server (ISC dhcpd) to provide PXE boot parameters:
sh# /usr/local/etc/dhcpd.conf subnet 192.168.1.0 netmask 255.255.255.0 { range 192.168.1.200 192.168.1.250; option routers 192.168.1.1; option domain-name-servers 1.1.1.1; # PXE boot next-server 192.168.1.10; filename "pxeboot"; # Per-host configuration for automated install host server01 { hardware ethernet 00:11:22:33:44:55; fixed-address 192.168.1.101; } }
TFTP Server Setup
shpkg install tftp-hpa sysrc tftpd_enable="YES" sysrc tftpd_flags="-s /tftpboot" mkdir -p /tftpboot
Prepare PXE Boot Files
Extract the PXE boot files from the FreeBSD release:
sh# Download the release fetch https://download.freebsd.org/releases/amd64/14.2-RELEASE/FreeBSD-14.2-RELEASE-amd64-bootonly.iso # Mount the ISO mdconfig -a -t vnode -f FreeBSD-14.2-RELEASE-amd64-bootonly.iso -u 2 mount -t cd9660 /dev/md2 /mnt # Copy PXE boot files cp /mnt/boot/pxeboot /tftpboot/ cp -r /mnt/boot/ /tftpboot/boot/ umount /mnt mdconfig -d -u 2
NFS Server for Installation Files
PXE-booted FreeBSD fetches the base system over NFS:
sh# Enable NFS sysrc nfs_server_enable="YES" sysrc mountd_enable="YES" sysrc rpcbind_enable="YES" # Export the installation files mkdir -p /install/freebsd tar xf FreeBSD-14.2-RELEASE-amd64-disc1.iso -C /install/freebsd echo '/install -ro -alldirs -maproot=root 192.168.1.0/24' > /etc/exports service nfsd start service mountd start service rpcbind start
PXE Boot loader.conf
Create /tftpboot/boot/loader.conf:
shcat > /tftpboot/boot/loader.conf << 'EOF' vfs.root.mountfrom="nfs:192.168.1.10:/install/freebsd" boot_nfs="YES" EOF
Per-Host installerconfig via HTTP
For per-host customization, use a web server that serves different installerconfig files based on IP or MAC address:
sh# In the common installerconfig preamble, fetch host-specific config: #!/bin/sh # Detect MAC address for host identification MAC=$(ifconfig em0 | awk '/ether/{print $2}' | tr ':' '-') # Fetch host-specific post-install from deployment server fetch -o /tmp/host-config.sh "http://deploy.internal/hosts/${MAC}.sh" sh /tmp/host-config.sh reboot
Automating with vm-bhyve
For virtual machine deployments on bhyve, automate VM creation and installation:
sh#!/bin/sh # create-vm.sh -- Create and install a FreeBSD VM VM_NAME=$1 VM_CPU=2 VM_MEM=2G VM_DISK=20G # Create the VM vm create -c $VM_CPU -m $VM_MEM -s $VM_DISK "$VM_NAME" # Copy custom ISO with installerconfig cp /vms/.iso/FreeBSD-14.2-custom.iso /vms/.iso/${VM_NAME}.iso # Install from ISO vm install "$VM_NAME" "${VM_NAME}.iso" echo "VM $VM_NAME created. Installation will proceed automatically."
Validation and Testing
After automated installation, verify the system:
sh#!/bin/sh # validate.sh -- Post-install validation ERRORS=0 # Check hostname hostname | grep -q "freebsd-auto" || { echo "FAIL: hostname"; ERRORS=$((ERRORS+1)); } # Check SSH service sshd status > /dev/null 2>&1 || { echo "FAIL: sshd not running"; ERRORS=$((ERRORS+1)); } # Check ZFS zpool status zroot > /dev/null 2>&1 || { echo "FAIL: zroot pool not online"; ERRORS=$((ERRORS+1)); } # Check networking ping -c 1 -t 5 1.1.1.1 > /dev/null 2>&1 || { echo "FAIL: no internet connectivity"; ERRORS=$((ERRORS+1)); } # Check DNS host freebsd.org > /dev/null 2>&1 || { echo "FAIL: DNS resolution"; ERRORS=$((ERRORS+1)); } # Check admin user id deploy > /dev/null 2>&1 || { echo "FAIL: deploy user missing"; ERRORS=$((ERRORS+1)); } # Check PF pfctl -si > /dev/null 2>&1 || { echo "FAIL: PF not running"; ERRORS=$((ERRORS+1)); } if [ $ERRORS -eq 0 ]; then echo "PASS: All checks passed" else echo "FAIL: $ERRORS checks failed" fi
Version Management and Reproducibility
Store your installerconfig files in version control:
shmkdir -p /usr/local/deploy/configs cd /usr/local/deploy/configs # Directory structure # configs/ # base/installerconfig -- common base # roles/web-server.sh -- web server post-install # roles/database.sh -- database server post-install # roles/jail-host.sh -- jail host post-install # hosts/00-11-22-33-44-55.sh -- per-host overrides
This approach ensures every installation is reproducible and auditable.
FAQ
Can bsdinstall automate UFS installations?
Yes. Set PARTITIONS=ada0 without any ZFSBOOT_* variables and bsdinstall uses UFS with auto-partitioning. For custom UFS layouts, use PARTITIONS="ada0 { 512k freebsd-boot, 4g freebsd-swap, auto freebsd-ufs / }".
How do I encrypt the root filesystem during automated install?
ZFS encryption (native) can be configured in the scripting section after pool creation. GELI full-disk encryption requires interactive passphrase entry at boot and is not suitable for fully automated installations without a key file on removable media or a network key server.
Can I use Ansible instead of installerconfig for post-install?
Yes. Use a minimal installerconfig that configures networking and SSH, creates a deploy user with an authorized key, and reboots. Then run Ansible against the new host from your control node. This two-stage approach (bsdinstall for base OS, Ansible for configuration) is the industry standard for large deployments.
How do I handle different hardware configurations?
Use DHCP reservations to map MAC addresses to hostnames, then serve per-host installerconfig files via HTTP. The installerconfig detects the hardware (MAC, disk devices) and adapts. Test each hardware configuration once manually before automating.
Does bsdinstall support UEFI boot?
Yes. On UEFI systems, bsdinstall automatically creates an EFI System Partition. No special configuration is needed in installerconfig. The PARTITIONS variable works the same way for both BIOS and UEFI.
How do I automate installation on cloud providers?
Most cloud providers (AWS, GCP, Azure, DigitalOcean, Vultr, Hetzner) provide FreeBSD images. Use cloud-init or the provider's user-data mechanism instead of bsdinstall. For custom images, build with bsdinstall locally, export the disk image, and upload it as a custom image to the provider.
Can I test installerconfig in a VM before deploying to hardware?
Absolutely. Use bhyve or VirtualBox to test your installerconfig. Create a VM, attach the custom ISO, boot, and verify the entire process completes without interaction. Iterate until the result matches your requirements.
How do I include firmware or kernel modules in the automated install?
Add the lib32.txz and ports.txz distributions to the DISTRIBUTIONS variable if needed. For custom kernel modules, compile them separately and fetch them in the post-install script with fetch and install with pkg add or copy to /boot/modules/.