How to Set Up Prometheus and Grafana with Docker Compose: Monitor Your Linux Server’s CPU, Memory, and Disk in 30 Minutes

Beginner

Why Monitoring Matters (Even for Small Setups)

Here’s a scenario every engineer hits eventually: your application goes down at 2 AM, and you have no idea why. Was it CPU maxing out? Did the disk fill up? Was memory exhausted by a runaway process? Without monitoring, you’re flying blind — and debugging after the fact is painful.

Prometheus and Grafana are the industry-standard open-source monitoring stack. Prometheus collects and stores metrics as time-series data. Grafana turns that data into beautiful, actionable dashboards. Together with Node Exporter (which exposes Linux hardware and OS metrics), you get full visibility into your server’s health.

The best part? With Docker Compose, you can have this entire stack running in about 30 minutes. No complex installation steps, no dependency headaches. Let’s build it together.

What We’re Building

By the end of this guide, you’ll have:

  • Node Exporter — collecting CPU, memory, disk, and network metrics from your Linux server
  • Prometheus — scraping those metrics and storing them in a time-series database
  • Grafana — displaying everything in a real-time dashboard you can customize

All three services will run as Docker containers managed by a single Docker Compose file.

Prerequisites

Before we start, make sure you have:

  • A Linux server (Ubuntu 20.04+, Debian 11+, or similar — any modern Linux distribution works)
  • Docker Engine installed (version 20.10 or newer)
  • Docker Compose plugin installed (the docker compose command — V2 syntax)
  • Basic comfort with the terminal

You can verify your Docker installation with:

docker --version
docker compose version

You should see output similar to:

Docker version 27.4.1, build b9d17ea
Docker Compose version v2.32.1

If you don’t have Docker installed yet, follow the official Docker installation guide for your distribution. Don’t use the version from your distro’s default package manager — it’s usually outdated.

Step 1: Create the Project Directory

Let’s keep things organized. Create a dedicated directory for your monitoring stack:

mkdir -p ~/monitoring-stack
cd ~/monitoring-stack

Step 2: Create the Prometheus Configuration File

Prometheus needs a configuration file that tells it where to scrape metrics from and how often. Create the file:

nano prometheus.yml

Paste the following configuration:

global:
  scrape_interval: 15s
  evaluation_interval: 15s

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

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

Let’s break this down:

  • scrape_interval: 15s — Prometheus will pull metrics from every target every 15 seconds. This is a sensible default.
  • job_name: “prometheus” — Prometheus can monitor itself. This is useful for debugging.
  • job_name: “node-exporter” — This tells Prometheus to scrape system metrics from Node Exporter.
  • The targets use container names (like node-exporter) instead of IP addresses because Docker Compose creates a shared network where containers can resolve each other by name.

Step 3: Create the Docker Compose File

This is the core of our setup. Create the file:

nano docker-compose.yml

Paste the following:

services:
  prometheus:
    image: prom/prometheus:v3.2.1
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.path=/prometheus"
      - "--storage.tsdb.retention.time=30d"
    restart: unless-stopped

  node-exporter:
    image: prom/node-exporter:v1.9.0
    container_name: node-exporter
    ports:
      - "9100:9100"
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - "--path.procfs=/host/proc"
      - "--path.sysfs=/host/sys"
      - "--path.rootfs=/rootfs"
      - "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"
    restart: unless-stopped

  grafana:
    image: grafana/grafana:11.5.2
    container_name: grafana
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=changeme
    restart: unless-stopped

volumes:
  prometheus_data:
  grafana_data:

Let’s walk through what each service does:

Prometheus Service

  • We mount our prometheus.yml config file as read-only (:ro) into the container.
  • A named volume prometheus_data persists metrics data across container restarts.
  • --storage.tsdb.retention.time=30d keeps 30 days of metrics. Adjust this based on your disk space.

Node Exporter Service

  • We mount /proc, /sys, and the root filesystem as read-only so Node Exporter can read system metrics.
  • The --path.* flags tell Node Exporter where to find these mounted paths inside the container.
  • The filesystem mount-points-exclude flag prevents Node Exporter from reporting on virtual filesystems that would create noise.

Grafana Service

  • A named volume grafana_data persists dashboards and settings.
  • We set a default admin username and password via environment variables. Change the password — we’ll cover this in a moment.

⚠️ Common beginner mistake: Forgetting the :ro (read-only) flag on Node Exporter volumes. While the stack will still work, running containers with write access to your host’s /proc and /sys is an unnecessary security risk.

Step 4: Start the Stack

From your ~/monitoring-stack directory, run:

docker compose up -d

The -d flag runs containers in detached mode (in the background). You should see output like:

[+] Running 4/4
 ✔ Network monitoring-stack_default  Created
 ✔ Container node-exporter           Started
 ✔ Container prometheus              Started
 ✔ Container grafana                 Started

Verify all three containers are running:

docker compose ps

Expected output:

NAME            IMAGE                        COMMAND                  SERVICE         PORTS                    STATUS
grafana         grafana/grafana:11.5.2       "/run.sh"                grafana         0.0.0.0:3000->3000/tcp   Up 30 seconds
node-exporter   prom/node-exporter:v1.9.0    "/bin/node_exporter …"   node-exporter   0.0.0.0:9100->9100/tcp   Up 30 seconds
prometheus      prom/prometheus:v3.2.1       "/bin/prometheus --c…"   prometheus      0.0.0.0:9090->9090/tcp   Up 30 seconds

All three should show a status of “Up”. If any container exited, check its logs:

docker compose logs prometheus
docker compose logs node-exporter
docker compose logs grafana

Step 5: Verify Prometheus Is Scraping Metrics

Open your browser and navigate to http://your-server-ip:9090. You’ll see the Prometheus web UI.

Click on Status → Targets in the top menu. You should see two targets — prometheus and node-exporter — both with a state of UP (shown in green).

If a target shows as DOWN, the most common causes are:

  • A typo in prometheus.yml (YAML is very sensitive to indentation — use spaces, never tabs)
  • The container name in the target doesn’t match the service name in Docker Compose

Let’s also run a quick query. In the Prometheus query box at the top of the main page, type:

node_cpu_seconds_total

Click Execute. You should see a table of CPU time metrics broken down by mode (idle, system, user, etc.). If you see data, Prometheus is successfully collecting metrics from Node Exporter. Great work!

Step 6: Connect Grafana to Prometheus

Now let’s make this data visual. Open http://your-server-ip:3000 in your browser.

Log in with the credentials we set in the Docker Compose file:

  • Username: admin
  • Password: changeme

Grafana will prompt you to change the password. Do it now. Use a strong password, especially if this server is accessible over a network.

Now, add Prometheus as a data source:

  1. Click the hamburger menu (☰) in the top-left corner
  2. Go to Connections → Data sources
  3. Click Add data source
  4. Select Prometheus
  5. In the Prometheus server URL field, enter: http://prometheus:9090
  6. Scroll to the bottom and click Save & test

You should see a green banner: “Successfully queried the Prometheus API.”

We use http://prometheus:9090 (not localhost) because Grafana is running inside a Docker container. It needs to reach Prometheus through the Docker network, where the container name prometheus resolves to the correct internal IP address.

⚠️ Common beginner mistake: Using http://localhost:9090 as the Prometheus URL in Grafana. This won’t work because localhost inside the Grafana container refers to the Grafana container itself, not your host machine.

Step 7: Import a Pre-Built Dashboard

You could build dashboards from scratch, but the community has already created excellent ones. The most popular Node Exporter dashboard is Dashboard ID 1860 (“Node Exporter Full” by rfraile). It’s been downloaded millions of times and covers CPU, memory, disk, network, and much more.

To import it:

  1. Click the hamburger menu (☰) → Dashboards
  2. Click NewImport
  3. In the “Import via grafana.com” field, type 1860 and click Load
  4. On the next screen, select your Prometheus data source from the dropdown
  5. Click Import

You should now see a fully populated dashboard with panels for:

  • CPU usage — broken down by mode (user, system, iowait, idle)
  • Memory usage — total, used, cached, buffers, free
  • Disk space — usage per mount point
  • Disk I/O — reads and writes per second
  • Network traffic — bytes received and transmitted per interface
  • System load — 1, 5, and 15 minute load averages

Take a moment to explore. Click on individual panels to see the underlying Prometheus queries. This is one of the best ways to learn PromQL (Prometheus Query Language).

Understanding Key Metrics

Now that you have data flowing, let’s make sure you understand what you’re looking at. Here are some useful Prometheus queries you can try in Grafana’s Explore section (hamburger menu → Explore):

CPU Usage Percentage

100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

This calculates the percentage of CPU time spent doing actual work (i.e., not idle). The irate function calculates the per-second instant rate over the last 5 minutes.

Memory Usage Percentage

(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100

This uses MemAvailable rather than MemFree, which is the correct way to measure usable memory on Linux.

Leave a Comment

Your email address will not be published. Required fields are marked *