← Back to Blog

Monitoring with Grafana & Prometheus on Bare Metal – Custom Dashboards for Industrial Telemetry

A production-grade guide to building a complete monitoring stack with Prometheus, Grafana, cAdvisor, and Pushgateway – designed for industrial IoT, edge devices, and bare-metal servers.

10 min read Roman Swetly

Monitoring with Grafana & Prometheus on Bare Metal

Custom dashboards for industrial telemetry – a complete observability stack for edge devices and servers.


The Problem

In industrial IoT environments, you need visibility into:

  • Host health – CPU, RAM, disk, network (servers running 24/7).
  • Container performance – resource usage, restarts, uptime.
  • Device telemetry – sensors, gateways, edge nodes (ESP32, LoRa, etc.).
  • Application metrics – API response times, error rates, user sessions.

Traditional monitoring tools (like top, htop, df -h) are insufficient. You need a centralised, long-term observability platform that can:

  1. Collect metrics from multiple sources.
  2. Store them reliably (time-series database).
  3. Visualise them in real-time dashboards.
  4. Alert on anomalies.

Enter Prometheus + Grafana.


Architecture Overview

My monitoring stack is designed for flexibility and scalability:

My monitoring stack

Part 1: Choosing the Right Monitoring Approach

ApproachProsConsBest For
Pull (Exporter)Simpler, stateless, Prometheus-nativeRequires network access to exportersPermanent servers, cloud VMs
Push (Pushgateway)Works behind firewalls, batch jobs, IoTMore complex, gateway stores last value onlyEdge devices, ESP32, short-lived jobs
HybridBest of both worldsSlightly more complexIndustrial environments (recommended)

For industrial telemetry, I recommend the hybrid approach:

  • Pull from host exporters (Node Exporter, cAdvisor).
  • Push from IoT devices and short-lived jobs (Pushgateway).
  • Pull from Pushgateway into Prometheus.

Part 2: Deployment with Docker Compose

Here’s my production docker-compose.yml for the full monitoring stack:

version: '3.8'

services:
  # ─── Prometheus ─────────────────────────────────────────────────────────
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=30d'
      - '--web.enable-lifecycle'
    networks:
      - monitoring

  # ─── Pushgateway (for IoT / Edge Devices) ────────────────────────────
  pushgateway:
    image: prom/pushgateway:latest
    container_name: pushgateway
    restart: unless-stopped
    ports:
      - "9091:9091"
    networks:
      - monitoring

  # ─── Grafana (Visualisation) ──────────────────────────────────────────
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource
    volumes:
      - ./grafana_data:/var/lib/grafana
    networks:
      - monitoring

  # ─── cAdvisor (Container Metrics) ─────────────────────────────────────
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    networks:
      - monitoring

  # ─── Node Exporter (Host Metrics) ──────────────────────────────────────
  node-exporter:
    image: prom/node-exporter:latest
    container_name: node-exporter
    restart: unless-stopped
    ports:
      - "9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    networks:
      - monitoring

networks:
  monitoring:
    driver: bridge

What Each Component Does

ComponentPurposePort
PrometheusTime-series database, scrapes metrics9090
PushgatewayAccepts pushed metrics from IoT devices9091
GrafanaVisualisation dashboards3000
cAdvisorContainer resource metrics (CPU, RAM, I/O)8080
Node ExporterHost-level metrics (CPU, RAM, disk, network)9100

Part 3: Prometheus Configuration

prometheus.yml

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  # ─── Prometheus self-monitoring ──────────────────────────────────────
  - job_name: 'prometheus'
    static_configs:
      - targets: ['prometheus:9090']

  # ─── Node Exporter (host metrics) ────────────────────────────────────
  - job_name: 'node-exporter'
    static_configs:
      - targets:
          - 'node-exporter:9100'

  # ─── cAdvisor (container metrics) ────────────────────────────────────
  - job_name: 'cadvisor'
    static_configs:
      - targets:
          - 'cadvisor:8080'

  # ─── Pushgateway (IoT / pushed metrics) ─────────────────────────────
  - job_name: 'pushgateway'
    honor_labels: true  # Important: preserve device labels
    static_configs:
      - targets:
          - 'pushgateway:9091'

Why honor_labels: true?

This tells Prometheus to keep the labels set by the pushing device (e.g., device="dev1") rather than overwriting them. This is critical for identifying individual IoT sensors.


Part 4: Pushing Metrics from IoT Devices (Node.js Example)

For devices that cannot be scraped (e.g., behind NAT, firewalls, or short-lived batch jobs), use Pushgateway.

Architecture

IoT Device / Edge Node ── HTTP PUT ──► Pushgateway ──► Prometheus (scrapes)

Node.js Example – Temperature & Humidity Pusher

Folder structure:

pusher/
├── app.js
├── package.json
└── Dockerfile

package.json:

{
  "name": "iot-pusher",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "axios": "^1.7.4",
    "prom-client": "^15.1.0"
  }
}

app.js:

import client from "prom-client";
import axios from "axios";

const gatewayUrl = "http://pushgateway:9091/metrics/job/sensor_push";

// Simulated IoT devices
const devices = ["dev1", "dev2", "dev3"];

// Create registry
const register = new client.Registry();

// Define metrics
const tempGauge = new client.Gauge({
  name: "sensor_temperature_celsius",
  help: "Temperature of sensor",
  labelNames: ["device", "location"]
});

const humGauge = new client.Gauge({
  name: "sensor_humidity_percent",
  help: "Humidity of sensor",
  labelNames: ["device", "location"]
});

register.registerMetric(tempGauge);
register.registerMetric(humGauge);

async function pushMetrics() {
  devices.forEach(dev => {
    // Simulate real sensor data
    const temp = +(20 + Math.random() * 10).toFixed(2);
    const hum = +(40 + Math.random() * 20).toFixed(2);

    tempGauge.set({ device: dev, location: "factory_floor" }, temp);
    humGauge.set({ device: dev, location: "factory_floor" }, hum);

    console.log(`📡 ${dev}: Temp=${temp}°C  Hum=${hum}%`);
  });

  try {
    await axios.post(gatewayUrl, await register.metrics(), {
      headers: { "Content-Type": register.contentType }
    });
    console.log("✅ Pushed to Pushgateway\n");
  } catch (err) {
    console.error("❌ Push failed:", err.message);
  }
}

// Push every 10 seconds
setInterval(pushMetrics, 10000);

// Initial push
pushMetrics();

Dockerfile:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY app.js ./
CMD ["node", "app.js"]

Testing the Push

# Check Pushgateway UI
curl http://localhost:9091

# View pushed metrics
curl http://localhost:9091/metrics | grep sensor

Part 5: Push vs Pull – Comparison

FeaturePull (Exporter)Push (Pushgateway)
Network requirementsScraper must reach targetTarget must reach gateway (outbound)
Firewall/NAT❌ Problematic✅ Works easily
IoT / Edge devices❌ Not suitable✅ Perfect fit
Short-lived jobs❌ Metrics disappear✅ Persisted until next push
Batch processing❌ Not suitable✅ Great for cron jobs
State persistence✅ Full history in Prometheus❌ Gateway only keeps last value
Complexity✅ Simple setup⚠️ Slightly more complex

My recommendation: Use pull for permanent servers, push for IoT/edge devices, and hybrid for industrial environments.


Part 6: cAdvisor – Container Metrics Made Easy

cAdvisor (Container Advisor) by Google automatically discovers containers and exposes resource usage metrics.

Adding cAdvisor to Your Stack

# Add to docker-compose.yml
cadvisor:
  image: gcr.io/cadvisor/cadvisor:latest
  container_name: cadvisor
  ports:
    - "8080:8080"
  volumes:
    - /:/rootfs:ro
    - /var/run:/var/run:ro
    - /sys:/sys:ro
    - /var/lib/docker/:/var/lib/docker:ro
  restart: unless-stopped

Metrics You Get

  • CPU usage per container (user/system/total).
  • Memory usage (RSS, cache, swap).
  • Network I/O (bytes in/out, packets).
  • Disk I/O (read/write bytes, operations).
  • Container restarts and uptime.

View cAdvisor UI

# Open web UI
http://localhost:8080

# View Prometheus metrics endpoint
http://localhost:8080/metrics

Integrate with Prometheus

# prometheus.yml
scrape_configs:
  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']

Now you can query container metrics in PromQL:

# CPU usage per container
container_cpu_usage_seconds_total{container_label_com_docker_compose_service="nextcloud"}

# Memory usage per container
container_memory_working_set_bytes{container_label_com_docker_compose_service="matrix"}

Part 7: Grafana Dashboards

Prebuilt Dashboards with Dockprom

If you want a fast start, use Dockprom – a prebuilt monitoring stack:

git clone https://github.com/stefanprodan/dockprom.git
cd dockprom
docker-compose up -d

Then access:

  • Grafana: http://localhost:3000 (admin/admin)
  • Prometheus: http://localhost:9090
  • cAdvisor: http://localhost:8080

Custom Dashboard – Industrial Telemetry

Here are some key PromQL queries for industrial dashboards:

Host Metrics

# CPU usage (percent)
100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# Memory usage (percent)
(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100

# Disk usage (percent)
(1 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})) * 100

Container Metrics

# Container CPU usage
rate(container_cpu_usage_seconds_total{container_label_com_docker_compose_service="nextcloud"}[5m])

# Container memory usage
container_memory_working_set_bytes{container_label_com_docker_compose_service="nextcloud"}

# Container restarts
container_start_time_seconds{container_label_com_docker_compose_service="nextcloud"}

IoT Sensor Metrics

# Temperature for specific device
sensor_temperature_celsius{device="dev1"}

# Average temperature across all devices
avg(sensor_temperature_celsius)

# Humidity trend (1-hour average)
avg_over_time(sensor_humidity_percent[1h])

Sample Dashboard Layout

Sample Dashboard Layout

Part 8: Security Considerations

Exposing Services

In my production setup, I restrict access:

# In docker-compose.yml – only expose ports on internal network
prometheus:
  ports:
    - "127.0.0.1:9090:9090"  # Only localhost

grafana:
  ports:
    - "4008:3000"  # Traefik exposes Grafana publicly

Traefik Configuration

I route Grafana through Traefik for HTTPS and authentication:

# dynamic/grafana.yml
http:
  routers:
    https-grafana-router:
      entryPoints:
        - https
      service: https_grafana_srvs
      rule: Host(`monitoring.example.com`)
      tls:
        certResolver: letsEncrypt

  services:
    https_grafana_srvs:
      loadBalancer:
        servers:
          - url: http://99.99.99.99:4008/
        passHostHeader: true

Part 9: Real-World Deployment

This monitoring stack is running on my production infrastructure:

Current Metrics

# Prometheus query: total scraped targets
count(scrape_samples_scraped)

# Result: 47 active targets
ComponentCountStatus
Hosts monitored3 bare-metal servers✅ Healthy
Containers monitored42✅ Healthy
IoT devices pushing12 (ESP32, LoRa)✅ Healthy
Grafana dashboards8 production dashboards✅ Active
Active alerts6 (disk space, container restarts)✅ Working

Uptime

  • Prometheus: 99.95% uptime over 6 months.
  • Grafana: 99.90% uptime over 6 months.
  • cAdvisor: 100% uptime (restartless).
  • Pushgateway: 100% uptime.

Summary Checklist

TaskStatusNotes
Prometheus deployedTime-series DB running on port 9090
Pushgateway deployedAccepting IoT metrics on port 9091
Grafana deployedVisualisation on port 3000
cAdvisor deployedContainer metrics on port 8080
Node Exporter deployedHost metrics on port 9100
IoT push scriptNode.js pushing to Pushgateway every 10s
Dashboards configured8 production dashboards
Alerts configuredDisk space, container restarts, IoT device timeouts

Final Thoughts

Observability is not optional for industrial IoT.

When your servers run 24/7, when your ESP32s are deployed in remote locations, when your customers depend on your uptime – you need visibility into every layer of the stack.

This Prometheus + Grafana stack gives me:

  • Real-time visibility – everything from host metrics to IoT sensor data in one place.
  • Historical context – 30-day retention means I can spot trends and anomalies.
  • Alerting – I know about problems before my customers do.
  • Customisation – I can build exactly the dashboards I need.

The best part? It’s 100% open-source, self-hosted, and under my control.


This monitoring stack is part of my Industrial Infrastructure Platform (II-PaaS). For a detailed architecture walkthrough or custom dashboard design, feel free to reach out.

← Previous Post
Dockerising Your IoT Stack
Next Post →
Building a Sovereign Nextcloud & Matrix Cluster – Replacing Google Workspace with 100% Self-hosted Infrastructure

Related Articles