I run HomeAssistant Core on FreeBSD inside a jail managed with BastilleBSD — no Docker, no VM, just a lightweight VNET jail with its own IP on the LAN. This post covers the full setup: three jails, a bootstrap script, USB passthrough for Zigbee, and WiFi devices on the same subnet.

Architecture

FreeBSD 15.1-RELEASE Host
│
├── homeassistant (192.168.2.50)  — HA Core :8123
├── mosquitto    (192.168.2.52)  — MQTT broker :1883
└── zigbee2mqtt  (192.168.2.51)  — Zigbee2MQTT, Sonoff USB passthrough

All three jails are VNET on the same /24 subnet. HA Core talks to Mosquitto over MQTT, Zigbee2MQTT publishes device states to Mosquitto, and HA subscribes. Shelly devices connect directly over WiFi and are auto-discovered by HA on the LAN.

Why jails?

  • Native FreeBSD — no Docker overlay, no Linux emulation
  • VNET gives each jail a real IP on the LAN — devices discover HA directly
  • ZFS snapshots for quick rollbacks before upgrades
  • Each service in its own jail — clean isolation, restart one without affecting others

Creating the jails

bastille create -V homeassistant 15.1-RELEASE 192.168.2.50/24 vtnet0
bastille create -V mosquitto 15.1-RELEASE 192.168.2.52/24 vtnet0
bastille create -V zigbee2mqtt 15.1-RELEASE 192.168.2.51/24 vtnet0

The -V flag gives each jail a VNET interface with its own IP and routing table — they live on the same broadcast domain as the host.

HomeAssistant jail

A bootstrap script handles the full setup:

  • Repository: codeberg.org/dkade/BSD
  • Method: Python 3.14 virtual environment under /usr/local/etc/homeassistant/venv
  • User: dedicated hass system user
  • Service: FreeBSD rc.d script using daemon -u hass
  • Enable: sysrc homeassistant_enable=YES

What the script does:

# Inside the jail as root
fetch https://codeberg.org/dkade/BSD/raw/branch/main/FreeBSD/install_ha_freebsd.sh
sh install_ha_freebsd.sh

It installs system packages (python314, gcc12, rust, dbus, libffi, openssl, etc.), creates a venv, and installs homeassistant along with numpy, zlib_ng, and isal for performance. The rc.d script starts HA as the hass user and listens on port 8123.

After first boot, complete the onboarding at http://192.168.2.50:8123.

Mosquitto jail

bastille console mosquitto
pkg install mosquitto
sysrc mosquitto_enable=YES
service mosquitto start

No authentication is needed on a local subnet — configure listener 1883 192.168.2.52 in /usr/local/etc/mosquitto/mosquitto.conf to bind to the jail IP.

Zigbee2MQTT jail

This jail needs access to the Sonoff Zigbee coordinator plugged into the host.

USB passthrough with devfs

On the host (/etc/devfs.rules):

[bastille_usb=101]
add path 'usbctl' mode 0660
add path 'usb/*' mode 0660
add path 'ugen*' mode 0660
add path 'cuaU*' mode 0660
add path 'ttyU*' mode 0660

Set the rule in /usr/local/bastille/jails/zigbee2mqtt/jail.conf:

devfs_ruleset = "bastille_usb";

Then find the Sonoff device:

# On the host
ls /dev/cuaU*
# /dev/cuaU0 <- this is the Sonoff

Restart the jail and you should see the device inside:

bastille restart zigbee2mqtt
bastille console zigbee2mqtt
ls /dev/cuaU*

Installing and configuring

bastille console zigbee2mqtt
pkg install node npm mosquitto-libs
npm install -g zigbee2mqtt

Create /usr/local/etc/zigbee2mqtt/configuration.yaml:

mqtt:
  server: mqtt://192.168.2.52:1883
serial:
  port: /dev/cuaU0
frontend:
  port: 8080

rc.d script at /usr/local/etc/rc.d/zigbee2mqtt to start via daemon, then sysrc zigbee2mqtt_enable=YES && service zigbee2mqtt start.

Device integration

  • Shelly (WiFi): connect them to your WiFi. HA auto-discovers them via the LAN once the Shelly integration is added in the UI.
  • Zigbee (Sonoff sensors, bulbs, etc.): pair through the Zigbee2MQTT frontend (http://192.168.2.51:8080). HA receives device states via MQTT auto-discovery.

Upgrading

# HA Core
bastille console homeassistant
service homeassistant stop
su -l hass -c \
  'HOME=/usr/local/etc/homeassistant PIP_CACHE_DIR=/tmp/pip-cache \
   /usr/local/etc/homeassistant/venv/bin/pip install --upgrade homeassistant'
service homeassistant start
# zigbee2mqtt
bastille console zigbee2mqtt
service zigbee2mqtt stop
npm update -g zigbee2mqtt
service zigbee2mqtt start
# mosquitto
bastille console mosquitto
pkg upgrade mosquitto
service mosquitto restart

Gotchas

  • Python compilation: HA Core compiles C and Rust extensions during pip install. Make sure gcc12, rust, libffi, and openssl are installed in the jail before installing HA.
  • dbus: HA Core expects D-Bus to be running (service dbus start). The rc.d script has REQUIRE: dbus, so dbus starts first.
  • USB devfs: The devfs ruleset must be applied before the jail starts. Double-check the ruleset number matches in both /etc/devfs.rules and the jail config.
  • VNET routing: If jails can’t reach the internet for pkg install, check that gateway_enable="YES" is set on the host and NAT/pf is configured for the VNET subnet.
  • Persistent pip cache: The script sets PIP_CACHE_DIR=/tmp/pip-cache so you don’t re-download wheels on every upgrade.

Summary

Three FreeBSD jails managed with BastilleBSD running HA Core, Mosquitto, and Zigbee2MQTT — each with its own IP, rc.d service, and clean isolation. The bootstrap script at codeberg.org/dkade/BSD automates the HA jail from scratch, and the rest is just standard FreeBSD package management.


Disclaimer: I use AI as a productivity tool. For a senior engineer, AI is incredibly powerful as one can focus on the solution design and conceptualization and leave the boring part that is implementation to the AI.