Jailed Zigbee2MQTT on FreeBSD 01 feb 2026

Setting up IoTs ("S" for security) at home, wanted to add Zigbee too. Especially so as the port has been broken for quite a while.

Jailed Zigbee2MQTT on FreeBSD

Overview

The goal is

  1. for zigbee2mqtt to run in the zigbee jail
  2. under zigbee user
  3. with the mosquitto network address the only allowed destination
  4. the jail will use an address on lo0 loopback.

Tasks

  1. Set up devfs to use the USB dongle in the zigbee jail
  2. Create a new jail for Zigbee2mqtt
  3. Create zigbee user on host and in jail

Set up devfs ruleset for the USB dongle

I've bought a cheap USB dongle on Aliexpress to play with.

FreeBSD detects the dongle as C340 and creates the necessary device nodes. The serial interface shows up as ttyU0.

I've chosen the first free number (6) for the ruleset. This is required in the jail config.

[devfsrules_jail_zigbee=6]
add include $devfsrules_hide_all
add include $devfsrules_unhide_basic
add include $devfsrules_unhide_login
add path 'ttyU0*' unhide group 1883 mode 660

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/zigbee.conf

zigbee {
        host.hostname = "zigbee";
        path = "/jails/zigbee";
        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_zigbee_console.log";
        mount.fstab = "/etc/fstab.zigbee";
        mount.devfs;
        devfs_ruleset = "6";
        mount.fdescfs;
        allow.set_hostname = 0;
        allow.sysvipc = 0;
        enforce_statfs = "2";
}

and in /etc/fstab.zigbee

/jails/basefull150 /jails/zigbee/basejail nullfs ro 0 0

And start the jail. Verify it is running.

Create a zigbee user

On the host system we create a zigbee user without login rights or homedir. This way the process shows up under user "zigbee" on the host.

pw useradd zigbee -u 1883 -d /var/empty -s /usr/bin/nologin

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

jexec zigbee useradd zigbee -u 1883 -d /home/zigbee -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 zigbee jail to use

  • github.com
  • registry.npmjs.org
  • nodejs.org

Your squid access logs will show what went wrong.

Install

Install required packages in the jail

Adapted from this PR.

pkg -r /jails/zigbee install npm-node24 python git-lite bash

(bash for later updating of zigbee2mqtt using the update.sh script)

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

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

Get the source (check for latest tag)

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

Your zigbee2mqtt now lives in /usr/local/z2m. Configure npm, install pnpm, and install zigbee2mqtt:

npm set prefix="$HOME"
npm config set proxy http://squid.example.org:3128
npm install -g pnpm@latest-9
which pnpm
/home/zigbee/bin/pnpm
pnpm --version
9.15.9
pnpm i --frozen-lockfile
pnpm run build

This should finish without errors.

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/zigbee2mqtt port.

#!/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"

You can now enable the service

service z2m enable

and start!

service z2m start