Overview
This page covers backing up and restoring Grafana: the underlying database, dashboards and data sources exported via the API, the grafana.ini configuration, installed plugins, and provisioning definitions kept as code. For the shared backup infrastructure the whole monitoring stack relies on — off-site storage (S3, MinIO), encryption, disaster recovery testing, and the combined automation scripts — see the Prometheus Backup and Recovery page.
Grafana Backups
Database Backups
SQLite Backup
#!/bin/bash
# grafana-sqlite-backup.sh
set -euo pipefail
GRAFANA_DB="/var/lib/grafana/grafana.db"
BACKUP_DIR="/backups/grafana"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/grafana-db-${TIMESTAMP}.db"
mkdir -p "${BACKUP_DIR}"
# Create SQLite backup
sqlite3 "${GRAFANA_DB}" ".backup '${BACKUP_FILE}'"
# Compress backup
gzip "${BACKUP_FILE}"
echo "Grafana SQLite backup completed: ${BACKUP_FILE}.gz"
PostgreSQL Backup
#!/bin/bash
# grafana-postgres-backup.sh
set -euo pipefail
DB_HOST="${DB_HOST:-postgres}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:-grafana}"
DB_USER="${DB_USER:-grafana}"
DB_PASSWORD="${DB_PASSWORD}"
BACKUP_DIR="/backups/grafana"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/grafana-db-${TIMESTAMP}.sql"
mkdir -p "${BACKUP_DIR}"
# Create PostgreSQL dump
export PGPASSWORD="${DB_PASSWORD}"
pg_dump -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" \
--format=custom \
--compress=9 \
--file="${BACKUP_FILE}.dump"
# Create plain SQL backup for readability
pg_dump -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${DB_NAME}" \
--format=plain \
--file="${BACKUP_FILE}"
gzip "${BACKUP_FILE}"
echo "Grafana PostgreSQL backup completed: ${BACKUP_FILE}.dump and ${BACKUP_FILE}.gz"
MySQL Backup
#!/bin/bash
# grafana-mysql-backup.sh
set -euo pipefail
DB_HOST="${DB_HOST:-mysql}"
DB_PORT="${DB_PORT:-3306}"
DB_NAME="${DB_NAME:-grafana}"
DB_USER="${DB_USER:-grafana}"
DB_PASSWORD="${DB_PASSWORD}"
BACKUP_DIR="/backups/grafana"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/grafana-db-${TIMESTAMP}.sql"
mkdir -p "${BACKUP_DIR}"
# Create MySQL dump
mysqldump -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" \
--single-transaction \
--routines \
--triggers \
"${DB_NAME}" > "${BACKUP_FILE}"
gzip "${BACKUP_FILE}"
echo "Grafana MySQL backup completed: ${BACKUP_FILE}.gz"
Dashboard Exports via API
Note
The scripts below authenticate with Authorization: Bearer ${GRAFANA_API_KEY}. Grafana API keys are deprecated in favor of service-account tokens (Grafana 9+). Create a service account with the required role and issue a token for it, then set GRAFANA_API_KEY to that token — the Authorization: Bearer header is identical.
Tip
Instead of hand-rolling these API scripts, consider the community grafana-backup-tool (grafana-backup), which exports and restores dashboards, folders, data sources, alert rules, and more via the Grafana API in one command.
#!/bin/bash
# grafana-dashboard-backup.sh
# Export all Grafana dashboards via API
set -euo pipefail
GRAFANA_URL="${GRAFANA_URL:-http://grafana:3000}"
GRAFANA_API_KEY="${GRAFANA_API_KEY}"
BACKUP_DIR="/backups/grafana/dashboards"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_PATH="${BACKUP_DIR}/${TIMESTAMP}"
mkdir -p "${BACKUP_PATH}"
Log()
{
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}
Log "Starting Grafana dashboard backup..."
# Get all dashboard UIDs
Log "Fetching dashboard list..."
DASHBOARDS=$(curl -s -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
"${GRAFANA_URL}/api/search?type=dash-db" | jq -r '.[] | .uid')
if [ -z "${DASHBOARDS}" ]; then
Log "No dashboards found"
exit 0
fi
DASHBOARD_COUNT=$(echo "${DASHBOARDS}" | wc -l)
Log "Found ${DASHBOARD_COUNT} dashboards"
# Export each dashboard
COUNT=0
for uid in ${DASHBOARDS}; do
COUNT=$((COUNT + 1))
Log "Exporting dashboard ${COUNT}/${DASHBOARD_COUNT}: ${uid}"
# Get dashboard JSON
dashboard_json=$(curl -s -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
"${GRAFANA_URL}/api/dashboards/uid/${uid}")
# Extract dashboard title for filename
title=$(echo "${dashboard_json}" | jq -r '.meta.slug')
# Save dashboard JSON
echo "${dashboard_json}" | jq '.dashboard' > "${BACKUP_PATH}/${title}-${uid}.json"
done
# Export data sources
Log "Exporting data sources..."
curl -s -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
"${GRAFANA_URL}/api/datasources" | jq '.' > "${BACKUP_PATH}/datasources.json"
# Export alert notification channels
# NOTE: /api/alert-notifications is a LEGACY-ALERTING endpoint. Under Grafana
# unified alerting (the default since Grafana 9) it returns nothing useful and
# does NOT capture contact points or notification policies. Unified-alerting
# config lives in the database and is provisioned via provisioning/alerting/;
# backing up the DB + the provisioning directory captures it (see below).
Log "Exporting notification channels (legacy alerting only)..."
curl -s -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
"${GRAFANA_URL}/api/alert-notifications" | jq '.' > "${BACKUP_PATH}/notification-channels.json"
# Export folders
Log "Exporting folders..."
curl -s -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
"${GRAFANA_URL}/api/folders" | jq '.' > "${BACKUP_PATH}/folders.json"
# Create archive
Log "Creating archive..."
cd "${BACKUP_DIR}"
tar -czf "grafana-dashboards-${TIMESTAMP}.tar.gz" "${TIMESTAMP}"
rm -rf "${TIMESTAMP}"
Log "Dashboard backup completed: grafana-dashboards-${TIMESTAMP}.tar.gz"
Automated Grafana Backup
#!/bin/bash
# grafana-full-backup.sh
# Complete Grafana backup including database and dashboards
set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/backups/grafana}"
GRAFANA_URL="${GRAFANA_URL:-http://grafana:3000}"
GRAFANA_API_KEY="${GRAFANA_API_KEY}"
DB_TYPE="${DB_TYPE:-postgres}"
S3_BUCKET="${S3_BUCKET:-s3://grafana-backups}"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
Log()
{
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}
Error()
{
Log "ERROR: $*" >&2
exit 1
}
mkdir -p "${BACKUP_DIR}"
Log "Starting full Grafana backup..."
# Backup database
Log "Backing up database..."
case "${DB_TYPE}" in
postgres)
pg_dump -h "${DB_HOST}" -U "${DB_USER}" -d "${DB_NAME}" \
--format=custom --compress=9 \
--file="${BACKUP_DIR}/grafana-db-${TIMESTAMP}.dump"
;;
mysql)
mysqldump -h "${DB_HOST}" -u "${DB_USER}" -p"${DB_PASSWORD}" \
--single-transaction "${DB_NAME}" | gzip > "${BACKUP_DIR}/grafana-db-${TIMESTAMP}.sql.gz"
;;
sqlite)
sqlite3 "${GRAFANA_DB}" ".backup '${BACKUP_DIR}/grafana-db-${TIMESTAMP}.db'"
gzip "${BACKUP_DIR}/grafana-db-${TIMESTAMP}.db"
;;
*)
Error "Unknown database type: ${DB_TYPE}"
;;
esac
# Backup dashboards via API
Log "Backing up dashboards..."
DASHBOARD_DIR="${BACKUP_DIR}/dashboards-${TIMESTAMP}"
mkdir -p "${DASHBOARD_DIR}"
# Export all dashboards
DASHBOARDS=$(curl -s -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
"${GRAFANA_URL}/api/search?type=dash-db" | jq -r '.[] | .uid')
for uid in ${DASHBOARDS}; do
curl -s -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
"${GRAFANA_URL}/api/dashboards/uid/${uid}" | \
jq '.dashboard' > "${DASHBOARD_DIR}/${uid}.json"
done
# Export configuration
curl -s -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
"${GRAFANA_URL}/api/datasources" > "${DASHBOARD_DIR}/datasources.json"
# LEGACY-ALERTING endpoint only — see note above. With unified alerting, contact
# points and notification policies are captured by the DB dump plus the
# provisioning/alerting/ directory, not by this call.
curl -s -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
"${GRAFANA_URL}/api/alert-notifications" > "${DASHBOARD_DIR}/notifications.json"
curl -s -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
"${GRAFANA_URL}/api/folders" > "${DASHBOARD_DIR}/folders.json"
# Create archive
tar -czf "${BACKUP_DIR}/grafana-dashboards-${TIMESTAMP}.tar.gz" -C "${BACKUP_DIR}" "dashboards-${TIMESTAMP}"
rm -rf "${DASHBOARD_DIR}"
# Backup provisioning files
Log "Backing up provisioning files..."
if [ -d "/etc/grafana/provisioning" ]; then
tar -czf "${BACKUP_DIR}/grafana-provisioning-${TIMESTAMP}.tar.gz" -C /etc/grafana provisioning
fi
# Backup the main configuration file (grafana.ini). The dashboard/provisioning
# exports above do NOT include it, yet it holds server, database, auth, SMTP
# and feature settings that must be restored for a faithful rebuild.
Log "Backing up grafana.ini..."
GRAFANA_INI="${GRAFANA_INI:-/etc/grafana/grafana.ini}"
if [ -f "${GRAFANA_INI}" ]; then
cp "${GRAFANA_INI}" "${BACKUP_DIR}/grafana-ini-${TIMESTAMP}.ini"
fi
# Backup the installed plugins directory. Plugins are not stored in the DB, so
# a DB-only restore would silently drop panels/data sources they provide.
Log "Backing up plugins..."
GRAFANA_PLUGINS_DIR="${GRAFANA_PLUGINS_DIR:-/var/lib/grafana/plugins}"
if [ -d "${GRAFANA_PLUGINS_DIR}" ]; then
tar -czf "${BACKUP_DIR}/grafana-plugins-${TIMESTAMP}.tar.gz" \
-C "$(dirname "${GRAFANA_PLUGINS_DIR}")" "$(basename "${GRAFANA_PLUGINS_DIR}")"
fi
# Create manifest
Log "Creating backup manifest..."
cat > "${BACKUP_DIR}/manifest-${TIMESTAMP}.json" <<EOF
{
"timestamp": "${TIMESTAMP}",
"grafana_version": "$(curl -s ${GRAFANA_URL}/api/health | jq -r '.version')",
"database_type": "${DB_TYPE}",
"files": {
"database": "grafana-db-${TIMESTAMP}.dump",
"dashboards": "grafana-dashboards-${TIMESTAMP}.tar.gz",
"provisioning": "grafana-provisioning-${TIMESTAMP}.tar.gz",
"config": "grafana-ini-${TIMESTAMP}.ini",
"plugins": "grafana-plugins-${TIMESTAMP}.tar.gz"
}
}
EOF
# Upload to S3
if [ -n "${S3_BUCKET}" ]; then
Log "Uploading to S3..."
aws s3 sync "${BACKUP_DIR}" "${S3_BUCKET}/$(date +%Y/%m/%d)/" \
--exclude "*" \
--include "*-${TIMESTAMP}.*"
fi
Log "Full Grafana backup completed"
Provisioning as Code
# provisioning/dashboards/dashboard.yml
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards
foldersFromFilesStructure: true
# provisioning/datasources/datasource.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
- name: Thanos
type: prometheus
access: proxy
url: http://thanos-query:9090
editable: false
Restore Procedures
Grafana Database Restore
PostgreSQL Restore
#!/bin/bash
# grafana-postgres-restore.sh
set -euo pipefail
BACKUP_FILE="$1"
DB_HOST="${DB_HOST:-postgres}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:-grafana}"
DB_USER="${DB_USER:-grafana}"
DB_PASSWORD="${DB_PASSWORD}"
if [ -z "${BACKUP_FILE}" ]; then
echo "Usage: $0 <backup-file.dump>"
exit 1
fi
echo "Stopping Grafana..."
docker stop grafana-1 grafana-2 grafana-3 || true
echo "Dropping existing database..."
export PGPASSWORD="${DB_PASSWORD}"
psql -h "${DB_HOST}" -U "${DB_USER}" -c "DROP DATABASE IF EXISTS ${DB_NAME};"
psql -h "${DB_HOST}" -U "${DB_USER}" -c "CREATE DATABASE ${DB_NAME};"
echo "Restoring database..."
pg_restore -h "${DB_HOST}" -U "${DB_USER}" -d "${DB_NAME}" \
--no-owner --no-privileges "${BACKUP_FILE}"
echo "Starting Grafana..."
docker start grafana-1 grafana-2 grafana-3
echo "Restore completed"
MySQL Restore
#!/bin/bash
# grafana-mysql-restore.sh
set -euo pipefail
BACKUP_FILE="$1"
DB_HOST="${DB_HOST:-mysql}"
DB_NAME="${DB_NAME:-grafana}"
DB_USER="${DB_USER:-grafana}"
DB_PASSWORD="${DB_PASSWORD}"
if [ -z "${BACKUP_FILE}" ]; then
echo "Usage: $0 <backup-file.sql.gz>"
exit 1
fi
echo "Stopping Grafana..."
docker stop grafana || true
echo "Restoring database..."
zcat "${BACKUP_FILE}" | mysql -h "${DB_HOST}" -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}"
echo "Starting Grafana..."
docker start grafana
echo "Restore completed"
Dashboard Restore
#!/bin/bash
# grafana-dashboard-restore.sh
set -euo pipefail
BACKUP_ARCHIVE="$1"
GRAFANA_URL="${GRAFANA_URL:-http://grafana:3000}"
GRAFANA_API_KEY="${GRAFANA_API_KEY}"
if [ -z "${BACKUP_ARCHIVE}" ]; then
echo "Usage: $0 <backup-archive.tar.gz>"
exit 1
fi
echo "Extracting backup..."
TEMP_DIR=$(mktemp -d)
tar -xzf "${BACKUP_ARCHIVE}" -C "${TEMP_DIR}"
DASHBOARD_DIR=$(find "${TEMP_DIR}" -type d -name "dashboards-*" | head -1)
if [ -z "${DASHBOARD_DIR}" ]; then
echo "No dashboard directory found in backup"
exit 1
fi
# Restore data sources first
echo "Restoring data sources..."
if [ -f "${DASHBOARD_DIR}/datasources.json" ]; then
jq -c '.[]' "${DASHBOARD_DIR}/datasources.json" | while read -r datasource; do
curl -X POST -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
-H "Content-Type: application/json" \
-d "${datasource}" \
"${GRAFANA_URL}/api/datasources"
done
fi
# Restore folders
echo "Restoring folders..."
if [ -f "${DASHBOARD_DIR}/folders.json" ]; then
jq -c '.[]' "${DASHBOARD_DIR}/folders.json" | while read -r folder; do
curl -X POST -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
-H "Content-Type: application/json" \
-d "${folder}" \
"${GRAFANA_URL}/api/folders"
done
fi
# Restore dashboards
echo "Restoring dashboards..."
find "${DASHBOARD_DIR}" -name "*.json" ! -name "datasources.json" ! -name "notifications.json" ! -name "folders.json" | while read -r dashboard_file; do
dashboard_json=$(cat "${dashboard_file}")
payload=$(jq -n --argjson dashboard "${dashboard_json}" '{dashboard: $dashboard, overwrite: true}')
curl -X POST -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
-H "Content-Type: application/json" \
-d "${payload}" \
"${GRAFANA_URL}/api/dashboards/db"
done
# Restore notification channels
# NOTE: /api/alert-notifications is LEGACY ALERTING only. If you use unified
# alerting (default since Grafana 9), restore contact points and notification
# policies from the database dump and/or the provisioning/alerting/ directory
# instead — this call will not recreate them.
echo "Restoring notification channels (legacy alerting only)..."
if [ -f "${DASHBOARD_DIR}/notifications.json" ]; then
jq -c '.[]' "${DASHBOARD_DIR}/notifications.json" | while read -r notification; do
curl -X POST -H "Authorization: Bearer ${GRAFANA_API_KEY}" \
-H "Content-Type: application/json" \
-d "${notification}" \
"${GRAFANA_URL}/api/alert-notifications"
done
fi
# Clean up
rm -rf "${TEMP_DIR}"
echo "Dashboard restore completed"
Shared Backup Infrastructure
Grafana backups rely on the same stack-wide backup infrastructure documented on the Prometheus Backup and Recovery page. Refer there for:
- Backup Storage — off-site S3 and MinIO uploads plus GPG encryption for all backups.
- Disaster Recovery Testing — DR drill and RTO/RPO scripts that cover both Grafana and Prometheus.
- Automation Scripts — the combined
backup-all.shorchestration and cron schedule that invokegrafana-full-backup.shalongside the Prometheus and Alertmanager backups.