The De Facto Standard for Server Monitoring
Prometheus + Grafana is the default pairing for modern server monitoring: Prometheus collects and stores time-series metrics, Grafana turns them into dashboards you can actually read, Node Exporter exposes CPU, memory, disk, and network data as scrapeable metrics, and Alertmanager fires alerts when metrics cross their thresholds. Each piece has a clear job; together they form a monitoring stack you fully control — no per-host SaaS billing, and the data stays in your hands.
It fits a single VPS as well as a cluster of dozens of machines. If you're starting from scratch, get the stack running on one host with Docker Compose first, then wire in business metrics step by step. Compared with SaaS monitoring, the main difference is operational cost — you manage container updates, backups, and alert channels yourself; in return you get no limits, controllable data, and deep customizability. If you only need a simple "is the site online" probe, Uptime Kuma gets you there faster. But once you need CPU trends, frontend/backend latency comparisons, or complex alerting, the Prometheus + Grafana pairing is hard to replace.
Architecture
Metrics → Node Exporter → Prometheus → Grafana
↓
Alertmanager → Notifications
Docker Compose Deployment
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
ports:
- "127.0.0.1:9090:9090"
restart: unless-stopped
grafana:
image: grafana/grafana:latest
volumes:
- grafana-data:/var/lib/grafana
ports:
- "127.0.0.1:3000:3000"
restart: unless-stopped
node-exporter:
image: prom/node-exporter:latest
ports:
- "127.0.0.1:9100:9100"
restart: unless-stopped
volumes:
prometheus-data:
grafana-data:
Prometheus Configuration
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
Grafana Setup
- Open
http://your-server:3000 - Default login:
admin/admin - Add a Prometheus data source:
http://prometheus:9090 - Import dashboard ID
1860(Node Exporter Full)
Verification & Troubleshooting
Once deployed, confirm the pipeline works:
- Open
http://your-server:9090/targetsin a browser and confirm node-exporter shows State UP; - In the Prometheus Graph page, run
up{job="node"}— the result should be 1; - Open
http://your-server:3000, log in withadmin/admin, and change the password after first login; - When adding the data source, use
http://prometheus:9090(containers talk over the Compose network).
Common gotchas: Prometheus is sensitive to memory rather than swap — if the container OOM-loops, check system memory headroom; and bind 9090/9100/3000 to 127.0.0.1 so monitoring ports are never exposed to the public internet.
Useful PromQL Queries
# CPU usage
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# Memory usage
100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
# Disk usage
100 * (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes)
# Network traffic
rate(node_network_receive_bytes_total[5m])
rate(node_network_transmit_bytes_total[5m])
Reading the Metrics
node_cpu_seconds_total{mode="idle"}: cumulative idle CPU time; pair withrate()to compute usage;node_memory_MemAvailable_bytes: available memory (including reclaimable cache), closer to reality than MemFree;node_filesystem_avail_bytes: free space per mount — remember to exclude pseudo-filesystems liketmpfs;rate(node_network_receive_bytes_total[5m]): 5-minute average ingress rate in bytes/sec;node_load1: 1-minute average load — on multi-core machines, load only means real overload when it exceeds the core count.
Watch out for label cardinality: never stuff high-cardinality labels like user IDs or URLs into custom metrics, or Prometheus memory will balloon quickly. For alert rule design, see Prometheus alert rule design.
Alert Rules
# alert-rules.yml
groups:
- name: server
rules:
- alert: HighCPUUsage
expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: warning
annotations:
summary: "CPU usage > 80% on {{ $labels.instance }}"
- alert: DiskFull
expr: 100 * (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) > 85
for: 5m
labels:
severity: warning
annotations:
summary: "Disk > 85% on {{ $labels.instance }}"
Recommended Grafana Dashboards
| ID | Name |
|---|---|
| 1860 | Node Exporter Full |
| 11074 | Server Monitoring |
| 11772 | Docker Monitoring |
| 13659 | Nginx Performance |
Best Practices
- The default 15s scrape interval is fine — don't drop it to 1s for a "real-time" feel; storage and memory double as a result;
- Metrics retention defaults to 15 days; raise
--storage.tsdb.retention.timeif you need a longer history; - Server clocks must be NTP-synced; Prometheus is very sensitive to time offsets, and clock drift directly pollutes alert evaluation;
- Manage charts and alerts separately: charts are for humans, while alert rules should be kept lean following alert-fatigue practices;
- On first setup, run on a test box for a week and confirm metrics and alerts behave as expected before pointing production at it.
A Worked Example
Say you want to monitor a blog server running Nginx + PHP. A minimal working setup starts like this: Node Exporter collects system metrics, blackbox_exporter probes the homepage response code every 30 seconds, Prometheus aggregates everything, Grafana shows CPU, memory, and disk charts, and Alertmanager sends "disk > 85% for 5 min" and "homepage non-200 for 3 checks" to Telegram.
On the business side, the two most-watched metrics are nginx_http_requests_total (exposed by nginx-module-vts) and php_fpm_processes_total (via the PHP-FPM exporter). Plotted as lines, they make the linkage between "traffic up, PHP processes up, CPU up" visible at a glance — far easier to spot a bottleneck than staring at a single chart. For broader tool selection and expansion ideas, see website monitoring tools.
FAQ
Isn't it risky to expose three monitoring ports? As long as they're all bound to 127.0.0.1 and accessed via SSH tunnel or reverse proxy, nothing is exposed. Don't map port 3000 directly to the internet for convenience — the default Grafana credentials get brute-forced by scanners almost immediately.
How do I monitor multiple servers? Add one target per host in scrape_configs, or use file_sd to read the host list from a file; add HTTP probing with blackbox_exporter when you need it.
What if Prometheus data is lost? Prometheus itself has no HA; a single-instance loss is permanent. For important data, use remote storage (e.g., Thanos) or take periodic backups with promtool tsdb snapshot.
Why does Grafana show no recent data? Check that the data-source URL is reachable from inside the Grafana container (http://prometheus:9090 in Compose, or http://127.0.0.1:9090 from the host), then confirm all targets show UP on the Prometheus Targets page.
Can I expose port 9090 to the internet? Not recommended. Prometheus' API has no built-in authentication, so exposing it publicly opens your metrics to anyone. For remote access, use an SSH tunnel, VPN, or an authenticated reverse proxy in front.
Reference: Prometheus docs — https://prometheus.io/docs/introduction/overview/ ; Node Exporter — https://github.com/prometheus/node_exporter ; Grafana docs — https://grafana.com/docs/