To monitor a VPS with node_exporter and Grafana you install the exporter on the server, let Prometheus scrape its metrics endpoint every few seconds, and point Grafana at Prometheus for graphs and alerts. This guide does all three on one Linux VPS, keeps every service bound to localhost, and leaves you with a dashboard that shows CPU, memory, disk and network history.
You will install node_exporter as a systemd service, run Prometheus and Grafana in Docker Compose, and reach both through an SSH tunnel so nothing new is exposed to the internet.
Before you start
- A Linux VPS running Ubuntu 22.04/24.04, Debian 12, or AlmaLinux/Rocky 8–9, with root or sudo access.
- Docker and the Compose plugin installed. If you have not done this yet, follow Deploy a Self-Hosted App with Docker Compose from Scratch first.
- About 1 GB of free RAM and a few GB of disk for metric history. Prometheus stores samples on disk, so plan for growth.
- Your server IP and SSH access. Log in to your VPS provider's control panel if you need to check the IP, reset the root password or open the browser console.
Note: Everything here binds to 127.0.0.1. You will view Grafana through an SSH tunnel, which is the safest default for a single-server setup.
Install node_exporter on the VPS
Download the current release rather than pinning a version you read in a blog post. The releases page on GitHub always lists the latest tag.
cd /tmp
VER=$(curl -fsSL https://api.github.com/repos/prometheus/node_exporter/releases/latest | grep -oP '"tag_name": "v\K[^"]+')
curl -fsSLO "https://github.com/prometheus/node_exporter/releases/download/v${VER}/node_exporter-${VER}.linux-amd64.tar.gz"
tar xzf "node_exporter-${VER}.linux-amd64.tar.gz"
sudo install -m 0755 "node_exporter-${VER}.linux-amd64/node_exporter" /usr/local/bin/node_exporterOn an ARM VPS, replace linux-amd64 with linux-arm64 in both the download and the extracted directory name. Confirm the binary runs:
node_exporter --versionCreate a service account with no shell and no home directory, then add the systemd unit.
sudo useradd --no-create-home --shell /usr/sbin/nologin node_exporter
sudo tee /etc/systemd/system/node_exporter.service >/dev/null <<'EOF'
[Unit]
Description=Prometheus Node Exporter
After=network-online.target
[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter --web.listen-address=127.0.0.1:9100
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now node_exporterCheck the endpoint returns metrics:
curl -s http://127.0.0.1:9100/metrics | head -n 20You should see lines such as node_cpu_seconds_total and node_memory_MemAvailable_bytes. If the service fails, sudo journalctl -u node_exporter -n 50 shows why.
Run Prometheus and Grafana with Docker Compose
Create a working directory and the scrape configuration. Prometheus needs to know where node_exporter lives; since both run on the same host, 127.0.0.1:9100 is correct.
sudo mkdir -p /opt/monitoring
cd /opt/monitoring
sudo tee prometheus.yml >/dev/null <<'EOF'
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: node
static_configs:
- targets: ['127.0.0.1:9100']
EOFNow the Compose file. Both containers use host networking so they can reach the exporter on the loopback address, and both listen only on 127.0.0.1.
services:
prometheus:
image: prom/prometheus:latest
restart: unless-stopped
network_mode: host
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=30d
- --web.listen-address=127.0.0.1:9090
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
grafana:
image: grafana/grafana:latest
restart: unless-stopped
network_mode: host
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GF_ADMIN_PASSWORD}
GF_SERVER_HTTP_ADDR: 127.0.0.1
GF_SERVER_HTTP_PORT: "3000"
volumes:
- grafana-data:/var/lib/grafana
volumes:
prometheus-data:
grafana-data:Put the Grafana admin password in a .env file next to the Compose file and lock it down.
cd /opt/monitoring
printf 'GF_ADMIN_PASSWORD=%s\n' "$(openssl rand -base64 24)" | sudo tee .env >/dev/null
sudo chmod 600 .env
sudo docker compose up -d
sudo docker compose psWarning: Do not publish port 9100 to the internet. The metrics endpoint exposes hostnames, kernel versions and mount points, and it has no authentication. Keep it on 127.0.0.1 and reach Grafana over SSH instead.
Open a tunnel from your own machine and browse to Grafana:
ssh -N -L 3000:127.0.0.1:3000 -L 9090:127.0.0.1:9090 deploy@203.0.113.10Log in at http://localhost:3000 as admin with the password from .env. Then add the data source: Connections → Data sources → Add data source → Prometheus, set the URL to http://127.0.0.1:9090, and click Save & test. You should see a green confirmation.
Import a node_exporter dashboard
You can build panels by hand, but the community dashboard "Node Exporter Full" already covers CPU, memory, disk, network and load. In Grafana go to Dashboards → New → Import, enter the dashboard ID 1860, select your Prometheus data source and import. Check the dashboard page on grafana.com for the current ID and any required exporter flags, since community dashboards change over time.
If you prefer a smaller board, create a new dashboard and add panels with queries like:
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)(node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100The first shows CPU usage as a percentage, the second shows free space on the root filesystem. Set the unit to percent and the graph type to Time series.
Check it works
Work through these three checks in order:
curl -s http://127.0.0.1:9100/metrics | wc -lreturns a large number, not zero.- In Prometheus at
http://localhost:9090, open Status → Targets. Thenodejob should be UP with a recent scrape time. - In Grafana, open the imported dashboard and confirm the time range is the last 6 hours. Panels should draw lines, and CPU should move when you run
sudo apt updateor a similar load.
To confirm data is really flowing, run a quick CPU burn for a few seconds and watch the graph:
timeout 20 bash -c 'while :; do :; done'Troubleshooting
- Prometheus target shows
connection refused. node_exporter is stopped or listening on the wrong address. Runsudo systemctl status node_exporterandsudo ss -lntp | grep 9100. If it is bound to127.0.0.1and Prometheus runs in a bridge network instead of host mode, either switch tonetwork_mode: hostas shown above or bind the exporter to the Docker bridge address. - Grafana says "No data" on every panel. The data source URL or the time range is wrong. Re-run Save & test on the Prometheus data source, then widen the dashboard time range to the last 24 hours.
node_exporter: command not foundafter a reboot. The binary was copied somewhere outsidePATH, or the unit file points at a different path. CheckExecStartin/etc/systemd/system/node_exporter.serviceand runsudo systemctl daemon-reload.- Disk fills up quickly. Metric history is the usual cause. Lower
--storage.tsdb.retention.time, raisescrape_intervalfrom 15s to 30s, or move theprometheus-datavolume to a larger disk. - High CPU from monitoring itself. Reduce collectors with flags such as
--no-collector.wifior--no-collector.bcache, and confirm you are not scraping the same host twice from two jobs.
Next steps
- Cover the basics first with Monitor CPU, memory, disk and logs on a Linux server.
- Watch file activity alongside metrics using inotify-tools to Monitor File System Changes in Real-Time on Linux.
- Put Grafana behind HTTPS on a real domain with Nginx Proxy Manager in Docker: HTTPS for many self-hosted apps or Traefik in Docker: automatic SSL and routing by container labels.
