Jailed HomeAssistant on FreeBSD 07 feb 2026

Setting up IoTs ("S" for security) at home, apparently HomeAssistant is the tooling to use. It's Python, so at least I have a chance of understanding what goes wrong. Which it will...

Jailed HomeAssistant on FreeBSD

Overview

Some caveats:

  1. HomeAssistant provides no support for FreeBSD (we have to tell it to ignore OS)
  2. HomeAssistant no longer provides this style of installation, only containers are supported.
  3. There is a FreeBSD HomeAssistant port, but it is broken.

A bit of a faff to get this to work, but it does run. We're using the latest available Python package

NOTE: When you start HomeAssistant, it will start using uv to build additional things (e.g. habluetooth)

Jail setup

The minimal-jail pkgset wasn't enough to install, but it was enough to run. I use "thin" jails that share the base jail. This ties nicely in to the breakage of the port: it needs to compile things so you need clang.

I've ended up with the folloing jail.conf file `/etc/jail.conf.d/hass.conf

hass {
        host.hostname = "hass";
        path = "/jails/hass";
        ip4.addr += "lo0|127.0.0.20/24";
        allow.raw_sockets = 0;
        exec.clean;
        exec.system_user = "root";
        exec.jail_user = "root";
        exec.start += "/bin/sh /etc/rc";
        exec.stop = "";
        exec.consolelog = "/var/log/jail_hass_console.log";
        mount.fstab = "/etc/fstab.hass";
        mount.devfs;
        devfs_ruleset = "6";
        mount.fdescfs;
        allow.set_hostname = 0;
        allow.sysvipc = 0;
        enforce_statfs = "2";
}

and in /etc/fstab.hass

/jails/basefull150 /jails/hass/basejail nullfs ro 0 0

And start the jail. Verify it is running.

Create a homeassistant user

On the host system we create a homeasisstant user ("hass" for short) without login rights or homedir. This way the process shows up under user "hass" on the host.

pw useradd hass -u 8123 -d /var/empty -s /usr/bin/nologin

In the jail, we'll give it a homedir /home/hass which we'll use for npm's storage.

jexec hass pw useradd hass -u 8123 -d /home/hass -m -s /bin/sh

Config for (temporary) internet access

pkg, git and npm need access to internet resources.

# Add to /usr/local/etc/pkg.conf
PKG_ENV {
    http_proxy = "http://squid.example.org:3128";
}
git config --global http.proxy = http://squid.example.org:3128
npm config set proxy http://squid.example.org:3128

NOTE: this works once you've installed npm using pkg

To use pkg, you must allow the following in squid

  • pkg.freebsd.org
  • pkgmir.geo.freebsd.org
  • (pkg0.fra.freebsd.org?)
  • (cloudfront.aws.pkgbase.freebsd.org?)

Your squid config will have to allow the hass jail to use

  • pypi.org
  • files.pythonhosted.org

Your squid access logs will show what went wrong.

Install

Install required packages in the jail

Adapted from this blog-post.

pkg -r /jails/hass install python314 rust

We need to create the directory in /usr/local as hass has no permissions to do so.

install -d -o 8123 -g 8123 /jails/hass/usr/local/homeassistant

Now we switch to the hass user. The rest of the install is as hass.

su -l hass
mkdir $HOME/bin $HOME/lib
export PATH=$PATH:$HOME/bin

Zigbee2MQTT

Another jail named zigbee, and a user zigbee.

The cheap dongle I bought shows up in dmesg like so

uchcom0 on uhub0
uchcom0: <vendor 0x1a86 USB Serial, rev 1.10/2.64, addr 1> on usbus1
uchcom0: CH340 detected

The device nodes are named ttyU*. To allow access to the Zigbee dongle from the jail, we need a specific devfs ruleset.

$ cat /etc/devfs.rules
[devfsrules_jail_zigbee=16]
add include $devfsrules_hide_all
add include $devfsrules_unhide_basic
add include $devfsrules_unhide_login
add path 'ttyU*' unhide group 1883 mode 660

The jail configuration is internal only with the specific devfs ruleset.

zigbee {
        host.hostname = "zigbee";
        path = "/jails/zigbee";
        ip4.addr += "lo0|127.0.0.21/24";
        allow.raw_sockets = 0;
        exec.clean;
        exec.system_user = "root";
        exec.jail_user = "root";
        exec.start += "/bin/sh /etc/rc";
        exec.stop = "";
        exec.consolelog = "/var/log/jail_zigbee_console.log";
        mount.fstab = "/etc/fstab.zigbee";
        mount.devfs;
        devfs_ruleset = "16";
        mount.fdescfs;
        mount.procfs;
        allow.mount;
        allow.set_hostname = 0;
        allow.sysvipc = 0;
        enforce_statfs = "2";
}

Create a group and user (id is the same as in devfs.rules!) and the homedir

pw -R /jails/zigbee groupadd zigbee -g 1883
pw -R /jails/zigbee useradd zigbee -u 1883 -d /usr/local/z2m -g zigbee -s /usr/sbin/nologin
install -d -o zigbee -g zigbee -m 750 /jails/zigbee/usr/local/z2m

Start the zigbee jail and open a shell as user zigbee in the jail.

Get the source (check for latest tag)

cd /usr/local
git clone --depth=1 https://github.com/Koenkk/zigbee2mqtt.git z2m
cd z2m
git remote set-branches --add origin 2.8.0
git fetch origin 2.8.0:2.8.0
git checkout 2.8.0

Your zigbee2mqtt now lives in /usr/local/z2m. For upgrading to later Zigbee2mqtt versions, add and checkout the new tag. Configure npm, install pnpm, and install zigbee2mqtt:

# Configure npm
$ npm set prefix="$HOME"
$ npm config set proxy http://squid.example.org:3128
# Install pnpm
$ npm install -g pnpm@latest-9
$ which pnpm
/usr/local/z2m/bin/pnpm
$ pnpm --version
9.15.9
# Install zigbee2mqtt dependencies
$ pnpm i --frozen-lockfile
# Install zigbee2mqtt
$ pnpm run build

This should finish without errors.

Create the rc script /usr/local/etc/rc.d/z2m

#!/bin/sh

# PROVIDE: z2m
# REQUIRE: DAEMON
# KEYWORD: shutdown

# FreeBSD rc.d script for zigbee2mqtt
#

# The z2m service has the following rc.conf options:
#
# z2m_enable (bool):    Set to YES to enable z2m
#                       Default: NO
# z2m_user (str):       The user to run z2m as
#                       Default: z2m
# z2m_group (str):      The group to run z2m as
#                       Default: z2m
# z2m_chdir (str):      The directory where z2m is installed
#                       Default: /usr/local/z2m
# z2m_datadir (str):    The directory where z2m's data is stored
#                       Default: /var/db/z2m
# z2m_restart (bool):   Set to YES if z2m should be automatically
#                       restarted after it crashes.
#                       Default: NO

. /etc/rc.subr

name=z2m
desc="zigbee2mqtt service"
rcvar=z2m_enable

load_rc_config $name

: ${z2m_enable:=NO}
: ${z2m_group:=zigbee}
: ${z2m_datadir:=/var/db/z2m}
: ${z2m_pidfile=/var/run/z2m/z2m.pid}
: ${z2m_restart=NO}
: ${z2m_user:=zigbee}
: ${z2m_chdir=/usr/local/z2m}
: ${z2m_env:="ZIGBEE2MQTT_DATA=${z2m_datadir}"}

# If z2m_restart is YES, then restart z2m when it crashes, otherwise
# daemon(8) will exit.
if checkyesno z2m_restart; then
        _restartargs="-r"
else
        _restartargs=""
fi

pidfile=${z2m_pidfile}

command=/usr/sbin/daemon
command_args="-f -H \
                -P ${pidfile} -t ${name} -T ${name} \
                ${_restartargs} \
                /usr/local/bin/node index.js"
required_files="${z2m_datadir}/configuration.yaml"

start_precmd="[ -d ${pidfile%/*} ] || install -d -o ${z2m_user} -g ${z2m_group} ${pidfile%/*}"

run_rc_command "$1"

Check and run

Inside your zigbee jail, you should see the USB dongle

/dev/ttyU0
/dev/ttyU0.init
/dev/ttyU0.lock

You can now start z2m by creating an rc-script /usr/local/etc/rc.d/z2m. This comes from the comms/hass2mqtt port.

#!/bin/sh

# PROVIDE: z2m
# REQUIRE: DAEMON
# KEYWORD: shutdown

# FreeBSD rc.d script for hass2mqtt
#

# The z2m service has the following rc.conf options:
#
# z2m_enable (bool):    Set to YES to enable z2m
#           Default: NO
# z2m_user (str):   The user to run z2m as
#           Default: z2m
# z2m_group (str):  The group to run z2m as
#           Default: z2m
# z2m_chdir (str):  The directory where z2m is installed
#           Default: /usr/local/z2m
# z2m_datadir (str):    The directory where z2m's data is stored
#           Default: /var/db/z2m
# z2m_restart (bool):   Set to YES if z2m should be automatically
#           restarted after it crashes.
#           Default: NO

. /etc/rc.subr

name=z2m
desc="hass2mqtt service"
rcvar=z2m_enable

load_rc_config $name

: ${z2m_enable:=NO}
: ${z2m_group:=hass}
: ${z2m_datadir:=/var/db/z2m}
: ${z2m_pidfile=/var/run/z2m/z2m.pid}
: ${z2m_restart=NO}
: ${z2m_user:=hass}
: ${z2m_chdir=/usr/local/z2m}
: ${z2m_env:="hass2MQTT_DATA=${z2m_datadir}"}

# If z2m_restart is YES, then restart z2m when it crashes, otherwise
# daemon(8) will exit.
if checkyesno z2m_restart; then
    _restartargs="-r"
else
    _restartargs=""
fi

pidfile=${z2m_pidfile}

command=/usr/sbin/daemon
command_args="-f -H \
                -P ${pidfile} -t ${name} -T ${name} \
                ${_restartargs} \
                /usr/local/bin/node index.js"
required_files="${z2m_datadir}/configuration.yaml"

start_precmd="[ -d ${pidfile%/*} ] || install -d -o ${z2m_user} -g ${z2m_group} ${pidfile%/*}"

run_rc_command "$1"

You can now enable the service

service z2m enable

and start!

service z2m start