ObservabilityLXXII · Grafana HAGrafanaHA
Plugin Consistency
What you'll learn
- Describe how Grafana plugins are loaded from the filesystem and why each replica must match
- Configure the image build to install plugins at build time via GF_INSTALL_PLUGINS
- Disable plugin_admin_enabled in HA so the UI cannot install on one replica only
- Validate the plugin list on every replica using /api/plugins
- Identify the failure modes that follow an unsigned plugin or a missing plugin version
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
A team installs a new datasource plugin through the Admin UI at
14:00. They click “Install” on grafana-01. The plugin downloads,
Grafana restarts, the plugin is registered. They test the new
data source. It works. They click “Save & Test” on a new dashboard
panel. The page redirects to grafana-02 (the load balancer
picks). The browser returns 404: “Plugin not found.” The plugin
is on g1 only. The other replicas have no idea it exists. The
user opens a ticket. The on-call engineer spends 90 minutes
figuring out why the same plugin auths on one replica and 404s
on another.
Plugin consistency is the property that every replica has the same set of plugins, at the same version, with the same signatures and the same configuration. The right shape is to bake the plugins into the image at build time, mount the plugins directory from a shared read-only volume, or use a plugin manager that fans out to every replica. The wrong shape is the Admin UI: it installs on one replica.
What it is
A Grafana plugin is a software module that extends Grafana with
a new data source, panel, app, or renderer. Each plugin is a
directory under /var/lib/grafana/plugins containing a
plugin.json manifest, the binary or JS code, and supporting
files. Grafana reads the directory on boot and on demand.
For HA, the plugin set must be identical across replicas. The filesystem is the source of truth for what is installed. If the files are not there, the plugin is not loaded. Plugins installed at runtime by the Admin UI exist only on the replica that performed the install.
g1 image ---> /var/lib/grafana/plugins/<plugin-a> 1.2.0
g2 image ---> /var/lib/grafana/plugins/<plugin-a> 1.2.0
g3 image ---> /var/lib/grafana/plugins/<plugin-a> 1.2.0
|
+-- grafana.com/plugins/<id>
Every replica has the same set of files.
The Admin UI never installs in HA.
Why a sysadmin cares
The five operational pains that disappear once plugin installation is correct:
- “Plugin not found” on half the dashboard renders. The user opened a dashboard, the load balancer routed to a replica without the plugin, the panel errors out.
- API drift between replicas.
g1exposes the new data source endpoint,g2does not. The/api/datasourcespayload returned byg1is not the same payload returned byg2. - Private plugin license only loaded on one replica. The
plugin key is in
g1’sgrafana.ini(or environment), not ing2’s.g1shows the licensed features,g2shows the read-only banner. - Plugin version drift.
g1has 1.2.0,g2has 1.1.0. A query issued againstg1returns the new schema; the same query againstg2returns the old schema. - Unsigned plugin rejection. A plugin was installed manually by extracting a zip file. Grafana’s signature verification rejects it. The user sees “plugin failed to load” in the log.
How it works
Grafana reads the plugins directory on boot and on the
/admin/plugins reload endpoint. The directory layout for a
single plugin is:
/var/lib/grafana/plugins/
marcusolsson-json-datasource/
plugin.json
module.js
img/
...
The plugin.json declares the plugin id, name, type, version,
and signature. Grafana reads the manifest, loads the binary
(Go plugin or JS module), and registers the plugin with the
runtime.
For Docker deployments, the Grafana image ships an entrypoint
script that reads GF_INSTALL_PLUGINS and installs the listed
plugins before Grafana starts. This is the standard HA path:
the image is built once, every replica runs the same image, the
plugins are installed at container start, every replica has the
same set.
For Kubernetes, the same pattern applies: the image is built with the plugins pre-installed, or the entrypoint is configured to install on each replica. The runtime install via Admin UI is disabled.
How to configure it
The Dockerfile for a production Grafana HA image with plugins baked in:
# Dockerfile
FROM grafana/grafana:11.2.0
# Bake the plugin set into the image. Versions resolved at
# container start from the grafana.com catalog.
ENV GF_INSTALL_PLUGINS="marcusolsson-json-datasource,grafana-piechart-panel,grafana-clock-panel"
# Bake the Grafana configuration. Secrets are mounted at
# runtime, not committed.
COPY grafana.ini /etc/grafana/grafana.ini
COPY provisioning/ /etc/grafana/provisioning/
# Restrict permissions on the plugins directory.
RUN chmod -R 755 /var/lib/grafana/plugins
The full [plugins] stanza in grafana.ini:
# /etc/grafana/grafana.ini
[plugins]
allow_loading_unsigned_plugins =
plugin_admin_enabled = false
private_server = https://grafana.com
plugin_api_key = ${GRAFANA_COM_API_KEY}
The fields, annotated:
allow_loading_unsigned_plugins— blank by default. Every plugin must be signed. Use a comma-separated allow list for specific unsigned plugins during a migration; never use wildcards.plugin_admin_enabled = false— disables the Admin UI install path. Critical in HA: the Admin UI installs on one replica only.private_server— endpoint for private plugins. Defaults to grafana.com. Override for a private registry.plugin_api_key— API key for the private plugin registry. Source from a secret. Rotate per the same policy as other credentials.
The [paths] stanza sets the plugins directory:
[paths]
data = /var/lib/grafana
logs = /var/log/grafana
plugins = /var/lib/grafana/plugins
provisioning = /etc/grafana/provisioning
Mount /var/lib/grafana/plugins from a shared read-only volume
when the deployment pattern requires ephemeral containers. The
volume must be the same content on every replica.
How to validate it
Confirm every replica has the same plugin set, the same version, and the same signature.
# READ-ONLY
# List the plugin directories on a replica.
ls -la /var/lib/grafana/plugins
drwxr-xr-x marcusolsson-json-datasource
drwxr-xr-x grafana-piechart-panel
drwxr-xr-x grafana-clock-panel
The same list on every replica is the canonical signal. Diff the output across replicas.
# READ-ONLY
# Query the Grafana API for the installed plugins. The
# payload should be identical on every replica.
curl -s -u admin:REDACTED http://g1:3000/api/plugins?core=0 | jq '.[] | {id, name, type, signature, version}'
{"id": "marcusolsson-json-datasource", "name": "JSON", "type": "datasource", "signature": "valid", "version": "1.3.0"}
{"id": "grafana-piechart-panel", "name": "Pie Chart", "type": "panel", "signature": "valid", "version": "1.6.2"}
The signature: valid field is the key check. signature: invalid or a missing field means a plugin is not loaded.
# READ-ONLY
# Cross-replica comparison. The same payload should come
# back from g2 and g3.
diff \
<(curl -s -u admin:REDACTED http://g1:3000/api/plugins?core=0 | jq -S .) \
<(curl -s -u admin:REDACTED http://g2:3000/api/plugins?core=0 | jq -S .)
An empty diff is the right answer. Any non-empty diff is a drift signal.
How to fail
Six failure modes hit plugin consistency in production. Each one maps to a recognisable symptom.
- Plugin installed via Admin UI on one replica. The user
installed it through the UI on
g1. Symptom: the plugin works ong1, returns 404 ong2andg3. The/api/pluginspayload differs. - Plugin version drift. A new container was deployed with the latest image while an old container is still running. Symptom: the same query returns different schemas depending on which replica the load balancer picks.
- Private plugin license key only on one replica. The
GF_PLUGIN_API_KEYenv var was set on theg1deployment but not on theg2org3deployments. Symptom: the plugin loads ong1only, the licensed features flag flickers between replicas. - Unsigned plugin rejection. A plugin was extracted from a
zip file into the plugins directory. Grafana rejects it
silently. Symptom: the plugin does not appear in
/api/plugins; the log shows “plugin signature is invalid”. - Plugin incompatible with Grafana version. A plugin was pinned in the image at a version that does not support the current Grafana version. Symptom: Grafana logs “plugin requires Grafana X or later”, the plugin does not load.
- Plugin admin enabled in HA. The default
plugin_admin_enabled = trueexposes the Admin UI install path. Symptom: an operator installs through the UI, the plugin drifts, the on-call engineer is paged.
How to troubleshoot it
Diagnose from the API inward.
- Is the plugin listed on every replica?
curl /api/plugins?core=0from each replica. The list must match. - Is the plugin signature valid? The
signaturefield in the JSON payload.invalidmeans the rejection is active. - Is the plugin version consistent? Compare the
versionfield across replicas. - Are the plugin files present?
ls /var/lib/grafana/pluginson each replica. The list must match. - Is the plugin_admin_enabled off? Inspect the
[plugins]section. The flag must befalsein HA. - Is the plugin compatible? Check the plugin’s
dependencies.grafanaVersioninplugin.json. The running Grafana version must satisfy the constraint.
Distinguish “is the plugin installed?” (the file is on disk) from “is the plugin loaded?” (Grafana knows about it). The file can be present but unloaded because of a signature failure.
Security implications
A plugin runs in the Grafana process. A vulnerability in a plugin is a vulnerability in Grafana. The plugin supply chain is a real attack surface.
- Signature verification. Every plugin must be signed by a trusted key. The default Grafana image trusts the grafana.com public key. Private plugins require the private key added to the configuration.
- No unsigned plugins.
allow_loading_unsigned_plugins = "*"disables the security check. Use a comma-separated allow list for specific plugins during a migration. - Plugin source. Plugins are downloaded from grafana.com or a private registry. Mirror the registry for an air-gapped environment.
- Plugin updates. Subscribe to plugin security advisories. Test the upgrade in non-production before rolling out.
- Plugin code review. The plugin runs in the same process as the rest of Grafana. Treat the plugin code as part of the codebase.
Performance implications
Plugins add to the Grafana startup time and to the runtime resource use.
- Startup time. Each plugin is loaded on boot. A plugin set of 10 adds 1-3 seconds to the cold start. The cold start is one-off; the steady-state is unaffected.
- Panel bundle size. Each panel plugin adds JavaScript to the front-end bundle. The bundle size is the long-tail download time for the first dashboard load.
- Datasource query path. Each plugin has its own query implementation. The plugin author is responsible for the query performance.
- Memory. Each plugin holds state in the Grafana process. A plugin with a cache can grow. Cap the cache in the plugin configuration.
Production guidance
The right approach is to bake plugins into the image and disable the Admin UI install path.
- All plugins defined in the Dockerfile
GF_INSTALL_PLUGINSor pre-installed in the image. - The same image for every replica.
plugin_admin_enabled = falsein HA.- Private plugin registry mirrored in an air-gapped environment.
- Plugin version pinning. Update with intent, not by accident.
- Subscribe to plugin security advisories.
Verification
You should now be able to answer:
- Why is the Admin UI install path the wrong answer for HA plugin installation?
- What is the right way to install a plugin in an HA Grafana deployment?
- Which configuration value disables the Admin UI install path, and why is it critical in HA?
- How do you confirm that every replica has the same plugin set from the API?
- What is signature verification, and why is it a security boundary?
Quiz
Knowledge check · 8 questions
Q1. What is the right way to install plugins in a Grafana HA deployment?
Q2. Which configuration value disables the Admin UI install path?
Q3. Baking plugins into the image is acceptable. Installing at runtime via the Admin UI is also acceptable.
Q4. Which of the following are signs of plugin drift across replicas? (Select all that apply.)
Q5. How do you confirm every replica has the same plugin set?
Q6. Name the environment variable that lists plugins to install at container start.
Q7. allow_loading_unsigned_plugins = "*" is a safe default for HA Grafana.
Q8. A plugin is installed on g1 only. The database is shared. What is the most likely cause?
Passing score: 75%. Answers are checked in this browser.