Grafana is an open-source visualization platform that turns time-series data into dashboards and alerts. For IoT projects it sits on top of a database such as InfluxDB and shows sensor readings as graphs, gauges and tables, and it can notify you when a value crosses a threshold. In this tutorial you will install Grafana on Ubuntu 24.04, connect it to an InfluxDB 2 bucket that already receives sensor data, build a dashboard with a per-device selector, and create an alert that emails you when a room gets too hot.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has sudo privileges and UFW enabled.
  • InfluxDB 2 on the same server with an organization called iot and a bucket called iot that receives sensor data. This guide uses the schema produced by a Telegraf MQTT pipeline: measurement environment, tag device, and the fields temperature, humidity and battery. Adjust the names in the queries if your data differs.
  • The influx CLI configured with an operator token (it is after running influx setup).
  • For email alerts: an SMTP account (host, port, username and password).

Step 1 - Installing Grafana

Install Grafana from Grafana Labs' official APT repository so you receive updates with apt. Add the signing key and the repository:

sudo apt update
sudo apt install apt-transport-https wget gnupg
sudo install -m 0755 -d /etc/apt/keyrings
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" \
  | sudo tee /etc/apt/sources.list.d/grafana.list

Install the open-source edition and start it:

sudo apt update
sudo apt install grafana
sudo systemctl enable --now grafana-server

Check that the service is running and listening on port 3000:

systemctl is-active grafana-server
sudo ss -tlnp | grep 3000
active
LISTEN 0  4096  *:3000  *:*  users:(("grafana",...))

Allow the port in the firewall:

sudo ufw allow 3000/tcp

Open http://your_server_ip:3000 in a browser and log in with admin / admin. Grafana asks you to set a new password immediately; choose a strong one.

Step 2 - Creating a read-only InfluxDB token

Grafana only needs to read data, so give it a token limited to reading the iot bucket. Look up the bucket ID:

influx bucket list --org iot --name iot

Create the token, replacing your_bucket_id with the ID from the output:

influx auth create --org iot --read-bucket your_bucket_id --description "grafana read"

Copy the value in the Token column. If you also downsample into a second bucket such as iot_hourly, add another --read-bucket flag with its ID.

Step 3 - Provisioning the InfluxDB data source

You can add data sources in the web interface, but a provisioning file keeps the configuration in version-controllable text and survives reinstalls. Create it:

sudo nano /etc/grafana/provisioning/datasources/influxdb-iot.yaml
apiVersion: 1

datasources:
  - name: InfluxDB-IoT
    type: influxdb
    access: proxy
    url: http://127.0.0.1:8086
    isDefault: true
    jsonData:
      version: Flux
      organization: iot
      defaultBucket: iot
    secureJsonData:
      token: your_grafana_read_token

The file contains a secret, so only root and the grafana group may read it:

sudo chown root:grafana /etc/grafana/provisioning/datasources/influxdb-iot.yaml
sudo chmod 0640 /etc/grafana/provisioning/datasources/influxdb-iot.yaml
sudo systemctl restart grafana-server

In Grafana, go to Connections > Data sources, open InfluxDB-IoT and click Test at the bottom of the page. A green message saying the data source is working, together with the number of buckets found, confirms that the URL and token are correct.

Step 4 - Adding a device variable

A dashboard variable lets one dashboard show any device from a drop-down, instead of building a copy per sensor. Create a new dashboard with Dashboards > New > New dashboard, open Settings > Variables and click Add variable:

  • Variable type: Query
  • Name: device
  • Data source: InfluxDB-IoT
  • Query:
import "influxdata/influxdb/schema"

schema.tagValues(bucket: "iot", tag: "device")
  • Multi-value: enabled
  • Include All option: enabled

The preview at the bottom lists the devices found in the bucket, for example bedroom, garage and livingroom. Save the dashboard. In the queries below, ${device:json} expands the selected values into a JSON array that Flux's contains() function can test against.

Step 5 - Building the sensor panels

Add each panel with Add > Visualization, paste the query into the query editor, pick the visualization type on the right and set the options listed. v.timeRangeStart, v.timeRangeStop and v.windowPeriod are filled in by Grafana from the dashboard's time picker and panel width, so the same query works for the last hour or the last month.

Temperature over time

Visualization: Time series. Unit: Temperature > Celsius (°C).

from(bucket: "iot")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r._measurement == "environment" and r._field == "temperature")
  |> filter(fn: (r) => contains(value: r.device, set: ${device:json}))
  |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
  |> keep(columns: ["_time", "_value", "device"])

aggregateWindow averages the raw points into one per pixel column, which keeps the panel fast over long ranges. You get one line per selected device.

Current humidity

Visualization: Gauge. Unit: Misc > Humidity (%H), Min 0, Max 100. Add thresholds, for example green from base, orange at 60 and red at 70.

from(bucket: "iot")
  |> range(start: -1h)
  |> filter(fn: (r) => r._measurement == "environment" and r._field == "humidity")
  |> filter(fn: (r) => contains(value: r.device, set: ${device:json}))
  |> last()
  |> keep(columns: ["_value", "device"])

The fixed -1h range shows the last value reported in the past hour regardless of the time picker; a gauge that goes empty means the device stopped reporting.

Battery levels

Visualization: Table. Under Overrides, set the _value field's unit to Percent (0-100) and its cell type to a colored background with thresholds red at base, orange at 20 and green at 50.

from(bucket: "iot")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "environment" and r._field == "battery")
  |> last()
  |> group()
  |> keep(columns: ["device", "_value", "_time"])
  |> sort(columns: ["_value"])

group() merges the per-device tables into one, so the panel shows a single table sorted from the emptiest battery upward. This panel ignores the device variable on purpose: battery status is most useful as a list of every device.

Save the dashboard and switch the device drop-down between values to check that the time series and gauge panels follow it.

Step 6 - Configuring SMTP for alert emails

Grafana needs an SMTP server to send email. Edit the configuration file:

sudo nano /etc/grafana/grafana.ini

Find the [smtp] section, which is commented out by default, and set these keys (remove the leading ; on each line you change):

[smtp]
enabled = true
host = smtp.your_provider.com:587
user = your_smtp_user
password = """your_smtp_password"""
from_address = grafana@your_domain
from_name = Grafana IoT

The triple quotes allow characters such as # and ; in the password, which Grafana would otherwise treat as comments. Restart Grafana:

sudo systemctl restart grafana-server

In the web interface, go to Alerting > Contact points, click Add contact point, choose Email, enter your address and click Test. A test email should arrive within a minute; if it does not, check sudo journalctl -u grafana-server -n 50 --no-pager for SMTP errors. Save the contact point.

Step 7 - Creating a temperature alert

Alert rules run on the Grafana server on a schedule, independently of whether anyone has the dashboard open. Go to Alerting > Alert rules > New alert rule and fill in:

  1. Name: High room temperature.
  2. Query A, data source InfluxDB-IoT:
from(bucket: "iot")
  |> range(start: -10m)
  |> filter(fn: (r) => r._measurement == "environment" and r._field == "temperature")
  |> aggregateWindow(every: 1m, fn: mean, createEmpty: false)
  1. Expressions: keep the default Reduce expression set to Last of A, and set the Threshold expression to fire when the reduced value is above 30. Because the query returns one series per device, Grafana creates one alert instance per device and includes the device label in notifications.
  2. Folder and Evaluation group: create a group such as iot with an evaluation interval of 1m.
  3. Pending period: 5m, so a single spike does not trigger an email.
  4. Contact point: the email contact point you created.

Save the rule. On the Alert rules page its state should be Normal. To test the whole chain, temporarily change the threshold to a value below the current temperature: after the pending period the state becomes Firing and the email arrives. Set the threshold back afterwards.

Step 8 - Sharing a read-only dashboard

To show the dashboard to someone without a Grafana account, open it, click Share and choose Share externally (called Public dashboard in older Grafana versions). Grafana generates a URL that renders the dashboard read-only without logging in.

Before enabling it, keep in mind that anyone with the link can see the data and that dashboards using template variables are shared with their default values only. For people who need to change filters, create Grafana users with the Viewer role instead.

Troubleshooting

The data source test fails with 401 Unauthorized. The token is wrong or cannot read the bucket. List tokens with influx auth list and check that the Grafana token has read permission on iot.

Panels say "No data". Widen the time range to confirm data exists, then run the same Flux query with influx query on the server. Watch for typos in measurement, field and tag names, which are case sensitive.

The device drop-down is empty. Run the variable query with influx query. If it returns nothing, the data has no device tag; check how Telegraf builds the tag.

Provisioned data source changes are not applied. Grafana reads provisioning files at startup. Restart grafana-server after editing them and check the journal for YAML errors.

Conclusion

Grafana now reads your sensor data from InfluxDB with a least-privilege token, shows it on a reusable dashboard with a device selector, and emails you when a room overheats. Next, put Grafana behind a TLS reverse proxy, add a dashboard that reads a downsampled bucket for long-term trends, and add Slack or Telegram contact points for faster notifications.