Monitor a VM fleet
A small VM fleet (under ~50 VMs) usually doesn't need a paid monitoring service. Run Prometheus + Grafana on one of the VMs, scrape node_exporter on each, and emit a metric per dimension that matters. This guide walks the setup end-to-end.
For larger fleets (~50+ VMs, or multi-region), you'll outgrow self-hosted Prometheus and want either a managed service (Grafana Cloud, Datadog) or a sharded Prometheus setup. The approach below scales fine up to that point.
What you'll set up
node_exporteron every fleet VM (CPU, RAM, disk, network)- A Prometheus VM scraping all of them
- A Grafana VM querying Prometheus
- 6 alerts that cover "the things you actually want to be paged for"
Total cost: ~$30/month for the Prometheus + Grafana VMs (two gp-1-2 instances in one region) plus the storage for metrics.
1 · Bring up the Prometheus VM
curl -X POST https://api.nimbusnexus.net/v1/vms \
-H "Authorization: Bearer $NIMBUS_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "monitoring-prometheus",
"size": "gp-1-2",
"region": "us-east-1",
"image": "ubuntu-24.04"
}'
SSH in and install Prometheus:
sudo apt update && sudo apt install -y prometheus
The default config at /etc/prometheus/prometheus.yml scrapes itself. We'll point it at our fleet next.
2 · Install node_exporter on every fleet VM
node_exporter is the canonical metrics collector for Linux — CPU, RAM, disk, network, load average. It listens on port 9100 by default.
# On every fleet VM
sudo apt install -y prometheus-node-exporter
sudo systemctl enable --now prometheus-node-exporter
For a fleet of more than a handful of VMs, use a VM image with node_exporter pre-baked, or run a one-shot Ansible / cloud-init script at boot. Manually SSH-ing to 20 VMs is the wrong long-term answer.
3 · Configure Prometheus to scrape the fleet
# /etc/prometheus/prometheus.yml
global:
scrape_interval: 30s
evaluation_interval: 30s
scrape_configs:
- job_name: 'fleet'
static_configs:
- targets:
- '10.0.1.10:9100'
- '10.0.1.11:9100'
- '10.0.1.12:9100'
# … one entry per VM
For dynamic fleets (autoscaling, frequent provisioning), use HTTP service discovery against the NimbusNexus API:
scrape_configs:
- job_name: 'fleet'
http_sd_configs:
- url: 'https://api.nimbusnexus.net/v1/vms/prometheus-targets'
authorization:
type: Bearer
credentials_file: /etc/prometheus/nimbus.key
This pulls the current list of VMs from the API every 60 seconds and re-syncs Prometheus's target list. New VMs get monitored automatically; deleted ones stop alerting.
Reload Prometheus:
sudo systemctl reload prometheus
4 · Bring up Grafana
curl -X POST https://api.nimbusnexus.net/v1/vms \
-H "Authorization: Bearer $NIMBUS_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "monitoring-grafana",
"size": "gp-1-2",
"region": "us-east-1",
"image": "ubuntu-24.04"
}'
# On the Grafana VM
sudo apt install -y software-properties-common
sudo add-apt-repository "deb https://packages.grafana.com/oss/deb stable main"
sudo apt install -y grafana
sudo systemctl enable --now grafana-server
Browse to http://<grafana-vm-ip>:3000. Default login admin/admin (change immediately). Add Prometheus as a datasource pointing at the Prometheus VM's internal IP, port 9090.
For dashboards: import dashboard ID 1860 ("Node Exporter Full") from Grafana's library. It covers every metric node_exporter emits, broken out per-instance and aggregated.
5 · The 6 alerts that matter
In /etc/prometheus/alerts.yml:
groups:
- name: fleet
rules:
# 1. VM unreachable
- alert: VMDown
expr: up{job="fleet"} == 0
for: 2m
labels: { severity: page }
annotations:
summary: "{{$labels.instance}} unreachable for 2 minutes"
# 2. High CPU sustained
- alert: HighCPU
expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
for: 10m
labels: { severity: warn }
annotations:
summary: "{{$labels.instance}} CPU > 90% for 10 minutes"
# 3. Memory pressure
- alert: HighMemory
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 90
for: 5m
labels: { severity: warn }
annotations:
summary: "{{$labels.instance}} memory > 90%"
# 4. Disk filling up
- alert: DiskAlmostFull
expr: (1 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})) * 100 > 85
for: 5m
labels: { severity: page }
annotations:
summary: "{{$labels.instance}} root disk > 85% — page now or it fills"
# 5. Disk-fill rate (catches "10 GB/hour log spike")
- alert: DiskFillingFast
expr: predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[1h], 4 * 3600) < 0
for: 10m
labels: { severity: warn }
annotations:
summary: "{{$labels.instance}} disk will be full in <4h at current rate"
# 6. Network errors
- alert: NetworkErrors
expr: rate(node_network_receive_errs_total[5m]) + rate(node_network_transmit_errs_total[5m]) > 10
for: 5m
labels: { severity: warn }
annotations:
summary: "{{$labels.instance}} network error rate > 10/s — bad NIC or driver"
Why these six:
- VMDown — the cheapest, most useful alert. Network partition, kernel panic, qemu crash; you want to know.
- HighCPU — sustained, not spiky. The 10-minute
for:filters out "one query ran hot" without missing "we're at the wall." - HighMemory — memory pressure precedes OOM kills. Catch it 10 minutes ahead.
- DiskAlmostFull — disk full = service down. 85% is the right threshold for paging because growth between 85 % and 100 % can be minutes during a log spike.
- DiskFillingFast — catches the "log spike" case before the threshold trips. Uses Prometheus's
predict_linearto extrapolate. - NetworkErrors — almost always a bad NIC or driver issue. Worth noticing before it cascades.
What's NOT in this list: load average (noisy, varies by workload), individual process metrics (use APM not Prometheus), business metrics (those go in their own job).
6 · Hook up notifications
Install Alertmanager on the Prometheus VM:
sudo apt install -y prometheus-alertmanager
Config at /etc/prometheus/alertmanager.yml:
route:
receiver: 'default'
group_by: ['alertname', 'instance']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receivers:
- name: 'default'
slack_configs:
- api_url: 'https://hooks.slack.com/services/T.../B.../...'
channel: '#alerts'
Slack incoming-webhook URL goes from your Slack workspace. For paging routes (PagerDuty, OpsGenie), Alertmanager has first-class config blocks for each.
What this isn't
- Not application performance monitoring. Prometheus is great for infrastructure metrics (CPU, RAM, disk); it's coarse for app-level latency tracking. Use OpenTelemetry + a backend (Honeycomb, Jaeger, Tempo) for that.
- Not log aggregation. Logs need a different stack (Loki, Elastic, etc.).
- Not a metrics service. This is a self-hosted setup; it has the drawbacks of self-hosting (you maintain the Prometheus VM, you scale it manually). Outgrow it when you outgrow it.
Next steps
- Virtual machines reference — the resource that emits all these metrics.
- Snapshots — back up the Prometheus VM's data volume so a year of history doesn't vanish in a single VM loss.
- Webhooks —
vm.state_changedevents are noisier than this monitoring but useful for "VM was rebooted by support" attribution.