Is
Streaming & Messaging Azure

IoT Stack (Node-RED, Mosquitto, InfluxDB, Grafana) on Ubuntu 24.04 on Azure User Guide

| Product: IoT Stack (Node-RED, Mosquitto, InfluxDB, Grafana) on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of the IoT Stack (Node-RED, Mosquitto, InfluxDB, Grafana) on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images.

Most IoT projects need the same four things, and assembling them is the boring part. This image is those four things already assembled and already wired together:

Tier Software Role
Ingress Eclipse Mosquitto 2.1 the MQTT broker your devices connect to and publish readings into
Processing Node-RED 5.0 a flow based, low code engine that parses, reshapes and routes each reading
Storage InfluxDB 2.9 a time series database that keeps every reading on a timeline
Presentation Grafana 13.2 OSS dashboards over the stored series

What makes it an appliance rather than four packages is that the path between them is already built. Node-RED ships with a flow that subscribes to the broker, converts incoming JSON readings into InfluxDB line protocol and writes them to the database, and Grafana ships with the InfluxDB data source and a starter telemetry dashboard already provisioned against the same bucket. Publish a reading from any MQTT client and it travels broker to flow to database to chart without you configuring a single connection.

From there the flow editor is where you extend the pipeline: add device specific parsing, alerting, filtering, or extra destinations, using the same drag and drop editor the shipped flow is built in.

The security model, in one paragraph

Each of these four projects ships with an insecure default, and this image closes all four before the virtual machine is ever reachable. Mosquitto ships with an empty password store, so with anonymous access disabled every connection is refused. InfluxDB ships un-onboarded, so its API rejects every call and there is no credential to steal. Node-RED and Grafana are held shut behind a boot gate and cannot start at all until one first boot routine has generated a fresh password, token and secret for each of the four services and written them to a file only root can read. There is no known or default credential anywhere in the image, and no two virtual machines share one. Only four ports are reachable from outside the machine; the database, the flow runtime and the dashboard server are all bound to the loopback interface behind a single TLS terminator whose certificate is also generated on your own machine at first boot.

Prerequisites

  • An Azure subscription with permission to create virtual machines, and the Azure CLI installed and signed in if you deploy from the command line.
  • An SSH key pair. Azure will use your public key for the azureuser account.
  • A client for testing. The examples use mosquitto_pub and mosquitto_sub (Debian and Ubuntu: apt-get install mosquitto-clients; macOS: brew install mosquitto), plus a browser.
  • Somewhere to keep secrets. First boot generates five credentials and prints them to a root only file; copy them into your password manager.

Step 1: Deploy from the Azure Portal

  1. In the Azure Portal choose Create a resource and search the Marketplace for IoT Stack (Node-RED, Mosquitto, InfluxDB, Grafana), published by cloudimg.
  2. Select the offer, then Create.
  3. On the Basics tab pick your subscription, resource group and region. Set the Size to Standard_B2ms or larger. This is a four service stack: 2 vCPUs and 8 GiB of memory is the recommended floor, and a 4 GiB size will run but leaves no headroom once real ingest starts.
  4. Set Authentication type to SSH public key, leave the username as azureuser, and paste your public key.
  5. Under Inbound port rules allow SSH (22) for now. Ports 443 and 8883 are opened in Step 3.
  6. Accept the defaults on the remaining tabs, then Review + create and Create.

Step 2: Deploy from the Azure CLI

Substitute your own resource group, region and virtual machine name.

az group create --name iot-stack-rg --location uksouth

az vm create \
  --resource-group iot-stack-rg \
  --name iot-stack-vm \
  --image cloudimg:nodered-mosquitto-influxdb-grafana:default:latest \
  --size Standard_B2ms \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

# Accept the marketplace terms once per subscription, if prompted:
az vm image terms accept --publisher cloudimg \
  --offer nodered-mosquitto-influxdb-grafana --plan default

Step 3: Open the ports your devices and dashboards need

The image itself ships no firewall rules. Open only what you need, and scope the source ranges to networks you control rather than leaving them open to the internet.

az network nsg rule create --resource-group iot-stack-rg --nsg-name iot-stack-vmNSG \
  --name allow-https --priority 1010 --destination-port-ranges 443 \
  --source-address-prefixes <your-mgmt-cidr> --protocol Tcp --access Allow

az network nsg rule create --resource-group iot-stack-rg --nsg-name iot-stack-vmNSG \
  --name allow-mqtt-tls --priority 1020 --destination-port-ranges 8883 \
  --source-address-prefixes <your-device-cidr> --protocol Tcp --access Allow

Port 80 serves only an unauthenticated /health endpoint and a redirect to HTTPS; you do not need to expose it unless something in your environment health checks over plain HTTP.

Step 4: Retrieve the credentials this virtual machine generated

First boot generates a separate secret for every tier and writes them to a root only file. Nothing is shared between virtual machines, and nothing was baked into the image.

sudo cat /root/nodered-mosquitto-influxdb-grafana-credentials.txt

The per VM credentials file shown over SSH, listing the Node-RED flow editor URL and admin user, the Grafana dashboard URL and admin user, the Mosquitto broker host, TLS port and broker user, and the InfluxDB URL, organisation and bucket, with every password and token masked and each marked as unique to this virtual machine

If that file still says placeholder, rotated at first boot, first boot has not finished yet. Watch it:

sudo journalctl -u iot-stack-firstboot.service --no-pager | tail -30

First boot does not simply create secrets and exit. It proves the whole pipeline before it declares success: it round trips a message through the broker on both the loopback and the TLS listener, confirms anonymous MQTT is refused, onboards InfluxDB, checks the Node-RED runtime answers, confirms Grafana rejects the upstream default password, and then publishes a reading and reads that exact point back out of InfluxDB. If the integration is not working, first boot fails loudly rather than leaving you a half wired appliance.

Step 5: Check the stack came up

systemctl is-active mosquitto influxdb node-red grafana-server nginx

All five should report active. You can also confirm the network posture, which is a deliberate part of this image:

echo "reachable from off-box:"
ss -tlnH | awk '{print $4}' | sed -E 's/%[A-Za-z0-9._-]+//' | grep -vE '^(127\.|\[::1\])' | sort -u
echo "loopback only:"
ss -tlnH | awk '{print $4}' | grep '^127\.0\.0\.1' | sort -u

Terminal output showing all five appliance services reporting active, the complete list of network facing listeners containing only ports 22, 80, 443 and 8883, the four internal services bound to the loopback interface on ports 1880, 1883, 3000 and 8086, and the unauthenticated health endpoint returning ok

Exactly four ports answer from outside the machine: 22 (SSH), 80 (health check and redirect), 443 (both web interfaces) and 8883 (MQTT over TLS). The flow runtime, the dashboard server, the database and the plaintext MQTT listener are all bound to 127.0.0.1 and cannot be reached from the network at all.

Step 6: Sign in to the Node-RED flow editor

Browse to https://<vm-ip>/ and sign in as admin with NODE_RED_ADMIN_PASSWORD from Step 4.

The certificate is self signed and generated on your own virtual machine, so your browser will warn on first visit. Step 13 covers replacing it with a certificate authority issued one.

The Node-RED sign in dialog served over HTTPS, showing the username and password fields of the editor's built in authentication, which is enabled by default in this image so the flow editor is never reachable unauthenticated

Once signed in you land on the IoT Pipeline flow. This is the wiring that makes the four packages one product.

The Node-RED editor showing the shipped IoT Pipeline flow, with an inject node feeding a function that builds a demo reading into an MQTT out node publishing to the local broker, and an MQTT in node subscribed to sensors hash showing a connected status and fanning out to a function that converts the reading to InfluxDB line protocol, an HTTP request node that writes it to InfluxDB, a file node appending to the inbox log, and a debug node

Reading the flow left to right and top to bottom:

  • Publish demo reading is an inject node. Click the button on its left edge to publish a synthetic reading through the real broker, which is the quickest way to see the pipeline move.
  • subscribe sensors/# receives everything published under the sensors/ topic tree, whatever the device.
  • to InfluxDB line protocol parses the JSON payload, turns each numeric field into an InfluxDB field and tags the point with the device name and topic.
  • write to InfluxDB posts that line protocol to the local database. Its write token comes from the systemd environment file, so no secret is stored inside the flow.
  • append to mqtt-inbox.log and received reading give you a file trail and a live view in the debug sidebar.

Step 7: Publish your first device reading over MQTT with TLS

Devices connect to port 8883 with the broker username and password from Step 4. From the virtual machine itself you can test the whole path immediately:

MQ=$(sudo grep '^MOSQUITTO_PASSWORD=' /root/nodered-mosquitto-influxdb-grafana-credentials.txt | cut -d= -f2-)
mosquitto_pub -h 127.0.0.1 -p 8883 --cafile /etc/cloudimg/tls/server.crt --insecure \
  -u cloudimg -P "$MQ" -t sensors/press-line-07 \
  -m '{"device":"press-line-07","temperature":74.2,"humidity":21.8}'
echo "published"

From a device or workstation elsewhere, the same call with your own address and the certificate copied off the machine:

mosquitto_pub -h <vm-ip> -p 8883 --cafile ./server.crt \
  -u cloudimg -P '<MOSQUITTO_PASSWORD>' -t sensors/press-line-07 \
  -m '{"device":"press-line-07","temperature":74.2,"humidity":21.8}'

The broker refuses anything unauthenticated. Both of these are rejected:

if mosquitto_pub -h 127.0.0.1 -t sensors/probe -m x >/dev/null 2>&1; then
  echo "UNEXPECTED: an anonymous publish was accepted"; exit 1
fi
echo "anonymous publish correctly refused"

if mosquitto_pub -h 127.0.0.1 -u cloudimg -P wrong-password -t sensors/probe -m x >/dev/null 2>&1; then
  echo "UNEXPECTED: a wrong password was accepted"; exit 1
fi
echo "wrong password correctly refused"

Terminal output showing a device reading published to the broker over TLS on port 8883 and accepted, then an anonymous publish and a wrong password publish both refused with connection refused not authorised, and finally the Node-RED flow's file sink showing the published JSON reading arriving in the pipeline

Confirm the flow received it:

sleep 3
sudo tail -3 /var/lib/node-red/mqtt-inbox.log

Any JSON object works. Every numeric field becomes an InfluxDB field, so {"device":"tank-3","level":88.5,"flow_rate":12.1} is stored with two fields tagged device=tank-3. A payload with no numeric fields is logged and skipped rather than written.

Step 8: Confirm the reading landed in InfluxDB

The flow writes into the iot bucket of the cloudimg organisation. Read it straight back:

IX=$(sudo grep '^INFLUXDB_ADMIN_TOKEN=' /root/nodered-mosquitto-influxdb-grafana-credentials.txt | cut -d= -f2-)
curl -fsS -XPOST 'http://127.0.0.1:8086/api/v2/query?org=cloudimg' \
  -H "Authorization: Token ${IX}" \
  -H 'Content-Type: application/vnd.flux' \
  -H 'Accept: application/csv' \
  --data-binary 'from(bucket:"iot") |> range(start:-15m) |> filter(fn:(r) => r._measurement == "sensor") |> keep(columns:["_time","_field","_value","device"])'

Terminal output showing a Flux query against the iot bucket returning the reading that was just published over MQTT, with separate rows for the humidity and temperature fields tagged with the device name, followed by an unauthenticated request to the same API being rejected with HTTP 401

The database is bound to the loopback interface, so that request only works from the machine itself. An unauthenticated one is refused even there:

curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:8086/api/v2/buckets?org=cloudimg'

Step 9: Open the Grafana dashboard

Browse to https://<vm-ip>/grafana/ and sign in as admin with GRAFANA_ADMIN_PASSWORD from Step 4. Anonymous access is disabled and the upstream admin / admin default is never valid on this image.

The Grafana sign in page served over HTTPS through the appliance's nginx front end, showing the email or username and password fields and identifying the build as Grafana v13.2.1 Open Source in the footer

Open Dashboards, then the cloudimg folder, then IoT Telemetry (cloudimg). The InfluxDB data source is already provisioned and pointed at the same bucket the flow writes to, so readings appear without any configuration.

The provisioned IoT Telemetry dashboard in Grafana showing live device data: stat panels reporting the number of readings ingested, the average temperature and humidity across devices and the count of reporting devices, above two time series panels charting temperature and humidity per device with separate labelled traces for three named devices

The dashboard is editable and is provisioned from a file, so your changes persist in Grafana while the shipped version stays on disk at /var/lib/grafana/dashboards/iot-telemetry.json as a reference to copy from.

Step 10: Connect a real device

Anything that speaks MQTT works: an ESP32 or ESP8266 with the PubSubClient library, a Raspberry Pi with Paho, an industrial gateway, or another Node-RED instance. Point it at:

  • Host: your virtual machine's address
  • Port: 8883
  • TLS: enabled, trusting /etc/cloudimg/tls/server.crt (copy it to the device, or replace the certificate as in Step 13 so devices trust it through a normal certificate authority)
  • Username: cloudimg
  • Password: MOSQUITTO_PASSWORD from Step 4
  • Topic: anything under sensors/, conventionally sensors/<device-name>

Copy the certificate to your workstation with:

sudo cp /etc/cloudimg/tls/server.crt /tmp/server.crt && sudo chmod 0644 /tmp/server.crt
echo "now fetch /tmp/server.crt with scp"

To give each device its own broker credential rather than sharing one, add users to the password file and restart the broker:

sudo mosquitto_passwd -b /etc/mosquitto/passwd press-line-07 'a-strong-per-device-password'
sudo systemctl restart mosquitto
sudo mosquitto_passwd -D /etc/mosquitto/passwd press-line-07
sudo systemctl restart mosquitto

The example above adds a device user and then removes it again so the machine is left as it was; drop the last two lines when you are provisioning for real. For per device topic restrictions, add an access control list with acl_file in /etc/mosquitto/conf.d/cloudimg.conf.

Step 11: Extend the pipeline in Node-RED

The shipped flow is a starting point, not a fixed appliance. Common next steps, all done in the editor:

  • Parse a different payload. Replace the to InfluxDB line protocol function with your own device format, for example a CSV or binary payload from an existing fleet.
  • Alert. Add a switch node after subscribe sensors/# and wire it to an email, Slack or webhook node when a value crosses a threshold.
  • Route somewhere else as well. MQTT in can feed as many outputs as you like; add a second HTTP request node to forward readings to another system.
  • Downsample. Add a delay node in rate limit mode, or use an InfluxDB task, if devices publish faster than you want to store.

Remember to click Deploy in the editor after changing a flow. The write token is available inside function nodes as env.get("INFLUX_TOKEN"), so you never need to paste a secret into a flow.

Step 12: Reach the InfluxDB user interface

InfluxDB has a good built in query and administration interface, but it is deliberately not exposed to the network. Reach it over an SSH tunnel from your workstation:

ssh -L 8086:127.0.0.1:8086 azureuser@<vm-ip>

With that running, browse to http://127.0.0.1:8086/ and sign in as admin with INFLUXDB_ADMIN_PASSWORD. This is where you manage retention, create scoped API tokens for other applications, and build Flux queries interactively.

Step 13: Replace the TLS certificate

The certificate generated at first boot is self signed and unique to this machine. For production, replace it with one from a certificate authority. With a real DNS name pointing at the machine:

sudo apt-get install -y certbot
sudo certbot certonly --standalone -d <your-domain> --agree-tos -m <your-email> --non-interactive
sudo cp /etc/letsencrypt/live/<your-domain>/fullchain.pem /etc/cloudimg/tls/server.crt
sudo cp /etc/letsencrypt/live/<your-domain>/privkey.pem /etc/cloudimg/tls/server.key
sudo install -o root -g mosquitto -m 0644 /etc/cloudimg/tls/server.crt /etc/mosquitto/certs/server.crt
sudo install -o root -g mosquitto -m 0640 /etc/cloudimg/tls/server.key /etc/mosquitto/certs/server.key
sudo systemctl reload nginx && sudo systemctl restart mosquitto

Both nginx and Mosquitto read from /etc/cloudimg/tls/, so replacing that pair covers the dashboards, the flow editor and the device listener. Note that a helper regenerates a self signed pair only when those files are absent, so your real certificate will not be overwritten.

Step 14: What is exposed, and what is not

Port Bound to Serves Authentication
22 all interfaces SSH your Azure SSH key
80 all interfaces /health and a redirect to HTTPS none, by design
443 all interfaces Node-RED at /, Grafana at /grafana/ per virtual machine passwords
8883 all interfaces MQTT over TLS broker username and password
1883 loopback plaintext MQTT for the local flow broker username and password
1880 loopback Node-RED runtime behind nginx editor authentication
3000 loopback Grafana behind nginx Grafana login
8086 loopback InfluxDB API and interface API token

The plaintext MQTT listener is loopback only on purpose: it is how Node-RED talks to the broker on the same machine, and exposing it would put device credentials on the wire in clear text. If you must expose it, change the bind address in /etc/mosquitto/conf.d/cloudimg.conf and open the port, but prefer 8883.

Step 15: Retention, disk and backups

InfluxDB stores its data under /var/lib/influxdb on the operating system disk. The shipped bucket has no retention limit, so it grows until you set one. Set a retention period from the InfluxDB interface (Step 12), or from the machine:

IX=$(sudo grep '^INFLUXDB_ADMIN_TOKEN=' /root/nodered-mosquitto-influxdb-grafana-credentials.txt | cut -d= -f2-)
influx bucket list --host http://127.0.0.1:8086 --org cloudimg --token "$IX"

To keep, for example, 90 days, use influx bucket update with --retention 90d and the bucket id from that listing.

For a high ingest deployment, move the database onto its own managed disk: attach and mount a data disk, stop influxdb, copy /var/lib/influxdb across, update bolt-path and engine-path in /etc/influxdb/config.toml, and start it again.

Back up by snapshotting the disk, or with the InfluxDB backup command:

IX=$(sudo grep '^INFLUXDB_ADMIN_TOKEN=' /root/nodered-mosquitto-influxdb-grafana-credentials.txt | cut -d= -f2-)
sudo mkdir -p /var/backups/influxdb
sudo influx backup /var/backups/influxdb/$(date +%F) --host http://127.0.0.1:8086 --org cloudimg --token "$IX"

Send that backup to durable storage rather than leaving it on the machine's own disk.

Your Node-RED flows live in /var/lib/node-red/flows.json and your Grafana dashboards in Grafana's own database at /var/lib/grafana/grafana.db; include both in any backup.

Step 16: Verify the deployment

A single pass that exercises every tier end to end:

CF=/root/nodered-mosquitto-influxdb-grafana-credentials.txt
MQ=$(sudo grep '^MOSQUITTO_PASSWORD=' $CF | cut -d= -f2-)
IX=$(sudo grep '^INFLUXDB_ADMIN_TOKEN=' $CF | cut -d= -f2-)
DEV=verify-$(date +%s)

systemctl is-active mosquitto influxdb node-red grafana-server nginx

mosquitto_pub -h 127.0.0.1 -p 8883 --cafile /etc/cloudimg/tls/server.crt --insecure \
  -u cloudimg -P "$MQ" -t "sensors/${DEV}" \
  -m "{\"device\":\"${DEV}\",\"temperature\":20.5,\"humidity\":50.0}"

sleep 5
curl -fsS -XPOST 'http://127.0.0.1:8086/api/v2/query?org=cloudimg' \
  -H "Authorization: Token ${IX}" \
  -H 'Content-Type: application/vnd.flux' \
  -H 'Accept: application/csv' \
  --data-binary "from(bucket:\"iot\") |> range(start:-10m) |> filter(fn:(r) => r.device == \"${DEV}\")" \
  | grep -q "${DEV}" && echo "END-TO-END OK: MQTT -> Node-RED -> InfluxDB"

curl -sk -o /dev/null -w 'grafana %{http_code}\n' https://127.0.0.1/grafana/login
curl -s -o /dev/null -w 'health %{http_code}\n' http://127.0.0.1/health

You should see five active lines, END-TO-END OK, and two 200 responses.

Baked versions and configuration

Component Version Source Licence
Eclipse Mosquitto 2.1.2 ppa:mosquitto-dev/mosquitto-ppa (the project's own Ubuntu channel) EPL-2.0 or EDL-1.0, redistributed under EDL-1.0
Node-RED 5.0.7 npm, global install Apache-2.0
Node.js 24 LTS NodeSource MIT
InfluxDB 2.9.1 repos.influxdata.com MIT
Grafana OSS 13.2.1 apt.grafana.com, the grafana package AGPL-3.0-only
nginx 1.24 (Ubuntu) Ubuntu 24.04 BSD-2-Clause

Key paths:

Path Contents
/root/nodered-mosquitto-influxdb-grafana-credentials.txt the five per virtual machine secrets
/etc/mosquitto/conf.d/cloudimg.conf broker listeners, authentication and TLS
/var/lib/node-red/flows.json the shipped IoT Pipeline flow
/etc/cloudimg/node-red.env the InfluxDB write token used by the flow
/etc/influxdb/config.toml database bind address and paths
/etc/grafana/provisioning/ the InfluxDB data source and dashboard provider
/var/lib/grafana/dashboards/iot-telemetry.json the shipped dashboard
/etc/cloudimg/tls/ the TLS certificate and key
/usr/local/sbin/iot-stack-firstboot.sh the first boot bootstrap

Licences and trademarks

This image bundles four independent open source projects, unmodified. Their licence texts ship in the image under /usr/share/cloudimg/licenses/nodered-mosquitto-influxdb-grafana/.

Node-RED is Apache-2.0. Eclipse Mosquitto is dual licensed EPL-2.0 or EDL-1.0 and is redistributed here under the EDL-1.0 arm. InfluxDB 2 is MIT. Grafana is AGPL-3.0-only; this image installs the open source grafana package and does not install Grafana Enterprise or any Enterprise plugin.

Node-RED is a registered trademark of the OpenJS Foundation. Eclipse Mosquitto is a trademark of the Eclipse Foundation AISBL. InfluxDB is a trademark of InfluxData, Inc. Grafana is a registered trademark of Grafana Labs. These names are used nominatively, to identify the unmodified open source software contained in this image. cloudimg is not affiliated with, and this image is not endorsed by, the OpenJS Foundation, the Eclipse Foundation, InfluxData, Inc. or Grafana Labs.

Security notes

  • No shared or default credential. Every secret is generated on your own virtual machine at first boot. The image ships an empty broker password store, an un-onboarded database, an unusable editor password hash and a Grafana password nobody holds.
  • The upstream defaults are actively refused. Anonymous MQTT is rejected, and Grafana's admin / admin does not work on this image.
  • Four ports, not eight. The database, flow runtime, dashboard server and plaintext broker listener are bound to loopback.
  • Per virtual machine TLS. The certificate and private key are generated on your machine; no two deployments share a key. Replace them with a certificate authority issued pair for production (Step 13).
  • No telemetry. Node-RED's update and telemetry prompt is answered off in the shipped settings, and Grafana's usage reporting and update checks are disabled, so the appliance does not phone home.
  • Scope your network rules. Restrict 443 to the networks that need the dashboards and 8883 to the networks your devices publish from.
  • Rotate the broker password and give each device its own credential (Step 10) before putting a fleet on it.
  • The base image is fully patched at build time and keeps unattended security upgrades enabled.

Support

This image is published and maintained by cloudimg with 24/7 support. For help with deployment or configuration, contact support through the cloudimg listing on the Azure Marketplace.