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.

Prometheus Node Exporter Grafana
Illustrated guide cover for Monitor a VPS With Prometheus, Node Exporter, and Grafana
Prometheus • Node Exporter • Grafana
What you learn

How to scrape host metrics, keep Prometheus retention sane for a small VPS, and expose Grafana privately for dashboard work.

Best for

Small self-hosting operators who have outgrown point-in-time checks and want history without jumping straight to a large observability platform.

Risk to watch

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
Warning: do not expose Grafana on a public port with the default admin login. Keep it bound to localhost until you place it behind an intentional access layer.

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 node target as UP.
  • 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.