A TIG stack with Docker Compose gives you a complete MQTT data pipeline on one machine: Telegraf subscribes to your broker, InfluxDB stores the readings and Grafana charts them. In this guide you will create every file needed, start the stack with one command, and open Grafana to a dashboard that is already provisioned and showing live data.
What You Will Build
The stack has three containers and one external dependency, the MQTT broker:
devices / simulator --> MQTT broker (broker.connectmq.com)
|
Telegraf (mqtt_consumer)
|
InfluxDB 2 (bucket "sensors")
|
Grafana (provisioned data source + dashboard)
The broker stays outside Compose on purpose. Brokers are long-lived infrastructure that many consumers share; the TIG stack is one of those consumers. Telegraf connects to the broker as an ordinary MQTT client, which works the same for the public test broker and for a private one.
Why Docker Compose for the TIG Stack
Each of the three tools can be installed natively, but doing so means three package repositories, three service definitions and configuration files scattered across the system. Compose puts the whole pipeline into one folder that you can version, copy to another machine or tear down without leaving traces. It also pins versions explicitly, so an operating system update never upgrades InfluxDB or Grafana behind your back.
The trade-off is that Compose is a single-host tool. It is a good fit for a development laptop, a lab, a small site gateway or an on-premises server that collects data from one plant. It does not give you high availability; if the host goes down, so does the stack. For many monitoring use cases that is acceptable, because devices keep publishing to the broker and Telegraf resumes when it comes back, and if you switch Telegraf to QoS 1 with persistent_session = true, the broker can hold its messages during a short outage.
Project Layout
Create this directory structure:
tig-mqtt/
.env
docker-compose.yml
telegraf/
telegraf.conf
grafana/
provisioning/
datasources/
influxdb.yml
dashboards/
dashboards.yml
dashboards/
mqtt-sensors.json
Six files in total. Each is shown in full below.
Step 1: Environment Variables
Secrets and names live in .env, which Docker Compose reads automatically. Keep it out of version control.
# .env
INFLUX_USERNAME=admin
INFLUX_PASSWORD=change-me-please
INFLUX_ORG=iot
INFLUX_BUCKET=sensors
INFLUX_TOKEN=replace-with-a-long-random-token
GRAFANA_PASSWORD=change-me-too
MQTT_SERVER=tcp://broker.connectmq.com:1883
MQTT_TOPIC_PREFIX=connectmq/demo/a1b2c3
MQTT_CLIENT_ID=telegraf-a1b2c3
MQTT_USERNAME=
MQTT_PASSWORD=
Generate a real token with openssl rand -hex 32. Replace a1b2c3 with your own random string so your topics stay separate on the public broker.
Step 2: The TIG Stack Docker Compose File
# docker-compose.yml
services:
influxdb:
image: influxdb:2.7
restart: unless-stopped
ports:
- "8086:8086"
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: ${INFLUX_USERNAME}
DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUX_PASSWORD}
DOCKER_INFLUXDB_INIT_ORG: ${INFLUX_ORG}
DOCKER_INFLUXDB_INIT_BUCKET: ${INFLUX_BUCKET}
DOCKER_INFLUXDB_INIT_RETENTION: 30d
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUX_TOKEN}
volumes:
- influxdb-data:/var/lib/influxdb2
- influxdb-config:/etc/influxdb2
healthcheck:
test: ["CMD", "influx", "ping"]
interval: 10s
timeout: 5s
retries: 12
telegraf:
image: telegraf:1.32
restart: unless-stopped
depends_on:
influxdb:
condition: service_healthy
environment:
INFLUX_TOKEN: ${INFLUX_TOKEN}
INFLUX_ORG: ${INFLUX_ORG}
INFLUX_BUCKET: ${INFLUX_BUCKET}
MQTT_SERVER: ${MQTT_SERVER}
MQTT_TOPIC_PREFIX: ${MQTT_TOPIC_PREFIX}
MQTT_CLIENT_ID: ${MQTT_CLIENT_ID}
MQTT_USERNAME: ${MQTT_USERNAME}
MQTT_PASSWORD: ${MQTT_PASSWORD}
volumes:
- ./telegraf/telegraf.conf:/etc/telegraf/telegraf.conf:ro
grafana:
image: grafana/grafana:11.3.0
restart: unless-stopped
depends_on:
- influxdb
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
INFLUX_TOKEN: ${INFLUX_TOKEN}
INFLUX_ORG: ${INFLUX_ORG}
INFLUX_BUCKET: ${INFLUX_BUCKET}
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
volumes:
influxdb-data:
influxdb-config:
grafana-data:
The health check matters. Without it, Telegraf can start before InfluxDB finishes its first-run setup, fail its first writes and fill the logs with errors. condition: service_healthy makes Compose wait until influx ping succeeds.
Step 3: Telegraf Configuration
Telegraf substitutes ${VAR} references from its environment, so the config file contains no secrets.
# telegraf/telegraf.conf
[agent]
interval = "10s"
flush_interval = "10s"
omit_hostname = true
[[inputs.mqtt_consumer]]
servers = ["${MQTT_SERVER}"]
topics = ["${MQTT_TOPIC_PREFIX}/+/+/telemetry"]
qos = 0
client_id = "${MQTT_CLIENT_ID}"
username = "${MQTT_USERNAME}"
password = "${MQTT_PASSWORD}"
name_override = "sensor"
topic_tag = ""
data_format = "json"
tag_keys = ["device"]
json_time_key = "ts"
json_time_format = "unix_ms"
[[inputs.mqtt_consumer.topic_parsing]]
topic = "${MQTT_TOPIC_PREFIX}/+/+/telemetry"
tags = "_/_/_/line/_/_"
[[outputs.influxdb_v2]]
urls = ["http://influxdb:8086"]
token = "${INFLUX_TOKEN}"
organization = "${INFLUX_ORG}"
bucket = "${INFLUX_BUCKET}"
Note the URL http://influxdb:8086: inside the Compose network, containers reach each other by service name, not localhost. The tags pattern assumes a three-level prefix like connectmq/demo/a1b2c3; if your prefix has a different depth, adjust the number of leading underscores. For a line-by-line explanation of these options, see the Telegraf MQTT consumer guide.
Step 4: Provision the Grafana Data Source
Provisioning creates the InfluxDB data source on startup, so nobody has to click through the UI. Grafana also expands environment variables in provisioning files.
# grafana/provisioning/datasources/influxdb.yml
apiVersion: 1
datasources:
- name: InfluxDB
uid: influxdb
type: influxdb
access: proxy
url: http://influxdb:8086
isDefault: true
jsonData:
version: Flux
organization: ${INFLUX_ORG}
defaultBucket: ${INFLUX_BUCKET}
secureJsonData:
token: ${INFLUX_TOKEN}
The fixed uid lets the dashboard reference this data source reliably.
Step 5: Provision a Ready-Made Dashboard
First tell Grafana where dashboards live:
# grafana/provisioning/dashboards/dashboards.yml
apiVersion: 1
providers:
- name: mqtt
folder: MQTT
type: file
options:
path: /var/lib/grafana/dashboards
Then add the dashboard itself. It has a temperature time series per device and a stat panel with the number of reporting devices.
{
"uid": "mqtt-sensors",
"title": "MQTT Sensors",
"schemaVersion": 39,
"refresh": "10s",
"time": { "from": "now-1h", "to": "now" },
"panels": [
{
"id": 1,
"type": "timeseries",
"title": "Temperature by device",
"gridPos": { "x": 0, "y": 0, "w": 18, "h": 10 },
"datasource": { "type": "influxdb", "uid": "influxdb" },
"fieldConfig": { "defaults": { "unit": "celsius" }, "overrides": [] },
"targets": [
{
"refId": "A",
"query": "from(bucket: \"sensors\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"sensor\" and r._field == \"temperature\")\n |> group(columns: [\"device\"])\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)"
}
]
},
{
"id": 2,
"type": "stat",
"title": "Reporting devices (last 5 min)",
"gridPos": { "x": 18, "y": 0, "w": 6, "h": 10 },
"datasource": { "type": "influxdb", "uid": "influxdb" },
"targets": [
{
"refId": "A",
"query": "from(bucket: \"sensors\")\n |> range(start: -5m)\n |> filter(fn: (r) => r._measurement == \"sensor\" and r._field == \"temperature\")\n |> group()\n |> distinct(column: \"device\")\n |> count()"
}
]
}
]
}
If you changed the bucket name in .env, change "sensors" in the two queries as well. You can also import this JSON through Dashboards, then New, then Import in any existing Grafana instance.
Step 6: Start the Stack and Send Data
cd tig-mqtt
docker compose up -d
docker compose ps
docker compose logs -f telegraf
Now publish data. The quickest continuous source is our Python MQTT device simulator; run it with --prefix connectmq/demo/a1b2c3 so it matches MQTT_TOPIC_PREFIX. A single test message also works:
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-a1b2c3 \
-t connectmq/demo/a1b2c3/line1/sensor-0001/telemetry \
-m "{\"device\":\"sensor-0001\",\"ts\":$(date +%s%3N),\"temperature\":22.4,\"humidity\":41.0}"
Open http://localhost:3000, log in as admin with GRAFANA_PASSWORD, and open the MQTT Sensors dashboard in the MQTT folder.
Verify Each Layer in Order
When the dashboard stays empty, resist the urge to change several things at once. Walk down the pipeline and confirm each layer before moving to the next:
- Broker. Run
mosquitto_sub -h broker.connectmq.com -p 1883 -i check-a1b2c3 -t "connectmq/demo/a1b2c3/#" -v. If nothing appears, the problem is the publisher, not the stack. - Telegraf connection.
docker compose logs telegrafshould show the plugin connecting without errors. Authentication failures and topic parsing errors appear here. - Telegraf parsing. Temporarily add an
[[outputs.file]]block withfiles = ["stdout"]and restart Telegraf. Parsed metrics then appear in the logs as line protocol, which proves the JSON and topic mapping are right. - InfluxDB. Run
docker compose exec influxdb influx query 'from(bucket: "sensors") |> range(start: -5m) |> limit(n: 3)'. The CLI inside the container is already configured by the init setup. - Grafana. Open the data source page and click Save and test, then check that the dashboard's time range covers the period when data was written.
In practice, most problems are found at step 1 or step 3: a topic prefix that differs by one character, or a payload that is not the JSON the config expects.
First-Run Pitfalls
| Problem | Cause | Fix |
|---|---|---|
Token from .env rejected |
InfluxDB init variables only apply to an empty volume | docker compose down -v to reset, or create a new token in the UI |
| Telegraf: connection refused to localhost:8086 | Wrong hostname inside containers | Use http://influxdb:8086 |
| Dashboard shows "No data" | Topic prefix in .env differs from what is published |
Compare with mosquitto_sub -t "connectmq/demo/a1b2c3/#" -v |
| Telegraf: topic parsing error | Prefix depth changed but tags pattern did not |
Match the number of _ segments to the topic levels |
| Grafana data source error | Token or org mismatch | Check the data source page; values come from .env |
Stack works, but data disappears after down -v |
Volumes deleted | Use docker compose down without -v |
Moving to a Private Broker
For real equipment, only .env changes. Point the stack at a fully managed MQTT service such as ConnectMQ:
MQTT_SERVER=ssl://broker.connectmq.com:8883
MQTT_TOPIC_PREFIX=connectmq/factory1/production
MQTT_CLIENT_ID=YOUR_CLIENT_ID
MQTT_USERNAME=YOUR_USERNAME
MQTT_PASSWORD=YOUR_PASSWORD
The certificate is issued by Let's Encrypt, so Telegraf trusts it without extra TLS settings. Run docker compose up -d again to recreate the Telegraf container with the new values. Also place Grafana behind a reverse proxy with HTTPS before exposing it beyond your machine, and back up the influxdb-data volume.
When the dashboard is in place, add Grafana alerts for IoT sensor data on the same queries. For broader client choices on the device side, see the MQTT client libraries guide, and the Docker Compose documentation covers health checks and environment files in detail.
Get Started with ConnectMQ
Use the public MQTT test broker to bring up the stack today with simulated data. For production, ConnectMQ runs a fully managed MQTT broker with TLS, MQTT 5 and a portal where you can watch topics live before Telegraf ever sees them. Create a free Serverless broker at app.connectmq.com with no credit card, then paste the credentials into .env. If you are weighing hosting options, read our guide to the cloud MQTT broker.
Frequently Asked Questions
Can I run the MQTT broker inside the same Docker Compose file?
You can, but a shared, managed broker is usually simpler to operate and lets other consumers use the same data. Keeping it outside Compose also means restarting the stack never interrupts your devices.
Does this TIG stack Docker Compose setup work with InfluxDB 3?
Not unchanged. InfluxDB 3 uses a different image, setup procedure and query languages, so the init variables and Flux queries here apply to InfluxDB 2.7.
How do I add more dashboards?
Drop additional dashboard JSON files into grafana/dashboards. Grafana picks them up automatically through the file provider.
How much disk space will the data use?
It depends on the number of series and the message rate. The bucket's 30-day retention keeps growth bounded; lower it or downsample older data if disk is tight.



