OpenFactory Home Assistant lab with Mosquitto MQTT broker, Zigbee2MQTT gateway and Node-RED automation

Build a Home Assistant Smart-Home Lab on OpenFactory

A four-VM smart-home stack: Home Assistant + Mosquitto + Zigbee2MQTT + Node-RED, from one prompt

May 11, 2026

← Back to Blog

Home Assistant is the only vendor-neutral smart-home hub that actually works at scale: it speaks every protocol your devices speak, keeps automations local, and survives the moment a cloud provider sunsets your hub or your light bulbs. It's the third-most-deployed self-hosted app in the 2026 r/selfhosted survey, after Jellyfin and Immich.

This post walks through the canonical Home Assistant lab shape on OpenFactory: four buildable VMs — Home Assistant core, Mosquitto as the MQTT broker, Zigbee2MQTT as the Zigbee gateway, and Node-RED for visual automation — from a single prompt, shipped as bootable ISOs.

What you'll build

  • homeassistant (10.72.0.10:8123) — the central hub with the MQTT integration pre-pointed at the broker.
  • mosquitto (10.72.0.20:1883) — the MQTT broker with persistence enabled at /var/lib/mosquitto.
  • zigbee2mqtt (10.72.0.30:8080) — the bridge from a USB Zigbee coordinator to MQTT, with the frontend ready on :8080.
  • nodered (10.72.0.40:1880) — visual automation engine wired to the same MQTT broker.

Why build it on OpenFactory

  • The ISO is the spec. configuration.yaml, mosquitto.conf, and zigbee2mqtt's YAML all live in bootable images, not in a recovered backup tarball.
  • Each role is its own VM. Update Zigbee2MQTT without touching Home Assistant. Reboot Mosquitto without losing your dashboards.
  • USB coordinator passthrough is one config. Zigbee2MQTT's YAML points at /dev/ttyUSB0; the host hypervisor maps the device through.
  • Scenario assertions ride along. The build fails closed if HA can't reach the broker, if Node-RED's flow endpoint doesn't respond, or if any of the four ports stops listening.

Topology

Four Debian Trixie VMs on 10.72.0.0/24. Mosquitto is the message-bus hub; Home Assistant, Node-RED, and Zigbee2MQTT all connect to it on 1883.

The prompt

Paste this verbatim into the chat builder at console.openfactory.tech. Nothing above or below it — the builder expects the prompt body to start at the “Build a compact multi-node lab…” line.

Build a compact multi-node lab named `home-assistant-smarthome`.

Output discipline: keep the plan small. Use one startup script per node, about 25 shell lines or less. Do not install Home Assistant Core, Mosquitto, Zigbee2MQTT, Node-RED, Python integrations, or USB Zigbee/Zwave coordinators at build time. Write deployment-time config examples and tiny Python stdlib or shell compatibility stubs only. The goal is a buildable preparation lab, not a production deployment.

## Topology

Create 4 buildable `debian-trixie` nodes, all `x86_64`, SSH enabled, DHCP/default route intact with lab aliases, firewall disabled, DNS `1.1.1.1` and `8.8.8.8`, user `ops` password `ha-ops` in `sudo`. Every recipe must set top-level `test_config` to `{ "enabled": false, "tests": [] }`.

- `homeassistant`: role `smarthome-hub`, 4 GB RAM, 24 GB disk, alias `10.72.0.10/24`, x `230`, y `60`
- `mosquitto`: role `mqtt-broker`, 1 GB RAM, 8 GB disk, alias `10.72.0.20/24`, x `110`, y `220`
- `zigbee2mqtt`: role `zigbee-gateway`, 1 GB RAM, 8 GB disk, alias `10.72.0.30/24`, x `350`, y `220`
- `nodered`: role `automation`, 2 GB RAM, 12 GB disk, alias `10.72.0.40/24`, x `230`, y `380`

Connections: `homeassistant` to `mosquitto:1883` and `nodered:1880`; `zigbee2mqtt` to `mosquitto:1883`; `nodered` to `mosquitto:1883`.

## Common Recipe Requirements

All nodes: features `headless`, `ssh`; packages `openssh-server`, `python3`, `curl`, `jq`, `iproute2`, `netcat-openbsd`, `ca-certificates`. Each startup script adds the alias with `IFACE=$(ip route show default | awk '{print $5; exit}')`, `ip link set "$IFACE" up || true`, and `ip addr add <alias> dev "$IFACE" || true`. If `os.startup_scripts[].after` is present, it must be the string `"network-online.target"`, not an array.

## Node Requirements

`homeassistant`: features `headless`, `ssh`. Write `/etc/homeassistant/configuration.yaml` containing:
```
homeassistant:
  name: lab-home
  time_zone: UTC
http:
  server_port: 8123
mqtt:
  broker: 10.72.0.20
  port: 1883
```
Create `/var/lib/homeassistant/{config,deps,www}` mode `0750 ops:ops`. Add a Python stdlib HTTP service on `0.0.0.0:8123` exposing:
- `GET /api/` -> `200 {"message":"API running."}`
- `GET /api/config` -> `200 {"version":"compat-1.0","location_name":"lab-home","time_zone":"UTC"}`
- `GET /metrics` -> `homeassistant_compat_up 1`
Register `homeassistant-compat.service`.

`mosquitto`: features `headless`, `ssh`. Write `/etc/mosquitto/conf.d/lab.conf` with `listener 1883 0.0.0.0`, `allow_anonymous true`, `persistence true`, `persistence_location /var/lib/mosquitto/`. Create `/var/lib/mosquitto/` mode `0750 ops:ops`. Add a Python stdlib stub listener on `0.0.0.0:1883` accepting connections and a `:9234/metrics` listener with `mosquitto_compat_up 1`. Register `mosquitto-compat.service`.

`zigbee2mqtt`: features `headless`, `ssh`. Write `/etc/zigbee2mqtt/configuration.yaml`:
```
homeassistant: true
mqtt:
  base_topic: zigbee2mqtt
  server: mqtt://10.72.0.20:1883
serial:
  port: /dev/ttyUSB0
frontend:
  port: 8080
```
Add a Python stdlib service on `0.0.0.0:8080` exposing `GET /` -> `200 {"status":"compat"}`, `GET /api/info` -> `200 {"version":"compat-1.0","coordinator":{"type":"stub"}}`, `GET /metrics` with `zigbee2mqtt_compat_up 1`. Register `zigbee2mqtt-compat.service`. Write `/root/zigbee2mqtt-notes.md` warning that real deployments need a passthrough USB Zigbee coordinator (CC2652 / EFR32MG21 / ConBee II).

`nodered`: features `headless`, `ssh`. Write `/etc/nodered/settings.js` snippet with `uiPort: 1880` and `flowFile: '/var/lib/nodered/flows.json'`. Add a Python stdlib service on `0.0.0.0:1880` exposing `GET /` -> `200 {"status":"compat"}`, `GET /flows` -> `200 []`, `GET /metrics` with `nodered_compat_up 1`. Register `nodered-compat.service`.

## Scenario

Emit exactly one group scenario named `home-assistant-smarthome-validation`. Put `custom_tests[].assertions[]` inside the scenario entry; leave `scenarios[].tests` empty. Every assertion needs `on_vm`. Use only `port_listening`, `command_output`, and `http_responds`; do not emit `vm_boots`, `network_reachable`, or `service_running`.

- `Stack ports listen`: `port_listening` for `homeassistant:8123`, `mosquitto:1883`, `mosquitto:9234`, `zigbee2mqtt:8080`, `nodered:1880`.
- `HA API responds`: on `homeassistant`, `curl -fsS http://localhost:8123/api/ | jq -e '.message == "API running."' >/dev/null && echo ha-ok`.
- `HA reaches MQTT broker`: on `homeassistant`, `nc -z -w 5 10.72.0.20 1883 && echo mqtt-reachable`.
- `Zigbee2MQTT info`: on `zigbee2mqtt`, `curl -fsS http://localhost:8080/api/info | jq -e '.coordinator.type == "stub"' >/dev/null && echo z2m-ok`.
- `Node-RED flows endpoint`: on `nodered`, `curl -fsS http://localhost:1880/flows | jq -e 'type == "array"' >/dev/null && echo nodered-ok`.

Preserve warnings that real Home Assistant Core or HAOS installation, USB Zigbee/Zwave coordinator passthrough, Mosquitto user/ACL setup, TLS for MQTT, persistent volume strategy for `/config`, integration credential storage, automation backup, and `10.72.0.0/24` aliasing are deployment-time concerns.

Running it

  1. Open the chat builder at console.openfactory.tech and paste the prompt into a new conversation.
  2. Review the streamed build plan. You'll see the topology, per-node recipes, and the scenario assertions that will run after boot. Edit the prompt and re-run if anything is off.
  3. Click Build group. OpenFactory fans the plan out to per-node ISO builds. When every ISO reaches built, boot the group on the runner network from the same UI.
  4. Exercise the stack. The scenario assertions run automatically against the live VMs. From the host you can also hit the service ports directly to confirm end-to-end behavior.

Driving OpenFactory from an AI agent instead of the browser? The same flow is exposed through the OpenFactory MCP server — submit the prompt programmatically, get the build-plan preview back, and call create_build / start_vm on the resulting recipes. Single-image builds go straight through the openfactory CLI.

What's still your responsibility

The prompt produces a buildable preparation lab — the right topology, the right ports listening, deployment-time config templates dropped in the right places, and tiny compatibility services that prove the wiring works. A few things still sit outside the recipe and need operator attention before this carries real load:

  • Real Home Assistant Core. Replace the stdlib compatibility service with the upstream Home Assistant container or HAOS install. The config file is already in place.
  • USB Zigbee coordinator. The Zigbee2MQTT YAML points at /dev/ttyUSB0; you need to pass a CC2652, EFR32MG21, or ConBee II through to the VM.
  • MQTT auth. allow_anonymous true is fine in a lab, not on a network where guests can talk to the broker. Add mosquitto_passwd users and ACLs.
  • Backup of /config. Home Assistant state, dashboards, and automations all live here. Off-host snapshots are mandatory.
  • Integration credentials. Cloud APIs (Spotify, weather, calendar), local device tokens — all out of scope of the recipe by design.

Where to go next

Smart-home traffic deserves a private DNS layer to keep cloud trackers off your network. See the Pi-hole DNS cluster post. For verifying the kernel under the smart-home stack, read the runtime attestation post. And the Enterprise & GxP page covers larger rollouts.

Ready to ship this in production?

OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.