VPS Snaps

How to back up Grafana

Grafana keeps dashboards, users, data sources, alert rules and nearly everything else in one database: SQLite at /var/lib/grafana/grafana.db by default, or MySQL or PostgreSQL. Back it up with the sqlite3 .backup command or a database dump, together with /etc/grafana/grafana.ini, whose secret_key is needed to decrypt every stored data source password, plus the provisioning and plugins folders. Dashboard JSON exported through the HTTP API is useful to have, but it is not a backup of the server.

8 min readUpdated Checked against official documentation

What to back up

Paths are those of the deb and rpm packages, which run Grafana as the grafana user under the grafana-server service. The official Docker image uses the same paths inside the container.

PartPathWhat it holds
Database/var/lib/grafana/grafana.dbDashboards and their version history, folders, users, teams, organizations, permissions, data sources with encrypted secrets, alert rules, contact points, notification policies, library panels, annotations, service accounts
Config/etc/grafana/grafana.inisecret_key, database and SMTP passwords, sign-in settings
Overrides/etc/default/grafana-server (deb), /etc/sysconfig/grafana-server (rpm)Paths, and any GF_<SECTION>_<KEY> variables, which override grafana.ini
Provisioning/etc/grafana/provisioningYAML for data sources, dashboards, alerting and plugins, applied at every start
Plugins/var/lib/grafana/pluginsInstalled plugin files

On a binary install the same parts are conf/custom.ini and data/ in the install folder. If type under [database] is mysql or postgres, the database lives on that server and you dump it instead; Grafana's configuration reference recommends one of those over SQLite in production.

The secret_key

Grafana encrypts the secrets it stores: data source passwords and tokens, contact point credentials. Since v9.0 it uses envelope encryption: each secret is encrypted with a data key kept in the database, and the data keys are encrypted with secret_key from the [security] section. A copy of grafana.db is useless for those secrets without that key.

Terminal
grep -in 'secret_key' /etc/grafana/grafana.ini /etc/default/grafana-server

-i also catches GF_SECURITY_SECRET_KEY. A line starting with ; is commented out, so the default from defaults.ini applies, and a new server has the same default. Grafana 13's defaults.ini also has a [secrets_manager.encryption.secret_key.v1] section with its own secret_key; if you changed either, keep both. In Docker, look for GF_SECURITY_SECRET_KEY in the compose file or .env.

With a different secret_key, Grafana cannot decrypt the passwords and tokens it stored, so data sources and other connections that use them stop working, and Grafana may not say why at startup. Put the original key back; the alternative is re-entering every password.

Copy grafana.db while Grafana runs

Grafana's backup guide says to stop Grafana before copying the SQLite file. To copy it while it runs, use SQLite's backup API through the sqlite3 shell (apt install sqlite3); cp can catch the file mid-write (why).

Terminal
install -d -m 700 /var/backups/grafana
Terminal
sqlite3 /var/lib/grafana/grafana.db ".timeout 10000" ".backup '/var/backups/grafana/grafana.db'"
Terminal
sqlite3 /var/backups/grafana/grafana.db "PRAGMA integrity_check;"

The check prints ok. .timeout 10000 waits up to 10 seconds for Grafana's write lock instead of failing with database is locked; Grafana leaves WAL off by default (wal = false), so a writer briefly blocks readers. With MySQL or PostgreSQL, use mysqldump --single-transaction grafana or pg_dump -Fc grafana (guide). Then archive the files, skipping the live database and its journal:

Terminal
tar -czf /var/backups/grafana/grafana-files.tar.gz --exclude='grafana.db*' -C / etc/grafana etc/default/grafana-server var/lib/grafana

-C / stores relative paths, so the archive unpacks back over /.

Automate it

Both steps as one nightly script, with the copy checked:

/usr/local/bin/grafana-backup.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077

OUT=/var/backups/grafana
STAMP=$(date +%F-%H%M)
mkdir -p "$OUT"

sqlite3 /var/lib/grafana/grafana.db ".timeout 10000" ".backup '$OUT/grafana-$STAMP.db'"
if [ "$(sqlite3 "$OUT/grafana-$STAMP.db" 'PRAGMA integrity_check;')" != "ok" ]; then
  echo "integrity check failed: $OUT/grafana-$STAMP.db" >&2
  exit 1
fi
gzip "$OUT/grafana-$STAMP.db"

rc=0
tar -czf "$OUT/grafana-files-$STAMP.tar.gz" --exclude='grafana.db*' \
  -C / etc/grafana etc/default/grafana-server var/lib/grafana || rc=$?
if [ "$rc" -gt 1 ]; then
  exit "$rc"
fi

find "$OUT" -maxdepth 1 -name 'grafana-*' -type f -mtime +7 -delete
/etc/cron.d/grafana-backup
30 3 * * * root /usr/local/bin/grafana-backup.sh >> /var/log/grafana-backup.log 2>&1
  • The copy is checked before it is compressed; a copy that fails integrity_check stops the script.
  • tar exits with 1 when a file changed while it was read, which happens on a running server; anything higher stops the script.
  • Files older than 7 days are deleted only after a successful run. Copy the folder off the machine, for example with rclone; cron schedules covers logging and timing.

Dashboards as code, and why that is not a backup

Provisioned dashboards come from JSON files a provider in /etc/grafana/provisioning/dashboards points at; Grafana reloads them at every start. Keep those files in Git and they restore themselves. To snapshot dashboards made in the UI, create a service account under Administration, Users and access, Service accounts, add a token, save it in /root/.grafana-backup-token (mode 600), and export every dashboard:

/usr/local/bin/grafana-dashboards.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077

GRAFANA_URL="http://localhost:3000"
AUTH="Authorization: Bearer $(cat /root/.grafana-backup-token)"
OUT="/var/backups/grafana/dashboards-$(date +%F)"
mkdir -p "$OUT"

curl -fsS -H "$AUTH" "$GRAFANA_URL/api/search?type=dash-db&limit=5000" |
  jq -r '.[] | select(.type == "dash-db") | .uid' |
  while read -r uid; do
    curl -fsS -H "$AUTH" "$GRAFANA_URL/api/dashboards/uid/$uid" > "$OUT/$uid.json"
  done
  • /api/search returns only what the token may see, in the token's organization, up to limit 5000 per page (page for more).
  • /api/dashboards/uid/<uid> returns {"dashboard": ..., "meta": ...}; meta.folderUid names the folder.
  • curl -f and pipefail make the script fail on any HTTP error instead of saving an error page.
  • Grafana 13 deprecates the /api endpoints in favor of /apis, but still serves them; the new equivalent is /apis/dashboard.grafana.app/v1/namespaces/default/dashboards/<uid>.

To load one back, put the token in TOKEN, set id to null and post it; overwrite replaces a dashboard with the same uid. The folder must already exist:

Terminal
jq '{dashboard: (.dashboard | .id = null), folderUid: .meta.folderUid, overwrite: true}' cIBgcSjkk.json | curl -fsS -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' --data-binary @- http://localhost:3000/api/dashboards/db

The JSON holds layout, panels, queries and variables. It does not hold users, teams, permissions, folders, data sources, alert rules, contact points, library panels, annotations or version history, and the data source API never returns stored passwords, only which fields are set. Restoring a server needs the database.

Docker installs

The image keeps data in /var/lib/grafana, a named volume such as grafana-storage in Grafana's examples, and runs as UID 472. Settings usually come from GF_* variables, so keep the compose file and .env, plus any mounted grafana.ini and provisioning folder. The image has no sqlite3, so run it on the host against the volume's folder:

Terminal
sqlite3 /var/lib/docker/volumes/grafana-storage/_data/grafana.db ".timeout 10000" ".backup '/var/backups/grafana/grafana.db'"

docker volume inspect grafana-storage shows the real Mountpoint; Compose puts the project name in front of the volume's name. To archive the whole volume, stop the container and use a helper container (Docker volume backups). After a restore, the files must belong to UID 472.

Restore onto a new server

Install the same Grafana version or a newer one. A newer version migrates the database when it starts; Grafana's guides treat going back as restoring the pre-upgrade backup, so never start an older version on it. Stop Grafana, unpack the files, put the database back and fix ownership:

Terminal
systemctl stop grafana-server
Terminal
tar -xzf grafana-files.tar.gz -C /
Terminal
cp grafana.db /var/lib/grafana/grafana.db
Terminal
chown -R grafana:grafana /var/lib/grafana
Terminal
systemctl start grafana-server
Terminal
curl -s http://localhost:3000/api/health

In the reply, database should be ok, next to the version. From the nightly script's files, gunzip the .db.gz before copying it. Sign in with the old server's admin password: admin_password in grafana.ini is only used on the very first start. Open a data source and click Save & test; success proves the secret_key matches. With MySQL or PostgreSQL, restore the dump into an empty database before Grafana's first start. Repeat this on a throwaway server now and then (testing restores).

Common errors

ErrorFix
GF_PATHS_DATA='/var/lib/grafana' is not writable.Docker: the restored volume or bind mount belongs to someone else. chown -R 472:0 it.
attempt to write a readonly databasegrafana.db or its folder belongs to root after the copy. Run chown -R grafana:grafana /var/lib/grafana.
database is lockedThe copy waited on Grafana's write lock. Keep .timeout 10000 before .backup, or stop Grafana for the copy.
Data sources fail to log in after the restore, with no clearer errorsecret_key differs from the old server's. Restore the original key.
The admin password from grafana.ini is rejectedThe restored database keeps the old password. grafana cli admin reset-admin-password <new-password> sets a new one.
Could not find config defaults, make sure homepath command line parameter is set or working directory is homepathYou called the binary directly. Add --homepath /usr/share/grafana, or use the package's grafana wrapper, which sets it.
Panels report a missing plugin/var/lib/grafana/plugins was not restored. Restore it or reinstall the same plugin versions.
The export script saves no filesThe token sees no dashboards: wrong organization, or a role without access to the folders.

Frequently asked questions

Where is the Grafana database stored?
With the deb or rpm package and in Docker, at /var/lib/grafana/grafana.db. On a binary install it is data/grafana.db in the install folder, unless [database] in the config points to MySQL or PostgreSQL.
How do I back up all Grafana dashboards?
Back up grafana.db, which holds every dashboard and its history. For portable copies, list them with /api/search and save each one by its uid from /api/dashboards/uid with a service account token.
What happens if I lose Grafana's secret_key?
Stored secrets can no longer be decrypted, so data sources and contact points lose their passwords and you re-enter them. Dashboards, users and settings are unaffected.
Can I restore a Grafana backup on a newer version?
Yes. Grafana migrates the database forward on its first start. Do not go the other way: Grafana's guides treat a rollback as restoring the backup taken before the upgrade.

How this was checked

Commands, limits and prices were checked against these official pages, on October 4, 2026: