Table of Contents

Overview

High Availability (HA) for Grafana keeps the visualization and alerting UI online during failures by running multiple Grafana instances behind a load balancer, all sharing a single SQL database for application state (users, sessions/auth tokens, dashboards). This page covers the Grafana-specific clustering and load balancing configuration.

Key Components:

  • Grafana Clustering: Shared SQL database for application state (users, sessions/auth tokens, dashboards), with an optional remote_cache (e.g. Redis) for caching
  • Load Balancing: Distribute traffic across instances
Note

The metrics pipeline side of HA — redundant Prometheus instances, Thanos long-term storage, federation, remote storage, and disaster recovery — is documented separately in Prometheus High Availability.

Grafana Clustering

Database Backend

Grafana requires a shared database for clustering.

Important

The default SQLite backend cannot be used for HA — it is a single-node, file-based database. A multi-instance (clustered) Grafana deployment requires a shared MySQL or PostgreSQL database so that every instance reads and writes the same application state.

PostgreSQL Setup

# docker-compose.yml

services:
  postgres:
    image: postgres:16-alpine
    container_name: grafana-postgres
    restart: unless-stopped
    environment:
      POSTGRES_DB: grafana
      POSTGRES_USER: grafana
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init-db.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    networks:
      - monitoring
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U grafana"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: grafana-redis
    restart: unless-stopped
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis-data:/data
    ports:
      - "6379:6379"
    networks:
      - monitoring
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  grafana-1:
    image: grafana/grafana:11.2.0
    container_name: grafana-1
    restart: unless-stopped
    environment:
      GF_DATABASE_TYPE: postgres
      GF_DATABASE_HOST: postgres:5432
      GF_DATABASE_NAME: grafana
      GF_DATABASE_USER: grafana
      GF_DATABASE_PASSWORD: ${DB_PASSWORD}
      GF_REMOTE_CACHE_TYPE: redis
      GF_REMOTE_CACHE_CONNSTR: addr=redis:6379,password=${REDIS_PASSWORD},db=0
      GF_SERVER_ROOT_URL: https://grafana.example.com
      GF_SECURITY_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      GF_INSTALL_PLUGINS: grafana-piechart-panel
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - grafana-1-logs:/var/log/grafana
    ports:
      - "3000:3000"
    networks:
      - monitoring
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  grafana-2:
    image: grafana/grafana:11.2.0
    container_name: grafana-2
    restart: unless-stopped
    environment:
      GF_DATABASE_TYPE: postgres
      GF_DATABASE_HOST: postgres:5432
      GF_DATABASE_NAME: grafana
      GF_DATABASE_USER: grafana
      GF_DATABASE_PASSWORD: ${DB_PASSWORD}
      GF_REMOTE_CACHE_TYPE: redis
      GF_REMOTE_CACHE_CONNSTR: addr=redis:6379,password=${REDIS_PASSWORD},db=0
      GF_SERVER_ROOT_URL: https://grafana.example.com
      GF_SECURITY_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      GF_INSTALL_PLUGINS: grafana-piechart-panel
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - grafana-2-logs:/var/log/grafana
    ports:
      - "3001:3000"
    networks:
      - monitoring
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  grafana-3:
    image: grafana/grafana:11.2.0
    container_name: grafana-3
    restart: unless-stopped
    environment:
      GF_DATABASE_TYPE: postgres
      GF_DATABASE_HOST: postgres:5432
      GF_DATABASE_NAME: grafana
      GF_DATABASE_USER: grafana
      GF_DATABASE_PASSWORD: ${DB_PASSWORD}
      GF_REMOTE_CACHE_TYPE: redis
      GF_REMOTE_CACHE_CONNSTR: addr=redis:6379,password=${REDIS_PASSWORD},db=0
      GF_SERVER_ROOT_URL: https://grafana.example.com
      GF_SECURITY_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      GF_INSTALL_PLUGINS: grafana-piechart-panel
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - grafana-3-logs:/var/log/grafana
    ports:
      - "3002:3000"
    networks:
      - monitoring
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

volumes:
  postgres-data:
  redis-data:
  grafana-1-logs:
  grafana-2-logs:
  grafana-3-logs:

networks:
  monitoring:
    driver: bridge

Load Balancing

Nginx Configuration

# nginx.conf
upstream grafana_backend {
    least_conn;
    server grafana-1:3000 max_fails=3 fail_timeout=30s;
    server grafana-2:3000 max_fails=3 fail_timeout=30s;
    server grafana-3:3000 max_fails=3 fail_timeout=30s;
}

server {
    listen 80;
    server_name grafana.example.com;
    return 301 https://$server_name$request_uri;
}

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

    ssl_certificate /etc/nginx/ssl/grafana.crt;
    ssl_certificate_key /etc/nginx/ssl/grafana.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://grafana_backend;
        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;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # Health check
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
    }

    location /api/live/ {
        proxy_pass http://grafana_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}

HAProxy Configuration

# haproxy.cfg
global
    log /dev/log local0
    log /dev/log local1 notice
    maxconn 4096
    tune.ssl.default-dh-param 2048

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    timeout connect 5000
    timeout client  50000
    timeout server  50000

frontend grafana_frontend
    bind *:80
    bind *:443 ssl crt /etc/ssl/certs/grafana.pem
    redirect scheme https if !{ ssl_fc }
    default_backend grafana_backend

backend grafana_backend
    balance leastconn
    option httpchk GET /api/health
    http-check expect status 200
    server grafana-1 grafana-1:3000 check inter 5s fall 3 rise 2
    server grafana-2 grafana-2:3000 check inter 5s fall 3 rise 2
    server grafana-3 grafana-3:3000 check inter 5s fall 3 rise 2

listen stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 30s
    stats admin if LOCALHOST

Metrics Pipeline HA

Grafana is the visualization layer of the monitoring stack. Making the underlying metrics pipeline highly available — running multiple Prometheus replicas, deduplicating with Thanos Query, long-term storage in object storage, hierarchical federation, remote storage backends (Cortex, VictoriaMetrics), and multi-region disaster recovery — is covered in a dedicated page:

Troubleshooting

Grafana Clustering Issues

# Check database connectivity
docker logs grafana-1 | grep database

# Verify Redis connection (Redis is used as the optional remote_cache, not for sessions)
redis-cli -h redis -a $REDIS_PASSWORD ping

# Inspect the remote_cache contents (cache entries only).
# NOTE: Grafana >= 9 does NOT store login/auth sessions as Redis "session:*" keys.
# Session/auth tokens live in the shared SQL database (auth_token table); Redis
# here is only the remote_cache for caching, not session storage.
redis-cli -h redis -a $REDIS_PASSWORD keys "*"

# Verify all instances use same database
docker exec grafana-1 grep GF_DATABASE /etc/grafana/grafana.ini

See Also