FreeBSD

Run Linux VMs on FreeBSD 15 with bhyve

Most FreeBSD users know jails for lightweight process isolation, but bhyve is a different beast entirely. It is FreeBSD’s native hypervisor, shipping in base since FreeBSD 10, and it runs full Linux (or Windows) VMs with hardware virtualization. No QEMU emulation overhead, no external hypervisor dependency. Just bhyve, a kernel module, and a tap interface.

Original content from computingforgeeks.com - post 166449

This guide walks through running an Alpine Linux VM on FreeBSD 15 using bhyve and the vm-bhyve management layer. You get a real nested-virt setup: Alpine booting under bhyve, networking via a bridge, full console access via nmdm, and autostart on boot. Tested April 2026 on FreeBSD 15.0-RELEASE with vm-bhyve 1.7.3, grub2-bhyve 0.40, and Alpine 3.20.3.

bhyve vs Jails

Jails share the host kernel. A jail running “Linux” via the Linux ABI still runs FreeBSD’s kernel underneath. bhyve is a Type 2 hypervisor: it runs guest VMs with their own kernel, their own kernel version, and full hardware isolation. If you need to run actual Linux kernel modules, test Linux-specific behaviour, or run Alpine/Ubuntu/Debian with full isolation, bhyve is the right tool. Jails are better for FreeBSD-native workloads where you want minimal overhead.

The Podman/container path sits between the two: containers share the host kernel but leverage jails for namespacing. bhyve gives you the full VM stack when that matters.

Prerequisites

Before loading modules, confirm your host supports hardware virtualization:

dmesg | grep -i vmx

On a bare-metal host you should see VT-x or SVM lines. If you are running FreeBSD inside a VM (like Proxmox/KVM as in this setup), you need the host VM to pass CPU flags through. For Proxmox, set cpu: host on the FreeBSD VM before booting. After boot, verify nested virtualization is active:

sysctl hw.vmm.maxcpu

The output should show a non-zero CPU count:

hw.vmm.maxcpu: 2

If you get sysctl: unknown oid 'hw.vmm.maxcpu', the vmm module is not loaded yet (see the next step). If it loads but returns 0, hardware virtualization is not available on the host.

You will also need:

Step 1: Load Kernel Modules

bhyve needs four kernel modules. Load them now and add them to /boot/loader.conf so they persist across reboots:

kldload vmm if_tap if_bridge nmdm

Confirm they are all loaded:

kldstat | grep -E 'vmm|if_bridge|nmdm'

All four modules should appear:

 8    1 0xffffffff83200000   340438 vmm.ko
 9    1 0xffffffff8302b000     8810 if_bridge.ko
10    1 0xffffffff83034000     6120 bridgestp.ko
11    1 0xffffffff8303b000     21dc nmdm.ko

if_tap may already be in the kernel (the module already loaded message from kldload is fine). Now make them permanent:

sysrc -f /boot/loader.conf vmm_load="YES" if_bridge_load="YES" nmdm_load="YES"

Step 2: Install vm-bhyve and grub2-bhyve

Raw bhyve is powerful but low-level. vm-bhyve wraps it with a simple CLI for creating, starting, stopping, and managing VMs. grub2-bhyve is needed to boot Linux guests (bhyve’s native loader handles FreeBSD; Linux needs GRUB to load its kernel).

pkg install -y vm-bhyve grub2-bhyve

Verify what was installed:

pkg info vm-bhyve grub2-bhyve

Both packages should be listed with their versions:

vm-bhyve-1.7.3
grub2-bhyve-0.40_11

Step 3: Initialize vm-bhyve with ZFS

Create a dedicated ZFS dataset for VMs and let vm-bhyve initialize it with its directory structure:

zfs create -o mountpoint=/vms zroot/vms
/usr/local/sbin/vm init /vms

vm-bhyve creates four subdirectories under /vms:

ls /vms

Four directories appear, each serving a distinct purpose in vm-bhyve:

.config  .img  .iso  .templates

Now configure rc.conf so vm-bhyve starts on boot and knows where to find VMs:

sysrc vm_enable="YES"
sysrc vm_dir="zfs:zroot/vms"

Step 4: Create the Virtual Switch

vm-bhyve manages VM networking through virtual switches backed by if_bridge. Create a switch named public and attach your physical NIC (vtnet0 in this setup) as the uplink:

/usr/local/sbin/vm switch create public
/usr/local/sbin/vm switch add public vtnet0

Confirm the switch is up and bridged to the physical interface:

/usr/local/sbin/vm switch list

The switch shows vm-public as its bridge interface with vtnet0 as the uplink port:

NAME    TYPE      IFACE      ADDRESS  PRIVATE  MTU  VLAN  PORTS
public  standard  vm-public  -        no       -    -     vtnet0

The bridge mode means VMs on the public switch will appear as full peers on your LAN. Each VM gets a tap interface that the bridge connects to vtnet0.

Step 5: Download the Alpine Linux ISO

Alpine’s virtual ISO (the -virt variant) is optimized for VMs: no desktop cruft, small footprint, fast boot. Place it in the vm-bhyve ISO directory:

fetch -o /vms/.iso/alpine-virt-3.20.3-x86_64.iso \
  https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-virt-3.20.3-x86_64.iso

Confirm the download completed:

ls -lh /vms/.iso/

The 61 MB ISO should be present:

total 55 MB
-rw-------  1 root wheel   61M Sep  6  2024 alpine-virt-3.20.3-x86_64.iso

Check the Alpine Linux downloads page for the current stable release if you want a newer version.

Step 6: Create the VM Template

vm-bhyve uses templates to define VM defaults. The built-in default.conf uses bhyveload (works for FreeBSD guests). Linux guests need grub as the loader. Create an Alpine template:

cat > /vms/.templates/alpine.conf << 'TMPL'
loader="grub"
cpu=1
memory=512M
network0_type="virtio-net"
network0_switch="public"
disk0_type="virtio-blk"
disk0_name="disk0.img"
grub_run_partition="1"
grub_run_dir="/boot/grub"
TMPL

The virtio-blk disk type and virtio-net network type give the best performance under bhyve. The grub_run_partition and grub_run_dir settings tell grub-bhyve where to find the bootloader after the first install reboot.

Step 7: Create and Install Alpine

Create a VM with 8 GB disk and 512 MB RAM using the alpine template:

/usr/local/sbin/vm create -t alpine -s 8G -m 512M alpine1

vm-bhyve creates a directory under /vms/alpine1/ with a config file and a sparse disk image. Start the installation:

/usr/local/sbin/vm install alpine1 alpine-virt-3.20.3-x86_64.iso

vm-bhyve launches grub-bhyve to load the kernel from the ISO, then hands off to bhyve proper:

Starting alpine1
  * found guest in /vms/alpine1
  * booting...

The VM boots from the ISO. bhyve runs grub-bhyve first to load the Linux kernel, then hands off to bhyve proper. Connect to the console via nmdm:

/usr/local/sbin/vm console alpine1

This opens a cu session on /dev/nmdm-alpine1.1B. You will see Alpine Linux booting. Log in as root (no password on the live ISO) and run the setup script:

setup-alpine

setup-alpine walks you through keyboard, hostname, network, root password, timezone, NTP, mirror, SSH, and disk setup. For the disk question, select sda (the virtio-blk disk), choose sys install mode, and confirm the wipe. The install takes about 30 seconds on a modern host.

After install completes, power off from inside Alpine:

poweroff

To exit the console without killing the VM, type ~. (tilde then dot). This is the cu escape sequence.

Step 8: Boot the Installed Alpine VM

After the install completes and the VM shuts down, start it from disk:

/usr/local/sbin/vm start alpine1

Check the VM is running:

/usr/local/sbin/vm list

Alpine1 should show Running with its bhyve process ID:

NAME     DATASTORE  LOADER  CPU  MEMORY  VNC  AUTO  STATE
alpine1  default    grub    1    512M    -    No    Running (2787)

Connect to the console and confirm Linux is running:

/usr/local/sbin/vm console alpine1

The screenshot below shows the vm list and vm info output from a running Alpine guest:

vm-bhyve list and info showing Alpine Linux running on FreeBSD 15
vm list and vm info showing Alpine1 VM running under bhyve with virtio networking

Once you log in to Alpine, run uname -a to confirm it is a real Linux kernel:

uname -a

The output confirms a real Linux kernel running under bhyve, not a FreeBSD compatibility layer:

Linux localhost 6.6.49-0-virt #1-Alpine SMP PREEMPT_DYNAMIC 2024-09-04 12:53:09 x86_64 Linux

That is Alpine 3.20.3 with Linux kernel 6.6.49 running inside a bhyve VM on FreeBSD 15. Check the network interface:

ip addr show eth0

The VM has an IP on the LAN via the FreeBSD bridge:

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP
    link/ether 58:9c:fc:0f:3d:59 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.200/24 scope global eth0
       valid_lft forever preferred_lft forever

The terminal screenshot below shows both uname -a and ip addr output from inside the running Alpine guest:

Alpine Linux uname and ip addr output running inside bhyve on FreeBSD 15
Alpine Linux kernel 6.6.49-0-virt with network connectivity confirmed inside bhyve

VM Lifecycle

The core vm-bhyve commands you will use daily:

vm list                    # list all VMs and their state
vm info alpine1            # detailed VM info including memory usage and disk paths
vm start alpine1           # start a stopped VM
vm stop alpine1            # graceful shutdown (sends ACPI power off)
vm poweroff alpine1        # forced power off (equivalent to pulling the plug)
vm console alpine1         # connect to serial console via nmdm
vm destroy alpine1         # delete the VM and all its disk images

Get a full picture of the running VM including memory usage and network stats:

vm info alpine1

This shows current memory usage, the nmdm console device path, the tap interface, and disk utilization:

------------------------
Virtual Machine: alpine1
------------------------
  state: running (2787)
  datastore: default
  loader: grub
  uuid: 4366c84d-3913-11f1-a6cd-bc2411c98a6f
  cpu: 1
  memory: 512M
  memory-resident: 146550784 (139.761M)

  console-ports
    com1: /dev/nmdm-alpine1.1B

  network-interface
    emulation: virtio-net
    virtual-switch: public
    active-device: tap0
    bridge: vm-public
    bytes-in: 7576 (7.398K)

  virtual-disk
    emulation: virtio-blk
    system-path: /vms/alpine1/disk0.img
    bytes-size: 8589934592 (8.000G)
    bytes-used: 1024 (1.000K)

Autostart on Boot

To start alpine1 automatically when FreeBSD boots, set the autostart flag:

/usr/local/sbin/vm set alpine1 loader=grub autostart=yes

Confirm it is set:

/usr/local/sbin/vm list

The AUTO column flips to Yes:

NAME     DATASTORE  LOADER  CPU  MEMORY  VNC  AUTO  STATE
alpine1  default    grub    1    512M    -    Yes   Running (2787)

The AUTO column now shows Yes. When vm_enable="YES" is in rc.conf, vm-bhyve’s rc script runs at boot and starts all VMs with autostart enabled.

Ubuntu Cloud Image (Bonus)

Alpine installs fast, but most production workloads run Ubuntu. vm-bhyve also supports cloud images via qcow2 disk import. Download an Ubuntu 24.04 cloud image, convert it with qemu-tools (installed earlier as a dependency), and create the VM from it:

fetch -o /vms/.img/ubuntu-24.04-server-cloudimg-amd64.img \
  https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

qemu-img convert -f qcow2 -O raw \
  /vms/.img/ubuntu-24.04-server-cloudimg-amd64.img \
  /vms/.img/ubuntu-24.04-server.raw

Create the Ubuntu VM using the converted raw image as the seed disk. Cloud images use cloud-init for first-boot configuration, so you also need a small seed ISO with your SSH key or password. Alternatively, add Ubuntu’s ISO installer route (same as Alpine above) for a full interactive install. The ISO method is covered in the bhyve documentation.

Troubleshooting

These are the errors I actually hit during testing.

sysctl: unknown oid 'hw.vmm.maxcpu'

The vmm module is not loaded. Run kldload vmm. If kldload then returns:

kldload: can't load vmm: No such file or directory

Your kernel was built without vmm support (uncommon on GENERIC) or you are on a VM without hardware passthrough. On Proxmox/KVM, the fix is to set cpu: host (not kvm64) on the outer VM and reboot FreeBSD. After that, kldload vmm should succeed and hw.vmm.maxcpu should show a positive value.

vm install hangs at “booting…” with no console output

Connect to the console separately:

/usr/local/sbin/vm console alpine1

The install process runs in the background. The console gives you the serial output. If the console shows all ports busy, an old cu session is still holding the nmdm device. Find and kill it:

fuser /dev/nmdm-alpine1.1B

Kill the PID shown, then reconnect.

grub-bhyve: Unrecognized image format

This means grub2-bhyve is not finding the correct GRUB paths in the guest. The template needs grub_run_partition and grub_run_dir set for the installed system. After installation to disk, Alpine puts GRUB in /boot/grub on partition 1. The template above already handles this. If you see this error, verify /vms/.templates/alpine.conf contains the grub_run_partition="1" and grub_run_dir="/boot/grub" lines.

vm switch add fails with “interface not found”

The if_bridge module is not loaded. Run:

kldload if_bridge

Then re-run the vm switch add command. This error only appears if the modules were not loaded before running vm switch create.

VM gets no DHCP address

The bridge has a 15-second STP learning delay by default. If the VM boots quickly and runs DHCP before the bridge port is in forwarding state, DHCP discovery packets get dropped. Two fixes: run setup-alpine with a static IP instead of DHCP, or disable STP on the bridge (acceptable for home/lab use):

ifconfig vm-public proto stp 0

This is per-boot. To make it permanent, add it to /etc/rc.local.

Kernel warning: Adding member interface vtnet0 which has IP address assigned

This shows up in /var/log/messages when you add a NIC with an IP to the bridge:

vm-public: WARNING: Adding member interface vtnet0 which has an IP address assigned is deprecated

It is a warning, not an error. Bridging still works. In a production setup, you would remove the IP from vtnet0, assign it to the bridge interface instead, and route everything through the bridge. For a lab bhyve setup this warning is safe to ignore.

bhyve Alpine Linux install boot sequence from grub-bhyve on FreeBSD 15
Alpine Linux boot console via grub-bhyve showing vm install progress and first login

For more on FreeBSD networking and configuration, the hostname and static IP guide covers rc.conf networking in detail.

Related Articles

FreeBSD How to Install pgAdmin4 on FreeBSD 12 Email Install and Configure iRedMail Server on FreeBSD 13 FreeBSD ZFS Pool Design on FreeBSD 15: mirror, raidz, draid FreeBSD FreeBSD vs GhostBSD vs NomadBSD – Comparison Table

Leave a Comment

Press ESC to close