Set Up Server Monitoring with Prometheus + Grafana

Spin up a Prometheus-scrapes-metrics, Grafana-draws-charts monitoring stack on your server in minutes with Docker Compose, and collect host metrics via node_exporter.

Once you run real workloads on your own server or VPS, you face the same questions every day: what's the load right now, how much memory is left, is the disk about to fill up? Prometheus + Grafana is the go-to open-source answer. Prometheus pulls and stores time-series metrics on a fixed interval; Grafana turns those metrics into visual dashboards. This guide walks you through a working setup on Ubuntu/Debian with Docker.

The architecture in one minute

  • Prometheus — at its core a time-series database plus a scraper. Following the target list in prometheus.yml, it periodically sends an HTTP request to each target's /metrics endpoint and stores what comes back. Note that Prometheus actively pulls; targets don't push out.
  • Exporter (e.g. nodeexporter) — the thing you monitor usually doesn't speak Prometheus natively, so an exporter translates host CPU, memory, disk, and network stats into the /metrics text format. nodeexporter is dedicated to host-level metrics.
  • Grafana — treats Prometheus as a data source, queries it with PromQL, and renders charts, dashboards, and alerts.

Data flow: nodeexporter → (scraped by Prometheus) → Prometheus storage → Grafana charts.

Bring up Prometheus + Grafana with Docker Compose

First make sure Docker and the Compose plugin are installed:

sudo apt update && sudo apt install -y docker.io docker-compose-plugin

Create a directory and write a docker-compose.yml:

services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prom_data:/prometheus
    ports:
      - "9090:9090"
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=change-me-please
    volumes:
      - grafana_data:/var/lib/grafana
    restart: unless-stopped

  node_exporter:
    image: prom/node-exporter:latest
    pid: host
    ports:
      - "9100:9100"
    restart: unless-stopped

volumes:
  prom_data:
  grafana_data:

Configure scrape targets in prometheus.yml

In the same directory, create prometheus.yml to tell Prometheus who to scrape:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ["localhost:9090"]

  - job_name: node
    static_configs:
      - targets: ["node_exporter:9100"]

Here nodeexporter:9100 uses the Compose service name — containers reach each other over the internal DNS, so there's no need to hardcode an IP. Then start everything:

docker compose up -d

Add the data source and import a dashboard

  • Open http://YOURSERVERIP:3000 in a browser and log in as admin with the password you set above.
  • Go to Connections → Data sources → Add data source → Prometheus. Set the URL to http://prometheus:9090 (service name, since they share the Compose network), then click Save & test.
  • Import a ready-made board: Dashboards → New → Import, enter the community dashboard ID 1860 (Node Exporter Full), pick the Prometheus data source you just added, and you get a full set of host charts instantly.

To confirm Prometheus is actually scraping, visit http://YOURSERVERIP:9090/targets and check that each target shows UP.

A few practical tips

  • Don't expose these ports to the public internet. Keep 9090/3000/9100 on a private network, or put a firewall and an authenticating reverse proxy in front of them.
  • In production, pin image versions instead of tracking latest, so an auto-update never surprises you with a breaking change.
  • The promdata and grafanadata volumes persist your history and dashboards across restarts.

Summary

Prometheus pulls and stores, Grafana draws — a cleanly decoupled combo with a rich library of community dashboards. Three services and two config files in Docker Compose are enough to get it running on your server: prometheus.yml declares the scrape targets, nodeexporter gathers host metrics, and once Grafana has the data source you import dashboard ID 1860 to watch CPU, memory, disk, and network out of the box. From there, add alerting rules and lock down the ports with authentication, and you have a self-hosted monitoring stack you can rely on long-term.