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:

  1. Connections → Data sources → Add → Prometheus, URL http://prometheus:9090.
  2. 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.

  • Prometheus
  • Grafana
  • Monitoring