FreeBSD.software
Home/Guides/How to Automate FreeBSD Installation with bsdinstall
tutorial·2026-04-09·9 min read

How to Automate FreeBSD Installation with bsdinstall

Automate FreeBSD installation: scripted bsdinstall, custom install media, ZFS-on-root automation, post-install configuration, and PXE network booting.

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:

  1. Preamble -- Environment variables that configure partitioning, network, and distribution selection.
  2. Scripting section -- Shell commands that run inside a chroot of 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:

sh
export 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)

sh
export 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)

sh
export 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

sh
pkg 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:

sh
cat > /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:

sh
mkdir -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/.

Get more FreeBSD guides

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