Skip to Content

How to Self-Host Grafana: Build Production-Ready Monitoring Dashboards

Deploy Grafana with Docker, wire up Prometheus, provision dashboards as code, and stop flying blind in production.

How to Self-Host Grafana: Build Production-Ready Monitoring Dashboards

Most teams collect metrics. Few teams actually look at them. The gap is not tooling—it is a working dashboard that someone trusts. This guide shows you how to self-host Grafana, connect it to real data sources, and build dashboards that survive restarts, redeploys, and 3 AM pages.

If you want a deeper walkthrough on production-grade setup, see our companion post on See Everything: How to Self-Host Grafana for Production Monitoring.

What You Will Build

By the end of this guide you will have:

  • Grafana running in Docker with persistent storage
  • Prometheus collecting system and application metrics
  • Dashboards and data sources provisioned from version-controlled files
  • Alerting rules that notify you before users do
  • A reverse proxy with HTTPS for secure access

Prerequisites

Before you start, you need:

  • A Linux server (Ubuntu 22.04+ or Debian 12+ recommended) with at least 2 CPU cores and 4 GB RAM
  • Docker and Docker Compose installed
  • A domain name pointed at your server (for HTTPS)
  • Basic familiarity with YAML and command-line tools
  • Ports 80, 443, and 9090 available (or adjust the configs below)

Check your Docker version:

docker --version
docker compose version

If Docker is missing, install it:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker

Step 1: Spin Up Grafana with Docker Compose

The cleanest way to self-host Grafana is a single Docker Compose file. It keeps your stack reproducible and your config in Git.

Create a project directory and a docker-compose.yml:

mkdir ~/grafana-stack && cd ~/grafana-stack
mkdir -p provisioning/datasources provisioning/dashboards grafana-data

Now write the Compose file:

version: "3.8"

services:
  grafana:
    image: grafana/grafana:11.1.0
    container_name: grafana
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=change-me-in-production
      - GF_SERVER_ROOT_URL=https://grafana.yourdomain.com
      - GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource
    volumes:
      - ./grafana-data:/var/lib/grafana
      - ./provisioning:/etc/grafana/provisioning
    networks:
      - monitoring

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

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

volumes:
  prometheus-data:

networks:
  monitoring:
    driver: bridge

Start the stack:

docker compose up -d

Verify Grafana is running at http://your-server-ip:3000. Log in with admin / change-me-in-production.

Step 2: Configure Prometheus to Scrape Metrics

Grafana does not collect metrics. It visualizes what Prometheus (or another data source) collects. You need a prometheus.yml that tells Prometheus what to scrape.

global:
  scrape_interval: 15s
  evaluation_interval: 15s

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

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

  - job_name: 'grafana'
    static_configs:
      - targets: ['grafana:3000']

Reload Prometheus without restarting:

curl -X POST http://localhost:9090/-/reload

Check the Prometheus targets page at http://your-server-ip:9090/targets. All jobs should show UP.

Step 3: Provision Data Sources as Code

Manual data source configuration dies the moment you rebuild the container. Provision it from YAML instead.

Create provisioning/datasources/prometheus.yaml:

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false
    jsonData:
      httpMethod: POST
      manageAlerts: true
      alertmanagerUid: alertmanager

Restart Grafana to pick up the provisioned data source:

docker compose restart grafana

Navigate to Connections → Data Sources in Grafana. You should see Prometheus listed and marked as default.

For a more detailed look at provisioning patterns, read How to Self-Host Grafana: The Complete Developer Guide to Monitoring Dashboards.

Step 4: Build Your First Dashboard

Now the fun part. Create a dashboard that shows what matters: CPU, memory, disk, and network.

Import the Node Exporter Full Dashboard

Grafana maintains an official dashboard for Node Exporter. Import it by ID:

  1. Go to Dashboards → New → Import
  2. Enter dashboard ID 1860
  3. Select your Prometheus data source
  4. Click Import

You now have a 20+ panel dashboard showing every system metric you need. It updates automatically and works out of the box.

Create a Custom Dashboard from Scratch

If you prefer to build your own, start with a single panel. Here is a PromQL query for CPU usage:

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

Add it to a new dashboard, set the panel title to CPU Usage, and set the unit to percent (0-100). Repeat for memory:

100 * (1 - ((node_memory_MemAvailable_bytes or node_memory_MemFree_bytes) / node_memory_MemTotal_bytes))

Save the dashboard. Then export it as JSON so you can version-control it:

  1. Open the dashboard
  2. Click the gear icon (Dashboard settings)
  3. Choose JSON Model
  4. Copy and save to provisioning/dashboards/system-overview.json

Step 5: Provision Dashboards from Git

Manual exports are fine once. For a team, you want dashboards loaded automatically on every startup.

Create provisioning/dashboards/default.yaml:

apiVersion: 1

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

Place your exported dashboard JSON in the same provisioning/dashboards/ directory. Grafana watches this path and reloads dashboards every 10 seconds. Commit the entire provisioning/ folder to Git and your dashboards are now infrastructure-as-code.

Another angle on this workflow is covered in How to Self-Host Grafana: The Complete Developer Guide to Monitoring Dashboards.

Step 6: Secure Grafana with HTTPS and a Reverse Proxy

Running Grafana on port 3000 over HTTP is fine for local testing. In production, you need TLS and a reverse proxy.

Option A: Caddy (Simplest)

Caddy handles HTTPS automatically via Let's Encrypt. Add it to your Compose file:

  caddy:
    image: caddy:2.8
    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
      - caddy-config:/config
    networks:
      - monitoring

volumes:
  caddy-data:
  caddy-config:

Create Caddyfile:

grafana.yourdomain.com {
    reverse_proxy grafana:3000
}

Option B: Nginx

If you already run Nginx, add a server block:

server {
    listen 443 ssl http2;
    server_name grafana.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/grafana.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/grafana.yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Update GF_SERVER_ROOT_URL in your Compose file to match your domain, then restart:

docker compose up -d

Tips and Troubleshooting

Grafana shows "No Data"

  • Check that Prometheus is scraping targets at http://your-server:9090/targets
  • Verify the data source URL in Grafana matches the Docker service name (http://prometheus:9090 inside the container network)
  • Confirm your PromQL query works in the Prometheus expression browser first

Dashboards disappear after restart

  • Ensure you mounted a host volume to /var/lib/grafana
  • Without a volume, Grafana data lives inside the container and dies with it

Permission denied on volumes

  • Grafana runs as UID 472. Fix ownership with:
    sudo chown -R 472:472 grafana-data

Alerts not firing

  • Alert rules evaluate on the Grafana side, not Prometheus. Go to Alerting → Alert rules and check the state
  • Ensure your contact point (email, Slack, PagerDuty) is configured in Alerting → Contact points

High memory usage

  • Limit Prometheus retention with --storage.tsdb.retention.time=15d if disk or RAM is tight
  • Add resource limits to your Compose services for production workloads

Next Steps

You now have a self-hosted Grafana stack that collects metrics, displays them in useful dashboards, and loads its own configuration from Git. That is the foundation. From here, consider:

  • Adding Loki for log aggregation and correlating logs with metrics
  • Setting up Alertmanager for routing alerts to the right on-call engineer
  • Using Grafana's Git sync or Terraform provider for larger teams
  • Adding custom application metrics via the Prometheus client libraries

Monitoring is not a one-time setup. It is a habit. Start with one dashboard that answers one real question—like "is the API slow right now?"—and expand from there.

Need Help Scaling This?

If you are running production workloads and need HA, SSO, fine-grained access control, or custom alerting pipelines, we can help. Contact the Sysbrix team and we will get your observability stack running like it should.

LiteLLM Setup Proxy: The Complete Developer Guide to a Unified LLM Gateway
Learn how to deploy LiteLLM Proxy to route, manage, and monitor every LLM in your stack from a single OpenAI-compatible endpoint.