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 composecommand — 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.ymlconfig file as read-only (:ro) into the container. - A named volume
prometheus_datapersists metrics data across container restarts. --storage.tsdb.retention.time=30dkeeps 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_datapersists 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:
- Click the hamburger menu (☰) in the top-left corner
- Go to Connections → Data sources
- Click Add data source
- Select Prometheus
- In the Prometheus server URL field, enter:
http://prometheus:9090 - 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:
- Click the hamburger menu (☰) → Dashboards
- Click New → Import
- In the “Import via grafana.com” field, type
1860and click Load - On the next screen, select your Prometheus data source from the dropdown
- 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.