Prometheus collects metrics — CPU, memory, disk, network, application counters — by scraping HTTP endpoints at regular intervals and storing them as time series. Grafana turns them into dashboards. Together with node_exporter on each server, they’re the standard open-source monitoring stack.
This guide runs Prometheus and Grafana with Docker Compose on a monitoring VPS and collects metrics from one or more servers.
Architecture
[server A: node_exporter :9100] ─┐
[server B: node_exporter :9100] ─┼──► Prometheus (scrapes every 15 s) ──► Grafana (dashboards)
[the monitoring VPS itself] ─┘ │
└──► Alertmanager (optional: email, Telegram…)
Metrics endpoints should not be public. Use a private network or VPN between servers (WireGuard, Tailscale), or firewall rules that only allow the Prometheus server.
1. node_exporter on each server
The simplest install is the distribution package:
sudo apt install prometheus-node-exporter
sudo systemctl enable --now prometheus-node-exporter
curl -s http://localhost:9100/metrics | head
Restrict port 9100 to your Prometheus server:
sudo ufw allow from 10.8.0.1 to any port 9100 proto tcp
(Replace with your monitoring server’s private/VPN IP.) See Prometheus node exporter for details and extra collectors.
2. Prometheus and Grafana with Docker Compose
On the monitoring server:
mkdir -p ~/monitoring && cd ~/monitoring
prometheus.yml:
global:
scrape_interval: 15s
rule_files:
- /etc/prometheus/alerts.yml
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
- job_name: node
static_configs:
- targets:
- "node-exporter:9100" # this host
- "10.8.0.2:9100" # web1
- "10.8.0.3:9100" # db1
labels:
env: production
compose.yaml:
services:
prometheus:
image: prom/prometheus:latest
restart: unless-stopped
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.retention.time=30d
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./alerts.yml:/etc/prometheus/alerts.yml:ro
- prometheus-data:/prometheus
ports:
- "127.0.0.1:9090:9090"
node-exporter:
image: prom/node-exporter:latest
restart: unless-stopped
pid: host
command: ["--path.rootfs=/host"]
volumes:
- /:/host:ro,rslave
grafana:
image: grafana/grafana:latest
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- grafana-data:/var/lib/grafana
ports:
- "127.0.0.1:3000:3000"
volumes:
prometheus-data:
grafana-data:
Pin image versions in production instead of latest.
3. Alert rules
alerts.yml:
groups:
- name: node
rules:
- alert: InstanceDown
expr: up == 0
for: 2m
labels: { severity: critical }
annotations: { summary: "{{ $labels.instance }} is down" }
- alert: DiskAlmostFull
expr: (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes) < 0.10
for: 10m
labels: { severity: warning }
annotations: { summary: "{{ $labels.instance }} {{ $labels.mountpoint }} below 10% free" }
- alert: HighMemory
expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.90
for: 10m
labels: { severity: warning }
annotations: { summary: "{{ $labels.instance }} memory above 90%" }
Start everything:
echo "GRAFANA_PASSWORD=$(openssl rand -base64 18)" > .env
docker compose up -d
Check targets at http://localhost:9090/targets (through an SSH tunnel: ssh -L 9090:127.0.0.1:9090 user@monitor). All should be UP.
To actually receive alerts, add Alertmanager with an email, Telegram, Slack or webhook receiver, or use Grafana’s own alerting.
4. Grafana
Publish Grafana through a reverse proxy with HTTPS (it serves on 127.0.0.1:3000), or reach it via SSH tunnel. Log in as admin with the password from .env, then:
- Connections → Data sources → Add → Prometheus, URL
http://prometheus:9090. - Dashboards → New → Import — enter dashboard ID 1860 (“Node Exporter Full”), choose the Prometheus data source.
You now have CPU, memory, disk, network, load and filesystem graphs for every server.
5. Useful PromQL
# CPU usage %
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# Memory used %
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100
# Disk free %
node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} * 100
# CPU steal (VPS host contention)
avg by (instance) (rate(node_cpu_seconds_total{mode="steal"}[5m])) * 100
Monitoring applications
Many services expose Prometheus metrics or have exporters: Nginx (nginx-prometheus-exporter), MySQL (mysqld_exporter), PostgreSQL (postgres_exporter), Redis, Docker containers (cAdvisor), and app frameworks via client libraries. Add each as another scrape job.
Sizing
Prometheus storage is efficient — roughly 1–2 bytes per sample. A dozen servers scraped every 15 s with node_exporter needs a few GB for 30 days. A small VPS with 2 GB RAM runs the stack for a handful of servers.
Simpler alternatives
- Netdata — per-second metrics and dashboards with almost no configuration.
- Uptime Kuma — external up/down checks and status pages.
Run your monitoring on a separate VPS from what it monitors — ideally in another location — so you still get alerts when a server or data center has problems.