Monitor a VPS With Prometheus, Node Exporter, and Grafana
Build a small, practical metrics stack that shows CPU, memory, disk, and load trends clearly enough to catch trouble before it becomes an outage.
How to scrape host metrics, keep Prometheus retention sane for a small VPS, and expose Grafana privately for dashboard work.
Small self-hosting operators who have outgrown point-in-time checks and want history without jumping straight to a large observability platform.
Prometheus will happily keep storing metrics until your disk says otherwise if you ignore retention and capacity.
Before you begin
- A Linux VPS with Docker and Docker Compose already working.
- Enough free disk space for a small metrics store and a Grafana volume.
- A private admin path such as SSH port forwarding or a tailnet for Grafana access.
Grafana's Docker Compose monitoring example mounts the host's /proc, /sys, and root filesystem into
node_exporter read-only, then points Prometheus at the exporter on 9100. That is the right scale for a small VPS guide:
useful host metrics, low ceremony, and no pretend enterprise complexity.
Step 1: Create the monitoring workspace
mkdir -p ~/apps/monitoring
cd ~/apps/monitoring
Create a strong Grafana admin password and keep it in an .env file:
cat > .env <<'EOF'
GRAFANA_ADMIN_PASSWORD=replace-this-with-a-real-password
EOF
Step 2: Define the Compose stack
Save this as docker-compose.yml:
services:
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
command:
- --path.procfs=/host/proc
- --path.rootfs=/rootfs
- --path.sysfs=/host/sys
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
expose:
- 9100
networks:
- monitoring
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=15d
ports:
- 127.0.0.1:9090:9090
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
networks:
- monitoring
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
env_file:
- .env
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
ports:
- 127.0.0.1:3000:3000
volumes:
- grafana_data:/var/lib/grafana
networks:
- monitoring
networks:
monitoring:
volumes:
prometheus_data:
grafana_data:
Step 3: Add the Prometheus scrape config
Save this as prometheus.yml in the same directory:
global:
scrape_interval: 1m
scrape_configs:
- job_name: prometheus
static_configs:
- targets:
- prometheus:9090
- job_name: node
static_configs:
- targets:
- node-exporter:9100
Start the stack:
docker compose up -d
docker compose ps
Step 4: Verify metrics and build a dashboard
Check that Prometheus is healthy and scraping:
curl http://127.0.0.1:9090/-/ready
docker compose logs prometheus --tail 50
docker compose logs node-exporter --tail 50
Then open Grafana through your private admin path, add Prometheus at http://prometheus:9090 as the data source, and import the
community Node Exporter Full dashboard ID 1860 if you want a fast baseline.
What success looks like:
- Prometheus shows the
nodetarget asUP. - Grafana graphs host CPU, memory, filesystem, and load values instead of empty panels.
- You can explain where the data is stored and how long it is retained.
Step 5: Operate the stack without wasting disk
For a single VPS, start with short retention and expand only after you measure actual storage use. Prometheus is powerful, but it is not a license to keep infinite metrics on a tiny disk.
docker volume inspect monitoring_prometheus_data
docker exec prometheus du -sh /prometheus
docker compose logs --tail 100
If you later want alerts, add them after the core metrics loop is stable. A pretty dashboard does not matter if nobody knows what threshold should trigger action.
Rollback and update notes
If Grafana is the only piece misbehaving, do not nuke Prometheus data immediately. Restart or recreate the broken service first:
docker compose up -d --force-recreate grafana
docker compose up -d --force-recreate prometheus
If you need a clean rebuild, stop the stack before manipulating volumes so you know exactly what data you are discarding. A destroyed Prometheus volume means lost history, not a harmless cache reset.
Troubleshooting
Grafana loads but the panels are empty.
Check the Prometheus data source URL and confirm the node target is UP in Prometheus.
Prometheus is running but uses too much disk.
Lower --storage.tsdb.retention.time, reduce scrape frequency, and verify you did not add noisy targets casually.
Node exporter shows permission or mount errors.
Recheck the read-only mounts and the --path.* flags. The container needs those host paths to report real host metrics.
You want alerts next.
Start with a few meaningful rules for disk, RAM pressure, and exporter availability instead of building a huge rulebook you will ignore.