Skip to Content

Self-Host Prometheus + Grafana: Complete Monitoring Stack in 20 Minutes

Your homelab is growing. You've got containers spinning up, databases running, reverse proxies routing traffic. But do you actually know what's happening inside? Is your disk filling up? Is that API response time creeping up? Is a container crash-looping at 3 AM?

Prometheus and Grafana answer those questions. Together, they form the industry-standard monitoring stack — Prometheus scrapes and stores metrics, Grafana turns them into beautiful, actionable dashboards. And yes, you can self-host the whole thing in about 20 minutes.

Why Self-Host Your Monitoring Stack?

Cloud monitoring solutions are convenient until they're not. Datadog bills can spiral fast. New Relic's pricing model punishes you for growing. And do you really want your infrastructure metrics sitting on someone else's servers?

Self-hosting Prometheus + Grafana gives you:

  • Full data ownership — your metrics never leave your infrastructure
  • Zero per-metric pricing — monitor as much as you want
  • No vendor lock-in — open-source, portable, future-proof
  • Deep integration — works natively with Docker, Node Exporter, and hundreds of exporters

What You'll Build

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

  • Prometheus scraping metrics from your Docker host and containers
  • Grafana with pre-built dashboards for system and container metrics
  • Alertmanager configured for notifications (optional but recommended)
  • Everything running in Docker Compose with persistent storage

Prerequisites

  • A VPS or homelab server with Docker and Docker Compose installed
  • Minimum 2GB RAM (4GB recommended for longer metric retention)
  • 10GB+ free disk space (metrics add up over time)
  • Basic familiarity with Docker Compose

Step 1: Create the Project Structure

mkdir -p ~/monitoring/{prometheus,grafana/provisioning/datasources,grafana/provisioning/dashboards,grafana/dashboards,alertmanager}
cd ~/monitoring

Step 2: Configure Prometheus

Create prometheus/prometheus.yml:

global:
  scrape_interval: 15s
  evaluation_interval: 15s

alerting:
  alertmanagers:
    - static_configs:
        - targets: ['alertmanager:9093']

rule_files:
  - "alert_rules.yml"

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']

  - job_name: 'docker'
    static_configs:
      - targets: ['host.docker.internal:9323']

Create prometheus/alert_rules.yml with some basic alerts:

groups:
  - name: system_alerts
    rules:
      - alert: HighCPUUsage
        expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High CPU usage on {{ $labels.instance }}"

      - alert: LowDiskSpace
        expr: (node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100 < 15
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Low disk space on {{ $labels.instance }}"

      - alert: ContainerDown
        expr: up{job="cadvisor"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "cAdvisor target is down"

Step 3: Configure Grafana Provisioning

Create grafana/provisioning/datasources/datasource.yml:

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: true

Create grafana/provisioning/dashboards/dashboard.yml:

apiVersion: 1

providers:
  - name: 'default'
    orgId: 1
    folder: ''
    type: file
    disableDeletion: false
    updateIntervalSeconds: 10
    allowUiUpdates: true
    options:
      path: /etc/grafana/provisioning/dashboards
      foldersFromFilesStructure: true

Step 4: Docker Compose Stack

Create docker-compose.yml in the project root:

version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prometheus/alert_rules.yml:/etc/prometheus/alert_rules.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=30d'
      - '--web.enable-lifecycle'
    ports:
      - "9090:9090"
    networks:
      - monitoring

  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"
    networks:
      - monitoring

  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
    privileged: true
    devices:
      - /dev/kmsg
    ports:
      - "8080:8080"
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin123
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning
      - ./grafana/dashboards:/var/lib/grafana/dashboards
      - grafana_data:/var/lib/grafana
    ports:
      - "3000:3000"
    networks:
      - monitoring
    depends_on:
      - prometheus

  alertmanager:
    image: prom/alertmanager:latest
    container_name: alertmanager
    restart: unless-stopped
    volumes:
      - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
    ports:
      - "9093:9093"
    networks:
      - monitoring

networks:
  monitoring:
    driver: bridge

volumes:
  prometheus_data:
  grafana_data:

Step 5: Configure Alertmanager

Create alertmanager/alertmanager.yml:

global:
  smtp_smarthost: 'localhost:587'
  smtp_from: '[email protected]'

route:
  receiver: 'default'

receivers:
  - name: 'default'
    # Add your notification config here:
    # email_configs:
    #   - to: '[email protected]'
    # slack_configs:
    #   - api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'
    #     channel: '#alerts'

Step 6: Launch the Stack

docker compose up -d

Verify all containers are running:

docker compose ps

You should see five containers: prometheus, node-exporter, cadvisor, grafana, and alertmanager.

Step 7: Verify Prometheus Is Scraping

Open http://your-server-ip:9090/targets in your browser. You should see all four scrape targets in an UP state:

  • prometheus — Prometheus monitoring itself
  • node-exporter — Host-level metrics (CPU, memory, disk, network)
  • cadvisor — Container-level metrics (per-container CPU, memory, network)
  • docker — Docker daemon metrics (if enabled)

If any target shows DOWN, check the container logs:

docker logs prometheus
docker logs node-exporter

Step 8: Access Grafana and Import Dashboards

Open http://your-server-ip:3000 and log in with admin / admin123.

The Prometheus datasource is already configured via provisioning. Now import some dashboards:

  1. Go to Dashboards → Import
  2. Import ID 1860 for Node Exporter Full — a comprehensive host metrics dashboard
  3. Import ID 14282 for cAdvisor Docker monitoring — per-container stats

Within seconds, you'll see live dashboards with CPU usage, memory consumption, disk I/O, network throughput, and per-container breakdowns.

Step 9: (Optional) Enable Docker Daemon Metrics

To get Docker engine-level metrics, enable the metrics endpoint on your Docker daemon. Edit /etc/docker/daemon.json:

{
  "metrics-addr": "0.0.0.0:9323",
  "experimental": true
}

Restart Docker:

sudo systemctl restart docker

Now the docker job in Prometheus will show engine-level metrics like container counts, image counts, and build info.

Step 10: (Optional) Add HTTPS with a Reverse Proxy

If you're exposing Grafana publicly, put it behind a reverse proxy like Traefik or Nginx with TLS. Check out our guides on self-hosting Traefik for a complete setup.

What's Next?

Now that you have metrics flowing, here are natural next steps:

  • Add more exporters — PostgreSQL, Redis, Nginx, Blackbox exporter for endpoint probing
  • Set up alert notifications — Connect Alertmanager to Slack, Discord, or email
  • Long-term storage — Add Thanos or Mimir for multi-year metric retention
  • Log aggregation — Pair with Loki for a complete observability stack (Grafana's "LGTM" stack)

Monitoring isn't optional once you run more than a couple of services. With Prometheus and Grafana self-hosted, you get enterprise-grade observability without the enterprise-grade invoice.

Happy monitoring. 🔥

Self-Host Supabase: Your Own Open-Source Firebase Alternative in 20 Minutes