Mosquitto is the most widely deployed open-source MQTT broker, and Zigbee2MQTT bridges Zigbee devices to MQTT, removing the need for vendor hubs like SmartThings or Hue Bridge. Running both on a RamNode VPS gives you a centralized MQTT fabric for IoT projects, home automation backends, and remote telemetry, without depending on cloud services.
This guide covers a production deployment of Mosquitto with TLS, password and ACL-based authentication, and a Zigbee2MQTT instance that connects to a network-attached Zigbee coordinator on your local network. Because a VPS has no USB ports, the Zigbee radio must live on-prem and expose itself over TCP, which is the only realistic architecture for cloud-hosted Zigbee2MQTT.
Architecture Overview
There are three sensible ways to combine these services with a VPS:
- Broker only: Mosquitto runs on the VPS, and Zigbee2MQTT runs at home on a Raspberry Pi or mini-PC with a USB Zigbee coordinator. The local Zigbee2MQTT publishes to the VPS over TLS-MQTT.
- Broker plus remote-coordinator Zigbee2MQTT: Both Mosquitto and Zigbee2MQTT run on the VPS. A network-attached Zigbee coordinator (such as the SMLIGHT SLZB-06, ITead Zigbee 3.0 USB Dongle Plus paired with
ser2net, or any TCP-exposed coordinator) lives at home. Zigbee2MQTT connects to it over the public internet, ideally tunneled. - Hybrid: Mosquitto on the VPS, multiple Zigbee2MQTT instances at different physical sites, each publishing to the same broker.
This guide walks through option 2, which is the most demanding and covers everything needed for options 1 and 3 as a subset.
Resource Requirements
For a RamNode VPS you should plan for the following minimums:
- CPU: 1 vCPU
- RAM: 1 GB (Mosquitto uses around 15 MB, Zigbee2MQTT around 150 MB)
- Disk: 10 GB SSD
- OS: Ubuntu 24.04 LTS or Debian 12
A 2 GB plan gives comfortable headroom and room to add Node-RED or a small time-series store like VictoriaMetrics later.
Prerequisites
- A RamNode VPS with Ubuntu 24.04 installed
- A domain name pointed at the VPS IPv4 address (A record on
mqtt.example.com) - SSH access as a non-root sudo user
- A network-attached Zigbee coordinator on your home network, with a public-facing tunnel (Cloudflare Tunnel, Tailscale, WireGuard, or a port-forwarded TCP port behind firewall ACLs)
If you don't already have a tunnel from your VPS into your home network, set up Tailscale or WireGuard before continuing. Exposing a Zigbee coordinator on the open internet without authentication is a serious security risk.
Initial Server Hardening
Log in and apply baseline hardening before installing anything:
sudo apt update && sudo apt upgrade -y
sudo apt install -y ufw fail2ban unattended-upgrades curl gnupg ca-certificates
sudo dpkg-reconfigure --priority=low unattended-upgradesConfigure the firewall with a deny-by-default posture:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH'
sudo ufw allow 80/tcp comment 'HTTP for Lets Encrypt'
sudo ufw allow 443/tcp comment 'HTTPS'
sudo ufw allow 8883/tcp comment 'MQTT over TLS'
sudo ufw enable
sudo ufw status verboseNote that we are not opening 1883 (plaintext MQTT). All client connections will use 8883 with TLS.
If you have a static IP or a known set of admin IPs, restrict SSH further:
sudo ufw delete allow 22/tcp
sudo ufw allow from 203.0.113.42 to any port 22 proto tcpInstall Docker
We will run Mosquitto and Zigbee2MQTT in containers for clean isolation and easy upgrades.
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp dockerVerify with docker compose version. You should see a v2.x release.
Directory Layout
Create a clean project structure so volumes and configs are easy to back up:
sudo mkdir -p /opt/mqtt/{mosquitto/{config,data,log},zigbee2mqtt/data,certs}
sudo chown -R $USER:$USER /opt/mqttGenerate TLS Certificates with Let's Encrypt
We will issue certificates with certbot standalone mode, then mount them read-only into Mosquitto.
sudo apt install -y certbot
sudo certbot certonly --standalone -d mqtt.example.com --agree-tos --register-unsafely-without-email --non-interactiveThe certificates land in /etc/letsencrypt/live/mqtt.example.com/. Mosquitto running as a non-root user inside the container cannot read these by default, so we deploy a renewal hook that copies them into our project directory with appropriate permissions:
Create /etc/letsencrypt/renewal-hooks/deploy/mosquitto.sh:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/mosquitto.sh > /dev/null <<'EOF'
#!/bin/bash
set -e
DOMAIN="mqtt.example.com"
DEST="/opt/mqtt/certs"
cp /etc/letsencrypt/live/${DOMAIN}/fullchain.pem ${DEST}/fullchain.pem
cp /etc/letsencrypt/live/${DOMAIN}/privkey.pem ${DEST}/privkey.pem
chown 1883:1883 ${DEST}/fullchain.pem ${DEST}/privkey.pem
chmod 644 ${DEST}/fullchain.pem
chmod 600 ${DEST}/privkey.pem
docker restart mosquitto || true
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/mosquitto.sh
sudo /etc/letsencrypt/renewal-hooks/deploy/mosquitto.shUID 1883 is the mosquitto user inside the Eclipse Mosquitto image.
Mosquitto Configuration
Create /opt/mqtt/mosquitto/config/mosquitto.conf:
persistence true
persistence_location /mosquitto/data/
log_dest stdout
log_type error
log_type warning
log_type notice
log_type information
log_timestamp true
# No anonymous access
allow_anonymous false
password_file /mosquitto/config/passwd
acl_file /mosquitto/config/acl
# TLS listener for external clients
listener 8883
protocol mqtt
cafile /mosquitto/certs/fullchain.pem
certfile /mosquitto/certs/fullchain.pem
keyfile /mosquitto/certs/privkey.pem
tls_version tlsv1.2
require_certificate false
# Internal listener for Zigbee2MQTT on the same Docker network
listener 1883
protocol mqtt
allow_anonymous falseNow create the password and ACL files. The password file is created empty and populated using mosquitto_passwd:
touch /opt/mqtt/mosquitto/config/passwd
touch /opt/mqtt/mosquitto/config/aclDocker Compose Manifest
Create /opt/mqtt/docker-compose.yml:
services:
mosquitto:
image: eclipse-mosquitto:2.0
container_name: mosquitto
restart: unless-stopped
ports:
- "8883:8883"
volumes:
- ./mosquitto/config:/mosquitto/config
- ./mosquitto/data:/mosquitto/data
- ./mosquitto/log:/mosquitto/log
- ./certs:/mosquitto/certs:ro
networks:
- mqtt
zigbee2mqtt:
image: koenkk/zigbee2mqtt:latest
container_name: zigbee2mqtt
restart: unless-stopped
depends_on:
- mosquitto
volumes:
- ./zigbee2mqtt/data:/app/data
- /run/udev:/run/udev:ro
environment:
- TZ=America/New_York
ports:
- "127.0.0.1:8081:8080"
networks:
- mqtt
networks:
mqtt:
driver: bridgeThe Zigbee2MQTT frontend (port 8080 inside the container) is bound only to localhost on the host, because we will expose it through a reverse proxy with authentication, not directly.
Creating MQTT Users
Start Mosquitto first so we can use it to hash passwords:
cd /opt/mqtt
docker compose up -d mosquitto
docker exec -it mosquitto mosquitto_passwd -b /mosquitto/config/passwd zigbee2mqtt 'STRONG_PASSWORD_HERE'
docker exec -it mosquitto mosquitto_passwd -b /mosquitto/config/passwd dashboard 'ANOTHER_STRONG_PASSWORD'
docker exec -it mosquitto mosquitto_passwd -b /mosquitto/config/passwd sensors 'YET_ANOTHER_PASSWORD'Use a password manager and 24+ character random strings. These credentials will be embedded in client configs and homes.
ACL Policy
Edit /opt/mqtt/mosquitto/config/acl:
# Zigbee2MQTT has full access to its own namespace
user zigbee2mqtt
topic readwrite zigbee2mqtt/#
topic readwrite homeassistant/#
# Dashboard / Home Assistant reads everything but only publishes to commands
user dashboard
topic read zigbee2mqtt/#
topic readwrite zigbee2mqtt/+/set
topic readwrite zigbee2mqtt/bridge/request/#
topic read homeassistant/#
# Read-only sensors data consumer
user sensors
topic read zigbee2mqtt/#Reload Mosquitto to apply password and ACL changes:
docker exec mosquitto kill -HUP 1Zigbee2MQTT Configuration
Edit /opt/mqtt/zigbee2mqtt/data/configuration.yaml. This config assumes you reach a remote coordinator via Tailscale at 100.64.10.5:6638:
homeassistant: true
permit_join: false
mqtt:
base_topic: zigbee2mqtt
server: 'mqtt://mosquitto:1883'
user: zigbee2mqtt
password: !secret mqtt_password
client_id: zigbee2mqtt-vps
serial:
port: 'tcp://100.64.10.5:6638'
adapter: zstack
advanced:
log_level: info
network_key: GENERATE
pan_id: GENERATE
ext_pan_id: GENERATE
channel: 25
frontend:
port: 8080
host: 0.0.0.0
auth_token: !secret frontend_token
availability: true
device_options:
legacy: false
retain: trueAdapter values for common coordinators:
zstackfor TI CC2652 and CC1352 (most SLZB-06 variants, Sonoff Dongle Plus)emberfor Silicon Labs EFR32 (Sonoff Dongle-E)deconzfor ConBee IIzbossfor Nordic nRF52840 with ZBOSS firmware
Now create the secrets file at /opt/mqtt/zigbee2mqtt/data/secret.yaml:
mqtt_password: 'STRONG_PASSWORD_HERE'
frontend_token: 'random-32-char-token-from-openssl-rand-hex-16'Generate a frontend token with openssl rand -hex 16.
Start both services:
docker compose up -d
docker compose logs -f zigbee2mqttWatch for Connected to MQTT server and Coordinator firmware version lines. If the coordinator connection fails, check the tunnel and confirm the coordinator's TCP port is reachable from the VPS with nc -vz 100.64.10.5 6638.
Reverse Proxy for the Frontend
Install Caddy for the Zigbee2MQTT web UI. Caddy handles ACME, automatic redirects, and basic auth in a few lines:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddyGenerate a bcrypt hash for HTTP basic auth:
caddy hash-passwordReplace /etc/caddy/Caddyfile:
z2m.example.com {
basic_auth {
admin BCRYPT_HASH_FROM_ABOVE
}
reverse_proxy 127.0.0.1:8081
encode gzip
log {
output file /var/log/caddy/z2m.log
}
}Reload Caddy:
sudo systemctl reload caddyCaddy will issue a Let's Encrypt cert automatically on first request. Confirm the A record for z2m.example.com resolves to your VPS before reloading.
Testing the Broker
From a workstation, use mosquitto_clients:
mosquitto_sub -h mqtt.example.com -p 8883 --capath /etc/ssl/certs -u sensors -P 'YET_ANOTHER_PASSWORD' -t 'zigbee2mqtt/#' -vIn another terminal, pair a device through the Zigbee2MQTT frontend. You should see device announcements appear immediately in the subscriber.
fail2ban for Mosquitto
Mosquitto logs failed auth attempts. Create /etc/fail2ban/filter.d/mosquitto.conf:
[Definition]
failregex = Client connection from <HOST> failed: not authorised\.
Socket error on client <HOST>, disconnecting\.
Bad socket read/write on client <HOST>: The connection was lost\.
ignoreregex =Configure /etc/fail2ban/jail.d/mosquitto.local:
[mosquitto]
enabled = true
port = 1883,8883
filter = mosquitto
backend = systemd
journalmatch = CONTAINER_NAME=mosquitto
maxretry = 5
findtime = 600
bantime = 86400Restart fail2ban:
sudo systemctl restart fail2ban
sudo fail2ban-client status mosquittoBackups
Three things matter for restore: the Mosquitto password and ACL files, the Zigbee2MQTT data directory (which holds the network key, device database, and configuration), and any custom blueprints.
Create /usr/local/sbin/mqtt-backup.sh:
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/var/backups/mqtt"
TS=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/mqtt-$TS.tar.gz" \
-C /opt/mqtt \
mosquitto/config \
zigbee2mqtt/data
find "$BACKUP_DIR" -name 'mqtt-*.tar.gz' -mtime +30 -deleteMake it executable and schedule it nightly:
sudo chmod +x /usr/local/sbin/mqtt-backup.sh
echo "0 3 * * * root /usr/local/sbin/mqtt-backup.sh" | sudo tee /etc/cron.d/mqtt-backupThe Zigbee2MQTT coordinator_backup.json file inside zigbee2mqtt/data is the only way to migrate to a new coordinator without re-pairing every device. Treat this file as critical and copy it off-server regularly.
For off-site backups, push the archive to S3-compatible storage with rclone or restic. Avoid storing the encrypted backup on the same VPS without redundancy.
Updates
cd /opt/mqtt
docker compose pull
docker compose up -d
docker image prune -fBefore updating Zigbee2MQTT, check the breaking changes section of the release notes. Major versions sometimes change configuration schema or drop adapter support.
Monitoring
Mosquitto exposes per-broker metrics on the $SYS/# topic tree. Subscribe to verify it's healthy:
mosquitto_sub -h mqtt.example.com -p 8883 --capath /etc/ssl/certs -u dashboard -P 'PASS' -t '$SYS/#' -vFor graphing, point Telegraf or mqtt-exporter at the broker and ship metrics to Prometheus or InfluxDB. The $SYS/broker/load/messages/sent/1min topic is a useful traffic gauge.
Zigbee2MQTT publishes health to zigbee2mqtt/bridge/state (online/offline) and per-device link quality on zigbee2mqtt/<device> payloads as linkquality. Mesh problems usually show up as repeated linkquality < 30 values, which is your cue to add a router or move the coordinator.
Common Issues
- Connection refused on TLS port: Check that the cert paths are mounted correctly into the container and that the
mosquittouser can read both files. - Coordinator disconnects every few hours: Often a network MTU issue between the VPS and home network. Drop the WireGuard MTU to 1380 and retest.
- Devices won't pair: Confirm
permit_join: trueis set in the frontend (do not leave it on permanently), and check that the device is in pairing mode within the join window. - ACL changes don't take effect: Send
SIGHUPto the broker withdocker exec mosquitto kill -HUP 1. A restart is not required.
This stack is now ready for production use. Add Home Assistant, Node-RED, or a custom application on top by creating dedicated MQTT users and ACL entries for each.
