If you've got a drawer full of Zigbee smart bulbs, sensors, and plugs, you've probably run into the same wall everyone does: most Zigbee gear wants to talk to its own cloud-connected hub. Philips Hue wants a Hue Bridge. IKEA wants a Dirigera. Nobody wants to run six different apps just to turn on a lamp.

Zigbee2MQTT fixes that by talking directly to a cheap USB Zigbee coordinator and translating everything into MQTT, an open messaging protocol that pretty much every home automation tool understands. Run it in its own LXC container on Proxmox VE and you get a clean, isolated, easy-to-back-up slice of your server dedicated to just this one job.

This guide walks through the whole thing: passing a USB Zigbee dongle into an unprivileged container, installing an MQTT broker, building Zigbee2MQTT from source (yes, from source — that's actually the normal way to install it), and wiring up a systemd service so it survives reboots.

What You Will Learn

  • What Zigbee2MQTT and MQTT actually do, and why they're useful even if you already use Home Assistant
  • How to pass a USB Zigbee coordinator into an unprivileged LXC container using Proxmox VE's modern device-passthrough syntax
  • How to install Mosquitto as your MQTT broker
  • How to install Node.js, pnpm, and Zigbee2MQTT itself, step by step
  • How to set Zigbee2MQTT up as a systemd service so it starts automatically
  • Common errors people hit with USB passthrough and serial ports, and how to fix them

What Is This Feature?

Zigbee is a low-power wireless radio protocol built for battery-operated smart home devices — door sensors, motion detectors, bulbs, that sort of thing. It's not Wi-Fi, and it's not Bluetooth. It uses its own mesh network, which means every powered Zigbee device (a bulb, a plug) actually helps relay signal for battery devices further away.

To talk to Zigbee devices, your server needs a Zigbee coordinator — a small USB radio dongle. Popular ones include the Sonoff Zigbee 3.0 USB Dongle Plus and the ConBee II. That dongle is the physical thing you'll be passing into your LXC container in a minute.

MQTT (Message Queuing Telemetry Transport) is a lightweight publish/subscribe messaging protocol. Think of it as a bulletin board: devices "publish" messages to named topics, and anything interested in that topic — like Home Assistant, Node-RED, or a script you wrote — "subscribes" and gets notified. It was designed for exactly this kind of low-bandwidth, many-small-devices scenario.

Zigbee2MQTT sits in the middle. It talks to the USB coordinator on one side and an MQTT broker on the other, translating raw Zigbee radio traffic into readable MQTT topics like zigbee2mqtt/living-room-sensor/temperature. Anything that speaks MQTT can then use that data without knowing anything about Zigbee at all.

Why Would You Use It?

The honest answer: because the alternative is worse. Vendor hubs work fine right up until the company changes its cloud terms, discontinues the app, or decides your device needs a subscription. Zigbee2MQTT runs entirely on hardware you control, with no cloud dependency and no account required.

It also supports an enormous range of devices — Zigbee2MQTT's supported-devices list runs into the thousands, covering brands like IKEA, Aqara, Sonoff, Tuya, Philips Hue (in Zigbee mode), and dozens more, all through one coordinator and one piece of software.

Running it in its own LXC container on Proxmox rather than bolting it onto an existing server has a few real advantages. You can snapshot it before an update and roll back in seconds if something breaks. You can back it up independently with vzdump or Proxmox Backup Server. And if it crashes or eats CPU for some reason, it's not going to take your other services down with it.

I'll be upfront about one thing: Zigbee2MQTT's own documentation actually recommends Docker as the easiest install path. That's true if all you care about is convenience. But a dedicated LXC container gives you better isolation on Proxmox, uses less overhead than a full VM, and if you're already running Proxmox, it fits naturally into your existing backup and snapshot workflow. Use whichever method you're comfortable maintaining.

Prerequisites

  • A Proxmox VE host running version 8.1 or later (the simplified USB passthrough syntax used here needs at least 8.1; if you're on the current 9.x branch, everything below works unchanged)
  • An existing Debian 12 (Bookworm) LXC container — unprivileged is fine, and is actually what we want here. 1 vCPU, 1 GB of RAM, and 4 GB of disk is plenty
  • Root access to the Proxmox shell (via the web console or SSH)
  • A USB Zigbee coordinator, such as a Sonoff Zigbee 3.0 USB Dongle Plus or a ConBee II, plugged into the Proxmox host
  • Basic comfort typing commands into a terminal — nothing here requires prior Node.js or MQTT experience

If you haven't created the container yet, use the Proxmox web UI's "Create CT" wizard with a Debian 12 template. Leave everything else at the defaults unless you have a reason to change it.

Step-by-Step Tutorial

Step 1: Find Your Zigbee Dongle's Persistent Device Path

Plug the USB dongle into the Proxmox host, then open a shell on the host (not the container) and run:

ls -l /dev/serial/by-id/

You'll see something like this:

usb-ITEAD_SONOFF_Zigbee_3.0_USB_Dongle_Plus_V2_20230509152410-if00 -> ../../ttyUSB0

Copy that full usb-ITEAD_... name. This matters more than it sounds like it should: if you pass through /dev/ttyUSB0 directly, that name can shift to /dev/ttyUSB1 after a reboot if you ever plug in a second USB-serial device. The by-id path is tied to the dongle's actual serial number, so it never changes.

Step 2: Pass the Dongle Into the Container

Find your container's ID from the Proxmox UI (it's the number next to its name, something like 105). Then, from the Proxmox host shell, run:

pct set 105 -dev0 /dev/serial/by-id/usb-ITEAD_SONOFF_Zigbee_3.0_USB_Dongle_Plus_V2_20230509152410-if00,uid=0,gid=20,mode=0660

Swap 105 for your actual container ID and the device path for whatever you copied in Step 1. This is Proxmox VE's device-passthrough syntax, available since version 8.1, and it works with unprivileged containers — you don't need to make this a privileged container just to reach a USB device, which is the old, less secure way people used to do this.

The uid=0,gid=20,mode=0660 part sets the ownership and permissions of the device node as it will appear inside the container: owned by root, group 20 (dialout, the group Linux traditionally uses for serial devices), readable and writable by owner and group only.

Restart the container for the change to take effect:

pct reboot 105

Step 3: Confirm the Device Is Visible Inside the Container

Enter the container:

pct enter 105

Then check:

ls -l /dev/ttyUSB0

If you see the device listed with group dialout, you're good. If it's missing entirely, double-check the device path from Step 1 — dongles sometimes enumerate as ttyACM0 instead of ttyUSB0 depending on the chipset, so don't assume the number.

Step 4: Install Mosquitto (Your MQTT Broker)

Still inside the container, update packages and install Mosquitto — a lightweight, well-tested MQTT broker that's in Debian's default repositories:

apt update && apt upgrade -y
apt install -y mosquitto mosquitto-clients

Enable it so it starts on boot and starts right now:

systemctl enable --now mosquitto

By default Mosquitto listens on localhost:1883, which is exactly what we want since Zigbee2MQTT will run on the same container and talk to it over 127.0.0.1. No extra configuration needed for this setup.

Step 5: Install Node.js

Zigbee2MQTT needs Node.js 20 or 22 — older versions aren't supported by the current release line. Install curl first, then pull Node.js from NodeSource's official repository:

apt install -y curl
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
apt install -y nodejs git make g++ gcc libsystemd-dev

Check that it installed a supported version:

node -v

You want to see v20.x or v22.x. The git, make, g++, and gcc packages are there because a couple of Zigbee2MQTT's dependencies compile native code during install — skip them and the next step will fail partway through with a cryptic node-gyp error.

Step 6: Enable pnpm

Recent Zigbee2MQTT versions use pnpm instead of npm for installing dependencies. Node ships with Corepack, which can enable pnpm for you without a separate install:

corepack enable

Step 7: Download and Build Zigbee2MQTT

Create a directory for it and hand ownership to your current user (if you're working as root in the container, this is mostly a formality, but it's the official recommendation):

mkdir /opt/zigbee2mqtt
chown -R ${USER}: /opt/zigbee2mqtt
git clone --depth 1 https://github.com/Koenkk/zigbee2mqtt.git /opt/zigbee2mqtt
cd /opt/zigbee2mqtt
pnpm install --frozen-lockfile

The --depth 1 flag on the clone just grabs the latest commit instead of the entire project history — there's no reason to download years of git log for an install. The pnpm install step downloads and builds all of Zigbee2MQTT's dependencies. On a modest LXC container this takes somewhere around three to five minutes. Grab a coffee.

Step 8: First Run and Onboarding

Start it manually the first time so you can walk through the setup wizard:

pnpm start

Watch the output — it'll print a line telling you to open a browser to something like http://<container-ip>:8080. Head there from your regular computer. You'll land on an onboarding screen that asks for two things: your MQTT server address (enter mqtt://localhost:1883 since Mosquitto is running locally) and your Zigbee adapter's serial path (enter the same /dev/ttyUSB0 or by-id path from earlier — the wizard usually detects it automatically).

Once you save, Zigbee2MQTT restarts itself with a proper configuration.yaml written to /opt/zigbee2mqtt/data/. Press Ctrl+C back in your terminal to stop the manual run — we're about to make this permanent with systemd.

Step 9: Create the systemd Service

Create a new service file:

nano /etc/systemd/system/zigbee2mqtt.service

Paste this in:

[Unit]
Description=zigbee2mqtt
After=network.target

[Service]
Environment=NODE_ENV=production
Type=notify
ExecStart=/usr/bin/node index.js
WorkingDirectory=/opt/zigbee2mqtt
StandardOutput=inherit
StandardError=inherit
WatchdogSec=10s
Restart=always
RestartSec=10s
User=root

[Install]
WantedBy=multi-user.target

Save and exit, then reload systemd and enable the service:

systemctl daemon-reload
systemctl enable --now zigbee2mqtt

Check that it's actually running:

systemctl status zigbee2mqtt

You should see "active (running)" in green. If it says "failed" instead, jump down to the Troubleshooting section before doing anything else.

Step 10: Pair Your First Device

Open http://<container-ip>:8080 again — this is the Zigbee2MQTT web frontend, and it's running as a systemd service now, so it'll survive reboots. Click "Permit join (All)" in the top right, then put a Zigbee device into pairing mode (for most bulbs, this is a specific power-cycle pattern — check the device's manual). Within a few seconds it should show up in the device list.

Commands Explained

CommandWhat It Does
ls -l /dev/serial/by-id/Lists USB serial devices by their permanent, hardware-based name instead of the reboot-unstable /dev/ttyUSBx name
pct set <id> -dev0 ...Passes a host device through to an LXC container — the modern, PVE 8.1+ way to do USB passthrough for containers
pct reboot <id>Reboots a specific container by its ID, needed after changing its device config
pct enter <id>Drops you into a root shell inside the container directly from the host, no SSH required
systemctl enable --now <service>Enables a service to start on boot and starts it immediately in one command
corepack enableTurns on Node's built-in package manager shim, which provides pnpm without a separate install step
git clone --depth 1Downloads only the latest snapshot of a repository, skipping its full commit history to save time and disk space
pnpm install --frozen-lockfileInstalls dependencies exactly as pinned in the lockfile, without silently upgrading anything
journalctl -u zigbee2mqtt -fStreams the service's live log output, the first place to look when something's wrong

Common Errors

"Error while opening serial port" or "Error: Cannot open /dev/ttyUSB0" — almost always a passthrough problem. Either the pct set -dev0 command didn't run correctly, the container wasn't rebooted afterward, or the dongle re-enumerated under a different name. Re-check ls -l /dev/serial/by-id/ on the host.

node-gyp build errors during pnpm install — this almost always means you skipped installing make, g++, or gcc in Step 5. Native modules need a working compiler toolchain to build.

"connect ECONNREFUSED 127.0.0.1:1883" — Zigbee2MQTT can't reach Mosquitto. Check that Mosquitto is actually running with systemctl status mosquitto, and confirm you entered mqtt://localhost:1883 exactly during onboarding, not an external IP.

Service shows "failed" immediately after systemctl start — usually a typo in the service file, or the working directory path being wrong. Run journalctl -u zigbee2mqtt -n 50 to see the actual error instead of guessing.

Devices pair but data never updates — often a weak mesh, not a software bug. Zigbee is a mesh network, and it genuinely improves as you add more powered devices (bulbs, plugs) that relay signal for battery sensors further away.

Troubleshooting

Start with the logs. Nine times out of ten the answer is sitting right there:

journalctl -u zigbee2mqtt -f

If the service won't start at all, check the device permissions from inside the container:

ls -l /dev/ttyUSB0

You're looking for group ownership matching what you set in the pct set -dev0 command. If it's wrong, adjust the uid=/gid= values on the host and reboot the container again.

If Mosquitto seems to be the problem, test it directly with the client tools you installed in Step 4. Open two terminals into the container. In one:

mosquitto_sub -h localhost -t "test/topic"

In the other:

mosquitto_pub -h localhost -t "test/topic" -m "hello"

If "hello" shows up in the first terminal, Mosquitto is working fine and your problem is somewhere in Zigbee2MQTT's configuration, not the broker.

One thing that trips people up specifically on Proxmox: if you clone the container later for a second Zigbee setup, the passthrough won't carry over automatically, and you'll need to run pct set again on the new container ID with the same dongle — assuming you even have a second dongle, since one coordinator can only be attached to one container at a time.

Best Practices

  • Always use the /dev/serial/by-id/ path for passthrough, never the raw /dev/ttyUSBx name — it's the difference between a setup that survives a reboot and one that randomly breaks
  • Back up /opt/zigbee2mqtt/data/ regularly. That folder holds configuration.yaml and database.db, and losing it means re-pairing every single device by hand
  • Keep the container unprivileged. There's no security reason to run this privileged, and it narrows the blast radius if the service is ever compromised
  • Set a custom network_key in configuration.yaml instead of leaving Zigbee2MQTT to generate a random one silently — write it down somewhere safe, because you'll need it if you ever have to restore from a backup that predates a key rotation
  • Take a Proxmox snapshot before updating Zigbee2MQTT (git pull and reinstall). Updates are generally smooth, but a snapshot means a bad one costs you thirty seconds, not an evening

Frequently Asked Questions

Do I need a privileged LXC container for this?

No. Proxmox VE 8.1 and later support device passthrough for unprivileged containers directly through pct set -dev0, which is what this guide uses.

Can I use Zigbee2MQTT without Home Assistant?

Yes. It works standalone through its own web frontend, and anything that speaks MQTT can subscribe to its topics. Home Assistant just happens to auto-discover Zigbee2MQTT devices out of the box, which is why the two are often mentioned together.

Which USB Zigbee dongle should I buy?

The Sonoff Zigbee 3.0 USB Dongle Plus (both V1 and V2) is the one most commonly recommended for new setups, mainly because it's cheap, well supported, and easy to find. The ConBee II is a solid alternative if you already own one.

Model numbers and specific hardware recommendations can change, so check Zigbee2MQTT's own supported-adapters documentation before buying if you want the current picture.

Will my devices lose their pairing if I reboot the container?

No, as long as /opt/zigbee2mqtt/data/database.db is intact, all paired devices stay paired across restarts.

How much RAM does Zigbee2MQTT actually need?

Not much. A container with 512 MB to 1 GB of RAM handles a home's worth of Zigbee devices without breaking a sweat. It's a lightweight Node.js process, not a database server.

Can two containers share the same USB dongle?

No. A USB device can only be passed through to one container (or VM) at a time on Proxmox. If you need Zigbee2MQTT in two places, you need two coordinators.

Conclusion

Once this is running, you'll mostly forget it's there — which is exactly the point. Your Zigbee devices show up in Home Assistant (or whatever you're piping MQTT into) without a single cloud account in sight, and the whole thing lives in a container small enough that it barely registers in your resource graphs.

The trickiest part of this whole process isn't Zigbee2MQTT itself — it's getting the USB passthrough right the first time. Get the by-id path correct, get the permissions correct, and everything downstream tends to just work.