
A four-VM smart-home stack: Home Assistant + Mosquitto + Zigbee2MQTT + Node-RED, from one prompt
May 11, 2026
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.
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.configuration.yaml, mosquitto.conf, and zigbee2mqtt's YAML all live in bootable images, not in a recovered backup tarball./dev/ttyUSB0; the host hypervisor maps the device through.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.
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.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.
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:
/dev/ttyUSB0; you need to pass a CC2652, EFR32MG21, or ConBee II through to the VM.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.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.
OpenFactory's free flow is for browsing. Persistent VMs, SSH access, snapshots, your own ISO, and fleet deployment live on a paid plan.