,

Prometheus and Grafana Docker Compose: Build a Self-Hosted Monitoring Stack

Server room data center representing a self-hosted Prometheus and Grafana Docker monitoring stack

The comparison of self-hosted monitoring tools covers why Prometheus and Grafana are usually the answer once a homelab outgrows a single-host dashboard. This is the part where you actually build it. By the end of this guide, Prometheus will be scraping metrics from your host and containers, Grafana will be turning those metrics into dashboards, and you’ll have a docker-compose.yml file you can extend as your homelab grows. It’s an afternoon of work, not a weekend, and most of that afternoon is just watching containers start.

What you’re building

The stack has four pieces, and it helps to know what each one does before pasting a config file:

  • Prometheus scrapes (pulls) metrics from targets on a schedule and stores them in its own time-series database
  • node_exporter exposes host-level metrics (CPU, RAM, disk, network) in a format Prometheus understands
  • cAdvisor exposes per-container metrics, so you can see what each Docker container is using
  • Grafana connects to Prometheus as a data source and turns the stored metrics into dashboards, graphs, and alerts

None of these run without the others doing their part. Prometheus without exporters has nothing to scrape. Grafana without Prometheus has nothing to visualize. It’s a pipeline, not four independent apps.

Prerequisites

Docker and Docker Compose need to be installed on the host, and it helps to already have a folder set aside for the stack’s config files. Nothing here needs a particularly powerful machine. Prometheus is the heaviest of the bunch, but for a typical homelab with a few dozen scrape targets, 1-2 GB of RAM and a bit of disk space for metric retention is enough to work with comfortably.

The docker-compose.yml file

Create a project folder, and inside it, a docker-compose.yml with the following:

version: "3.8"

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

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

  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    restart: unless-stopped
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    ports:
      - "8080:8080"

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

volumes:
  prometheus_data:
  grafana_data:

Change GF_SECURITY_ADMIN_PASSWORD before starting anything, obviously. The named volumes keep Prometheus’s metric history and Grafana’s dashboards and settings intact across container restarts and updates, which matters more than it sounds like it should the first time an update wipes a dashboard nobody backed up.

The Prometheus scrape configuration

Prometheus needs to be told what to scrape and how often. In the same folder, create prometheus.yml:

global:
  scrape_interval: 15s

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

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

  - job_name: "cadvisor"
    static_configs:
      - targets: ["cadvisor:8080"]

The container names from the compose file (node-exporter, cadvisor) work directly as hostnames here because Docker Compose puts all the services on the same network by default. A 15-second scrape interval is a reasonable default: frequent enough for useful dashboards, not so frequent that it drowns Prometheus in data on a small homelab.

Starting the stack

From the project folder:

docker compose up -d

Give it a few seconds, then check that all four containers are running with docker compose ps. Prometheus should be reachable at http://your-server-ip:9090, and its Status → Targets page is worth checking first: it lists every scrape target and whether Prometheus can reach it. If node-exporter or cadvisor show as “down” here, it’s almost always a networking issue in the compose file rather than anything wrong with Prometheus itself.

Connecting Grafana to Prometheus

Grafana is reachable at http://your-server-ip:3000. Log in with admin and the password set in the compose file, then:

  1. Go to Connections → Data sources → Add data source
  2. Choose Prometheus
  3. Set the URL to http://prometheus:9090 (the container name again, not localhost, since Grafana is asking Prometheus over the Docker network)
  4. Save & test

A green confirmation means Grafana can query Prometheus successfully. If it fails, double-check that both containers are on the same Docker network, which they will be by default with this compose file unless something else has been customized. Grafana’s own Prometheus data source documentation covers authentication and query options beyond what’s needed for this setup, worth a look once the basics are working.

Building your first dashboard

Building dashboards panel by panel is a real skill, but it’s not where a first monitoring stack should start. Grafana’s dashboard import feature pulls in dashboards other people have already built and shared publicly, and for node_exporter and cAdvisor specifically, the community options are excellent:

  1. Go to Dashboards → New → Import
  2. Enter a dashboard ID: 1860 is the standard “Node Exporter Full” dashboard, and 893 is a solid cAdvisor dashboard for container metrics
  3. Select the Prometheus data source created earlier
  4. Import

That’s a working, detailed dashboard in about two minutes, and a far better starting point than an empty canvas. Once it’s live, it’s easy to see which panels get looked at day to day and build custom ones for whatever those pre-built dashboards don’t cover.

Setting up a basic alert

A dashboard is only useful if someone’s actually looking at it, which is why alerting matters as much as the panels themselves. Grafana handles this through its own alert rules rather than anything configured in Prometheus directly (Prometheus does have its own Alertmanager, but for a first setup, Grafana’s built-in alerting is the simpler path). A basic disk-space alert looks roughly like this: create a new alert rule, base it on a query against the node_exporter disk metrics, set a threshold such as less than 10% free space, and set how long that condition needs to hold before it actually fires, so a brief spike that resolves itself on its own doesn’t trigger a false alarm. Contact points, which is Grafana’s term for where an alert gets sent, support email, Slack, webhooks, and several other integrations out of the box, configured once under Alerting → Contact points and then attached to whichever rules should use them.

Where Netdata fits into this

Prometheus and Grafana are built for scale and history, not instant feedback. A scrape interval of 15 seconds means a problem has to persist for at least that long before it shows up, and building a new Grafana panel to investigate a live issue takes longer than the issue sometimes lasts. This is where Netdata is worth running alongside this stack rather than instead of it: point it at the same host, and it gives per-second metrics with zero dashboard-building the moment something looks wrong. Prometheus and Grafana stay the system of record for trends and alerts; Netdata becomes the tool that’s actually open when a container starts misbehaving right now. The full breakdown of when each tool makes sense is in the self-hosted monitoring comparison.

Adding more targets as your homelab grows

This four-container stack is a starting point, not the finished product. Most self-hosted apps that expose Prometheus metrics natively (or through a dedicated exporter) can be added the same way node-exporter and cAdvisor were: add a new job_name block to prometheus.yml with the target’s host and port, then reload Prometheus without restarting the whole stack by sending a POST request to its /-/reload endpoint (or just running docker compose restart prometheus if the reload endpoint isn’t enabled). Common additions include exporters for specific databases, blackbox_exporter for HTTP-level checks, and custom exporters that homelab software increasingly ships with by default. The scrape config grows one job at a time; the docker-compose.yml rarely needs to change once the pattern is set.

Exposing Grafana securely

Port 3000 open directly to the internet is a bad idea for anything with login credentials attached to your infrastructure, and Grafana is no exception. If dashboards need to be reachable outside the local network, put a reverse proxy in front of it instead of forwarding the port directly. Nginx Proxy Manager is a common choice for this: it handles the Let’s Encrypt certificate and lets Grafana sit on a subdomain over HTTPS without touching port 3000 from the outside at all. For most homelabs, though, the better answer is not exposing Grafana publicly in the first place. A WireGuard tunnel back into the home network keeps dashboards private while still reachable from a phone.

Keeping the stack running

Editing docker-compose.yml by hand and re-running docker compose up -d works fine, but it stops being fun once there are more than a few stacks to manage. Portainer gives this stack a web UI for updating images, checking logs, and restarting individual containers without SSH-ing in every time. It’s not required, but it’s worth knowing about before the homelab grows past this one stack.

Once it’s running, the natural next question is whether Prometheus and Grafana alone cover what actually needs watching, or whether uptime checks and public status pages belong in the mix too. That’s a different tool with a different job, and worth its own guide.

Related guides