Build a Grafana Dashboard with Prometheus

Set up a local monitoring stack and visualize system metrics on a custom dashboard.

Introduction

30 Second Summary

Every app you use has a team watching dashboards to make sure things are running smoothly, catching problems before users even notice.

In this project, you will build a complete local monitoring stack using Prometheus and Grafana running inside Docker containers, collecting real system metrics and visualizing them on a professional dashboard you build from scratch.

What You'll Build

You will set up a Docker Compose stack where Prometheus scrapes system-level metrics from a Node Exporter container, and Grafana visualizes those metrics on a multi-panel dashboard.

By the end of this project, you'll have:

  • A Docker Compose file that spins up Prometheus, Grafana, and Node Exporter together.
  • A Prometheus configuration file that defines scrape targets for collecting metrics.
  • A custom Grafana dashboard with four panels showing CPU usage, memory usage, disk space, and network traffic using PromQL queries.
  • A stretch challenge to configure Grafana alerting rules that fire when CPU usage exceeds 80%.

Are there any prerequisites?

None. This project includes full setup instructions for Docker Desktop and all required tools. Everything runs locally on your machine at zero cost.

Install Docker Desktop and Set Up the Project

Before you can spin up Prometheus and Grafana, you need a way to run them on your machine. Docker lets you run each tool inside its own isolated container, so you don't have to install anything directly on your operating system. Docker Compose takes this further by letting you define your entire stack in a single file and start everything with one command.

In This Step, Get Ready To:
  • Install Docker Desktop and verify it works from your terminal.
  • Create your project folder and configuration files.
  • Write a docker-compose.yml file that defines your Prometheus, Grafana, and Node Exporter services.
Download and Install Docker Desktop

Docker Desktop is a desktop application that bundles everything you need to build and run containers. It includes the Docker engine, the docker CLI, and docker compose.

  • Download Docker Desktop from the official Docker website.
  • Run the installer and follow the on-screen prompts for your operating system.
  • Open Docker Desktop and wait for the engine to start. You should see a green "running" indicator.

Now verify Docker is working from your terminal.

  • Open your terminal (Terminal on macOS, PowerShell on Windows).
  • Run the following commands to check your installation:
docker --version
docker compose version

What should I see?

Both commands should print version numbers. The exact versions don't matter as long as the commands are recognized. If you see command not found, Docker Desktop may not have finished starting up.

I see version numbers

Docker is installed and ready to go.

I see an error

Make sure Docker Desktop is fully running. Look for the Docker whale icon in your system tray or menu bar. It should show a green "running" status.

Still seeing an error?

Try restarting Docker Desktop. On Windows, you may need to enable WSL 2 or Hyper-V in your system settings. Help me troubleshoot my Docker installation.

Create Your Project Directory and Files

Your monitoring stack needs two configuration files. The docker-compose.yml file defines which containers to run. The prometheus.yml file tells Prometheus where to collect metrics from.

  • Run the following commands in your terminal to create the project folder and files:

macOS

mkdir grafana-prometheus-lab
cd grafana-prometheus-lab
touch docker-compose.yml prometheus.yml

Windows

mkdir grafana-prometheus-lab
cd grafana-prometheus-lab
New-Item docker-compose.yml, prometheus.yml
  • Open this folder in VS Code (or your preferred text editor).
code .

Pro tip: any editor works

If you don't have VS Code, you can use any text editor. The code . command is just a shortcut to open the current folder in VS Code.

You should see two empty files in your editor: docker-compose.yml and prometheus.yml.

Write the Docker Compose File

Docker Compose reads a YAML file that describes all the containers you want to run together. Instead of starting each container with a separate docker run command, you define everything in one place and launch it all at once.

Why Docker Compose instead of individual commands?

You could start each container separately with docker run commands, but Docker Compose is the standard for multi-container setups. It creates a shared network automatically so your containers can talk to each other by name. It also lets you start, stop, and rebuild everything with a single command.

This mirrors how real-world observability stacks are deployed using infrastructure-as-code.

Your stack needs three services:

  • Prometheus collects and stores metrics. It runs on port 9090.
  • Grafana visualizes metrics on dashboards. It runs on port 3000.
  • Node Exporter exposes system-level metrics (CPU, memory, disk) for Prometheus to collect. It runs on port 9100.
  • Open docker-compose.yml in your editor and paste the following:
services:
  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"

  grafana:
    image: grafana/grafana-enterprise
    ports:
      - "3000:3000"
    volumes:
      - grafana-data:/var/lib/grafana

  node-exporter:
    image: prom/node-exporter
    ports:
      - "9100:9100"

volumes:
  grafana-data:

What does this file do?

This file defines three services that Docker Compose will run as containers.

  • The ports section maps a port on your machine to a port inside the container. For example, "9090:9090" means visiting localhost:9090 on your machine reaches the Prometheus container.
  • The volumes section for Prometheus mounts your local prometheus.yml file into the container at /etc/prometheus/prometheus.yml. This is how Prometheus reads your custom configuration.
  • The grafana-data named volume stores Grafana's data at /var/lib/grafana so your dashboards survive container restarts.

Why grafana/grafana-enterprise?

The grafana/grafana-enterprise image includes all the open-source features for free. As of Grafana 12.4.0, the older grafana/grafana-oss image is deprecated. Using the enterprise image keeps you aligned with official Grafana documentation.

  • Save the file.

Your project structure is set and your Docker Compose file is ready. Next up, you'll configure Prometheus to know where to collect metrics from and launch the entire stack.

Configure Prometheus and Launch the Stack

You have a Docker Compose file that defines your three services, but Prometheus doesn't know what to monitor yet. Prometheus relies on a configuration file that tells it which endpoints to scrape for metrics. In this step, you'll write that configuration and bring the entire stack to life.

In This Step, Get Ready To:
  • Write the Prometheus configuration file that defines scrape targets.
  • Launch all three containers with a single Docker Compose command.
  • Verify that Prometheus is successfully scraping metrics from both targets.
Write the Prometheus Configuration File

Prometheus uses a YAML configuration file to know how often to collect metrics and where to find them. Each endpoint Prometheus monitors is called a scrape target. You'll configure two targets: Prometheus itself (so it monitors its own health) and Node Exporter (which provides your system metrics).

  • Open prometheus.yml in VS Code.
  • Paste in the following configuration:
global:
  scrape_interval: 15s

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

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

What does this configuration do?

The scrape_interval tells Prometheus to collect metrics every 15 seconds. This is the standard recommended interval.

Under scrape_configs, each job defines a target to monitor. The prometheus job points to localhost:9090 because Prometheus scrapes itself from within its own container. The node-exporter job uses the Docker Compose service name node-exporter:9100 instead of localhost, because Docker Compose creates a shared network where containers find each other by service name.

  • Save the file.
Launch the Stack with Docker Compose

With both configuration files in place, a single command spins up all three containers. Docker Compose reads your docker-compose.yml, pulls the required images, and starts everything on a shared network.

  • Open your terminal and make sure you're inside the grafana-prometheus-lab directory.
  • Run the following command:
docker compose up -d

What does this command do?

The docker compose up command creates and starts all the services defined in your docker-compose.yml file. The -d flag runs everything in detached mode, so the containers run in the background and you get your terminal back.

Docker will pull the images if this is your first time running the command. This may take a minute or two depending on your internet connection.

  • Verify all three containers are running:
docker compose ps

What should I see?

You should see three containers listed, all with a status of "running". The services are prometheus, grafana, and node-exporter.

Now verify each service is accessible in your browser.

  • Open http://localhost:9090 to see the Prometheus UI.
  • Open http://localhost:3000 to see the Grafana login page.
  • Open http://localhost:9100/metrics to see the raw metrics output from Node Exporter.

What is all that text on the Node Exporter page?

The wall of text at localhost:9100/metrics is raw time-series data. Each line is a single metric with a name, labels, and a numeric value. This is what Prometheus scrapes every 15 seconds. On its own, this data is hard to read. That's exactly why you need Grafana to turn it into visual dashboards.

One of the pages won't load?

Run docker compose logs to check for error messages. Common issues include a typo in docker-compose.yml or port conflicts if another application is already using port 9090, 3000, or 9100. Help me troubleshoot my Docker Compose stack.

Confirm Prometheus Targets Are Up

Seeing the Prometheus UI load is a good sign, but you need to confirm that Prometheus is actually collecting data from both scrape targets. The Targets page shows the health status of every endpoint Prometheus is monitoring.

  • Go to http://localhost:9090 in your browser.
  • Click **Status** in the top navigation bar.
  • Select **Targets** from the dropdown.

You should see two target groups: prometheus and node-exporter. Both should display a State of UP in green. This confirms that Prometheus is successfully scraping metrics from both endpoints every 15 seconds.

What does UP mean on the Targets page?

A target with a state of UP means Prometheus successfully reached that endpoint and received metrics data during the last scrape. If a target shows DOWN, it means Prometheus tried to connect but failed. The error column will tell you why.

Pro tip: Node Exporter on macOS and Windows

Node Exporter running inside a Docker container on macOS or Windows reports metrics for the Docker virtual machine, not your host machine directly. The metrics are still real and change over time, which is exactly what you need for learning. In production, Node Exporter typically runs directly on the host.

Your monitoring stack is live. Prometheus is collecting real system metrics every 15 seconds, and all three services are communicating over the Docker network. Next up, you'll connect Grafana to Prometheus so you can start turning all that raw data into visual dashboards.

Connect Prometheus to Grafana as a Data Source

Your monitoring stack is running and Prometheus is actively scraping metrics from both itself and Node Exporter. But Prometheus on its own only collects and stores data. It doesn't give you a visual way to understand what's happening on your system.

Grafana is the visualization layer. To make it work, you need to tell Grafana where to find the metrics. This connection is called a data source. In this step, you'll log into Grafana and connect it to your Prometheus server so it can start querying real metrics.

In This Step, Get Ready To:
  • Log into the Grafana web interface for the first time.
  • Add Prometheus as a data source using Docker networking.
  • Verify that Grafana can successfully reach Prometheus.
Log Into Grafana

Grafana ships with default credentials that let you access the web interface immediately after startup.

  • Open your browser and navigate to http://localhost:3000.
  • Enter admin as the username.
  • Enter admin as the password.
  • Click Log in.

Grafana prompts you to change the default password. You can set a new password or click Skip for now since this is a local project.

What should I see?

After logging in, you should see the Grafana home page with a welcome banner and options to add your first data source, create a dashboard, and explore data.

Add Prometheus as a Data Source

A data source tells Grafana where to fetch metrics from. You can connect Grafana to many different backends, but for this project you'll connect it to Prometheus.

  • In the left sidebar, click Connections.
  • Click Data sources.
  • Click Add data source.
  • Select Prometheus from the list.
  • Scroll down to the Connection section.
  • In the Prometheus server URL field, enter http://prometheus:9090.

Why prometheus Instead of localhost?

Each Docker container has its own network namespace. When Grafana runs inside a container, localhost refers to the Grafana container itself, not your host machine.

Docker Compose automatically creates a shared network for all services defined in the same docker-compose.yml file. This lets containers find each other by their service name. Since you named the Prometheus service prometheus in your Compose file, Grafana can reach it at http://prometheus:9090.

Save and Test the Connection

Before you start building dashboards, you need to confirm that Grafana can actually talk to Prometheus. Grafana has a built-in test for this.

  • Scroll to the bottom of the data source configuration page.
  • Click Save & test.

You should see a green success message confirming that Grafana can reach Prometheus and query its data.

I see a green success message

Your data source is connected. Grafana can now query Prometheus for any metrics it has collected. You're ready to build your first dashboard.

I see an error

If you see an error message, check the following:

  • Make sure the URL is exactly http://prometheus:9090 and not http://localhost:9090. Using localhost is the most common mistake here.
  • Confirm that your Prometheus container is still running by opening http://localhost:9090 in your browser.
  • Run docker compose ps in your terminal to verify all three containers are in a running state.

After fixing the URL or restarting containers, click Save & test again.

Still seeing an error?

If the connection still fails after checking the URL and container status, help me debug my Grafana to Prometheus connection.

Your monitoring pipeline is now fully wired. Prometheus collects the metrics, and Grafana knows where to find them. Next up, you'll put this connection to work by building a multi-panel dashboard that visualizes real system metrics.

Build Your First Dashboard with Multiple Panels

Prometheus is collecting metrics and Grafana can talk to it. Now it's time to turn raw numbers into something visual. In this step, you'll build a dashboard with four panels that show real-time CPU, memory, disk, and network data from your machine.

In This Step, Get Ready To:
  • Write PromQL queries that transform raw metrics into meaningful percentages and rates.
  • Build four dashboard panels covering CPU usage, memory usage, disk space, and network traffic.
  • Arrange and save your first complete monitoring dashboard in Grafana.
Create Your First Panel for CPU Usage

A panel is a single visualization on a Grafana dashboard. Each panel runs its own query against a data source and displays the result as a graph, gauge, or table. Your first panel will show CPU usage as a time series chart.

  • Click Dashboards in the left sidebar.
  • Click New, then New dashboard.
  • Click Add visualization.
  • Select the Prometheus data source.
  • In the query editor at the bottom, enter the following PromQL query:
100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

What Does This Query Do?

This PromQL query calculates the percentage of CPU time that is NOT spent idle, which equals your CPU usage. Here's how it breaks down:

  • node_cpu_seconds_total{mode="idle"} grabs the raw counter of idle CPU seconds.
  • rate(...[5m]) calculates the per-second average rate of increase over the last 5 minutes.
  • avg() averages the result across all CPU cores.
  • 100 - (... * 100) flips the idle percentage into a usage percentage.
  • Click Run queries to see the query results appear in the graph preview above.
  • In the panel options on the right side, set the Title to CPU Usage %.
  • Make sure the visualization type is set to Time series (this is the default).
  • Click Apply in the top right to add the panel to your dashboard.

You should see your first panel appear on the dashboard, showing a line graph of CPU usage over time. The line will be relatively low if your machine is idle.

Why Are My Metrics From the Docker VM?

If you're on macOS or Windows, Node Exporter running inside Docker reports metrics for the Docker virtual machine, not your host machine directly. This is completely fine for learning. The metrics are still real and change over time as your containers do work.

Add Memory and Disk Space Panels

A single panel only tells part of the story. Monitoring dashboards are powerful because they let you see multiple metrics side by side, making it easy to spot correlations between CPU spikes and memory pressure.

  • Click Add in the top toolbar, then select Visualization.
  • Select the Prometheus data source.
  • Enter the following PromQL query for memory usage:
100 * (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes))

What Does This Query Do?

This query divides available memory by total memory to get the fraction of memory that is free. Subtracting from 1 flips it to the fraction that is in use. Multiplying by 100 converts it to a percentage.

  • Set the panel title to Memory Usage %.
  • Click Apply.

Now add the disk space panel.

  • Click Add > Visualization again.
  • Select the Prometheus data source.
  • Enter the following PromQL query for disk space:
node_filesystem_avail_bytes{mountpoint="/"}

What Does This Query Do?

This query returns the number of available bytes on the root filesystem (/). The {mountpoint="/"} filter is a label selector that narrows the results to just the root mount point.

  • Set the panel title to Disk Space Available.
  • Click Apply.

Your dashboard now has three panels. You should see CPU usage, memory usage, and disk space all displayed side by side.

Add a Network Panel and Save Your Dashboard

Your fourth panel will track network receive rate, completing a well-rounded view of your system's health.

  • Click Add > Visualization.
  • Select the Prometheus data source and enter the following query:
rate(node_network_receive_bytes_total[5m])

What Does This Query Do?

This query uses rate() to calculate how many bytes per second are being received across your network interfaces over the last 5 minutes. Without rate(), you would see a constantly increasing counter, which is hard to interpret. The rate function turns that counter into a meaningful "bytes per second" metric.

  • Set the panel title to Network Receive Rate.
  • Click Apply.

All four panels are now on your dashboard. Time to arrange them and save your work.

  • Drag the panels by their title bars to rearrange them into a 2x2 grid layout.
  • Resize panels by dragging their bottom-right corners so they fill the grid evenly.
  • Click the save icon (floppy disk) at the top of the page.
  • Name your dashboard System Monitoring Dashboard.
  • Click Save.

Your dashboard is saved and will persist across Grafana restarts thanks to the Docker volume you configured in Step 1.

Pro tip: auto-refresh

Click the refresh icon next to the time range selector at the top right of your dashboard. Set it to auto-refresh every 10 seconds. Your panels will update with new data automatically, giving you a live monitoring experience.

You now have a fully functional monitoring dashboard showing live system metrics. Next up, you'll explore PromQL in more depth and customize your panels with different visualization types, color thresholds, and dashboard variables.

Explore PromQL and Customize Your Dashboard

You have a working dashboard with four panels showing live system metrics. Right now, every panel uses the same visualization type and default styling. In this step, you will dig deeper into PromQL to unlock more detailed data, customize how your panels look, and add a dashboard variable that lets you filter metrics dynamically.

In This Step, Get Ready To:
  • Experiment with PromQL functions to break down CPU usage per core.
  • Customize panel visualizations with gauge types, thresholds, and color coding.
  • Add a dashboard variable for dynamic instance filtering.
Experiment with PromQL Queries

The CPU query you wrote in the previous step averages usage across all CPU cores into a single number. That is useful for a high-level view, but sometimes you need to see which specific core is under heavy load. PromQL lets you group results using the avg by clause, which breaks a single aggregated metric into separate lines per label.

  • Click on the CPU Usage % panel on your dashboard.
  • Select Edit.
  • In the query editor at the bottom, replace the existing query with the following:
avg by (cpu)(rate(node_cpu_seconds_total{mode="idle"}[5m]))

What does this query do?

The avg by (cpu) clause tells Prometheus to calculate a separate average for each CPU core instead of combining them all into one number. You will see multiple lines appear on the graph, one for each core.

You should see multiple lines appear on the time series graph, each representing a different CPU core's idle rate.

  • Now change the time range selector in the top-right corner of the dashboard from Last 6 hours to Last 15 minutes.

The graph zooms in to show more recent data points. This makes it easier to spot short-lived spikes in CPU activity.

  • Revert the query back to the original CPU usage query so the panel shows overall usage again:
100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
  • Click Apply in the top-right corner to save your changes and return to the dashboard.
Customize Panel Visualizations

Time series graphs are great for tracking trends over time, but some metrics are easier to read as a gauge. A gauge shows the current value on a dial with color-coded thresholds, so you can tell at a glance whether something needs attention.

  • Click on the Memory Usage % panel and select Edit.
  • In the top-right corner of the edit view, find the visualization type dropdown (it currently says Time series).
  • Change it to Gauge.

The panel transforms from a line graph into a dial showing the current memory usage percentage. Now let's set meaningful boundaries so the gauge changes color based on severity.

  • In the panel options on the right side, scroll down to the Standard options section.
  • Set Min to 0 and Max to 100.
  • Scroll down to the Thresholds section.
  • Set the base (green) threshold to cover values from 0 to 60.
  • Click Add threshold and set a yellow threshold at 60.
  • Add another threshold and set a red threshold at 80.
  • Click Apply.

Your Memory Usage panel now shows a color-coded gauge. Green means healthy, yellow means watch it, and red means take action.

Pro tip: thresholds on any panel

You can add thresholds to time series panels too. The graph background will change color when the metric crosses your threshold values. Try it on your CPU Usage panel for extra visual feedback.

Add a Dashboard Variable for Dynamic Filtering

In a real monitoring environment, you often have multiple servers reporting metrics. A dashboard variable creates a dropdown at the top of your dashboard that lets you filter all panels by a specific label value, like which server instance to display. Even with a single instance, setting this up teaches you a pattern you will use constantly in production.

  • Click the gear icon (Dashboard Settings) at the top of your dashboard.
  • Select Variables in the left sidebar.
  • Click Add variable.
  • Set the Name to instance.
  • Set the Type to Query.
  • Select the Prometheus data source.
  • In the query field, enter:
label_values(node_cpu_seconds_total, instance)

What does this query do?

The label_values() function is a Grafana-specific query that asks Prometheus for all unique values of the instance label found on the node_cpu_seconds_total metric. Each unique value becomes an option in your dropdown.

  • Click Apply to save the variable.
  • Click Back to dashboard or close the settings panel.

You should now see an instance dropdown at the top of your dashboard. Right now it shows your single Node Exporter instance. In a production environment with dozens of servers, this dropdown would let you switch between them instantly.

  • Click the save icon at the top of the dashboard to save all your changes.

Your dashboard now has professional-grade features: custom visualizations, color-coded thresholds, and dynamic filtering. These are the exact patterns used in production monitoring dashboards at companies of every size.

Secret mission

Set Up Grafana Alerting

A dashboard is only useful if someone is watching it. In the real world, observability engineers configure alerts that automatically notify the team when something goes wrong. In this secret mission, you will set up alert rules in Grafana that fire when CPU usage spikes above 80%.

Before you go

Clean Up Your Resources

This project runs entirely locally using Docker, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Prometheus container (running on port 9090).
  • Grafana container (running on port 3000).
  • Node Exporter container (running on port 9100).
  • Docker named volume for Grafana data persistence.
  • Project directory grafana-prometheus-lab with docker-compose.yml and prometheus.yml.
  • Grafana dashboard and data source configuration (stored in the Docker volume).

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with your dashboard.

  • Your containers will keep running and consuming memory until you stop them or restart your machine.
  • Grafana will continue to be available at http://localhost:3000 and Prometheus at http://localhost:9090.
  • There are no cloud costs. The only resource being used is local memory and CPU.

Pause - I'll come back to this later

Shut down running containers to free up memory while keeping your project files and Grafana data intact.

  • Open your terminal and navigate to your grafana-prometheus-lab directory.
  • Run docker compose stop to stop all three containers without removing them.
  • Your Grafana dashboards, data source configuration, and project files will all be preserved.
  • When you are ready to resume, navigate back to the project directory and run docker compose start.

Delete - I don't want to use this again

Remove all project resources and start fresh. This will delete your containers, volumes, and all Grafana dashboard configurations.

  • Open your terminal and navigate to your grafana-prometheus-lab directory.
  • Run docker compose down -v to stop and remove all containers, networks, and named volumes.
  • Run docker image rm prom/prometheus grafana/grafana-enterprise prom/node-exporter to remove the downloaded Docker images and free up disk space.
  • Delete the grafana-prometheus-lab project directory from your machine.
  • If you no longer need Docker Desktop for other projects, you can uninstall it from your system.

Mission accomplished

That's a Wrap!

Nice work! You just built a complete local monitoring stack from scratch using Prometheus, Grafana, and Node Exporter.

This is the exact observability pipeline that production teams rely on every day. You now have hands-on experience with the tools that show up in nearly every monitoring and observability job posting.

Here's what you learned:

  • Set up a Docker Compose stack with Prometheus, Grafana, and Node Exporter, and configured them to communicate over a shared Docker network.
  • Wrote PromQL queries to transform raw system metrics into meaningful visualizations for CPU usage, memory usage, disk space, and network traffic.
  • Built a multi-panel Grafana dashboard with custom visualization types, color-coded thresholds, and dynamic filtering using dashboard variables.
  • Configured Grafana alerting with alert rules, contact points, and notification policies that fire when CPU usage exceeds a threshold.

Ready to quiz yourself?