# grafana-image-renderer: turning dashboards into PNGs with a headless browser

> grafana-image-renderer is the Go service behind Grafana's panel and dashboard image export, driving a headless Chromium through chromedp to produce PNGs, PDFs and, with Grafana Enterprise, CSVs and emailed reports. It ships as a static binary or a Docker image, and its Chromium and base image versions are maintained by hand through pull requests.

**grafana/grafana-image-renderer** — A Grafana backend service that handles rendering of panels & dashboards to PNGs using headless browser (Chromium/Chrome)

- Repository: https://github.com/grafana/grafana-image-renderer
- Stars: 542 · Forks: 175
- Language: Go
- License: Apache-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/grafana-grafana-image-renderer

## A rendering sidecar, not a Grafana plugin

grafana-image-renderer is a backend service for Grafana that provides panel and dashboard rendering with a headless browser, and the project description puts it plainly as a Grafana backend service that handles rendering of panels and dashboards to PNGs using headless browser, Chromium or Chrome. The architecture is a service that Grafana calls out to, and the README states the practical value in one sentence, that you can get your favourite dashboards as PDFs, PNGs or, with Grafana Enterprise, CSVs and even over emails with Grafana Reports. The Enterprise qualifier matters, because the CSV and email paths are a separate commercial product, and the renderer underneath is open. The primary language is Go and the service is Apache-2.0 licensed, with the last push to the repository on 2026-10-22 and regular patch releases, v5.12.7 on 2026-10-13, v5.12.6 on 2026-10-09 and v5.12.5 on 2026-09-30.

## The browser is driven over the DevTools protocol, not by hand

The dependency list explains how the rendering works. The service uses chromedp, and specifically a fork of it published as github.com/grafana/chromedp, alongside the upstream chromedp/cdproto for the protocol definitions. That is a Chrome DevTools Protocol client, so the service drives a real browser over its debugging interface rather than through a browser automation library that wraps it, which is the right level of control for something that has to reproduce exactly what a user's Grafana session renders. The PDF side is handled by a different library, klippa-app/go-pdfium, which is a PDFium binding, so the service can take the rendered page and turn it into a PDF. The rest of the dependency set is operational: the Prometheus client for metrics, gopsutil for system and process information, OpenTelemetry for tracing with the OTLP exporters over gRPC and HTTP, urfave/cli for the command line, the moby client API for container interactions, and the Linux capability library, which is there because a browser sandbox needs privileges managed carefully. The service is instrumented with otelhttp for outbound HTTP, and the tracing exporters are configured in the manifest rather than left to defaults.

## The Dockerfile is two images in one file, and the reason is in the comment

The Dockerfile opens with a comment that explains the whole file, and it is the most useful paragraph in the repository. It does two things in parallel. First it builds a statically linked Go binary, which is fine to use in Debian, Alpine, RHEL and similar base images, and the reason given is that static linking means the base image can be switched with little or no effort. Second it builds a running environment, which is the environment that exists for the Go binary and should have all the necessary pieces to run the application. So the build stage uses golang on Alpine with a pinned digest, and the runtime stage uses Debian trixie with its own pinned digest, and the Go binary is copied in with the external linking flags for a static build. The runtime image installs the fonts that a dashboard needs, and the list is long and specific, with IPAex Gothic, WenQuanYi Zen Hei, the Thai and Khmer and Kacst fonts, Unifont, Open Sans, Roboto, Inter and the recommended fonts set, which is a font pack strategy for rendering dashboards that contain text in many scripts. Chromium and its driver and shell are installed from the Debian packages at a pinned minimum version.

## Chromium is updated by hand, which is a maintenance cost

The updating Chromium section is a description of manual work, and it is the clearest signal of how this project is run. One of the quick ways to check the latest version available is to go to the Debian packages page for the Chromium package in the current Debian codename, changing the codename for whatever the Dockerfile is using, which at the time of writing is 13. The instructions say to make sure the build for amd64 and arm64 are both available first by scrolling to the bottom of the page and checking there are no red backgrounds, and to ignore anything after the dash in the version, which is what the Dockerfile uses. Then you update the CHROMIUM_VERSION argument in the Dockerfile, and the README notes that changing this argument also reruns the package update and upgrade step, with the cachebuster date used for manual package refreshes. The commit is then a pull request, merged, followed by a new GitHub release. The same manual treatment applies to the Go version and the Debian base image, both of which are pinned by digest, and the Go policy is stated with a rule: for a new minor version, only update to a .0 patch if there are security vulnerabilities fixed and not present in the N-1 minor, otherwise stay on the N-1 minor until the N minor is at least on a .1 patch. That is a cautious policy for a service where a browser update can change rendering output.

## Compiling the service and the image

The compile section gives the two commands you need, one for the Go service and one for the container image.

```shell
$ go build -buildvcs -o grafana-image-renderer .
```

```shell
$ docker build .
```

The buildvcs flag is what stamps version information from the repository into the binary, which embeds build metadata in the binary. The README then names the tools the engineers use day to day, with goimports described as the code formatter of choice, golangci-lint as the linter of choice, and the acceptance test command taking an IMAGE tag so the suite runs against a freshly built image rather than a cached one.

```shell
$ go tool goimports # our code formatter of choice
$ golangci-lint run # our linter of choice
$ IMAGE=tag-here go test ./tests/acceptance/... -count=1 # run acceptance tests
```

The count of one on the test command is the detail that matters, since without it the Go test cache would happily return a previous result and the acceptance suite would not actually have run against the new image.

## Building, testing and the acceptance suite

The compile section gives the two commands you need. The Go service builds with go build and the buildvcs flag, and the Docker image builds with docker build. The README also names the three tools the engineers use, calling goimports the code formatter of choice, golangci-lint the linter of choice, and the acceptance test command, which takes an IMAGE tag and runs the acceptance tests under tests/acceptance with a count of one so the tests are not cached. The Makefile expands on that. There is a check target that runs lint and then test, a test target that runs the unit tests with a sixty second timeout, a test-acceptance target that builds a Docker image tagged gir and runs the acceptance suite against it with a hundred and twenty second timeout, a lint target that runs goimports in list mode and then golangci-lint, a fix target that writes the formatting and applies the lint fixes, and a build-all target that produces binaries for six platform combinations, Linux and Darwin on amd64 and arm64, and Windows on amd64 and arm64. The all target chains lint, build-all, test and test-acceptance, which is a useful summary of what the project considers a complete verification.

## Releases are a tag and a push

The release process is described in three sentences, and they are short because the automation does the rest. When you are ready to make a release, tag it in Git with the vX.Y.Z form, remembering the v prefix, then push it, and GitHub Actions deals with the build and the deployment. The consequence for a user is that the tag is the source of truth, so the version of the renderer is knowable from the tag on a running container, and the release history is the release history of a project where the interesting artefacts are container images rather than binaries. The CHANGELOG.md is checked in, and the top level of the repository also holds an adrs/ directory for architecture decision records, a devenv/ directory, a vale.ini for prose linting and a scripts/ directory. The .vale.ini is a small detail worth noting, because it means the documentation is checked for style the same way the code is checked by the linter, which fits a project whose output is largely a service with an HTTP interface and a container image.

## Where grafana-image-renderer is the wrong tool

The first limit is that the service exists to serve render requests, so if your use case is a one-off export from a dashboard you control, running a Chromium in a container is a large amount of machinery for a screenshot. The second is the maintenance model, and it is the real cost of this project. Chromium is pinned by hand in a Dockerfile, the base image is pinned by digest, the Go toolchain moves on a stated minor-by-minor policy, and each of these is a change a maintainer has to make, review and release, and a change in Chromium can change how a dashboard renders, which means a service that renders reports may produce slightly different images over time without any code change on your side. The third is that the service inherits the browser's security requirements, since Chromium in a container with a sandbox needs capabilities managed, which is why the capability library is a dependency, and running that as a privileged process in a shared environment is a decision an operator has to make. The fourth is the split with the Enterprise features, since CSV export and emailed reports are not in this repository, so the open service covers images and PDFs only. And the fifth is the coupling to Grafana's own API, which means the renderer version and the Grafana version have to be a matched pair, and installation details live in the docs directory of the repository.

## Conclusion

Adopt grafana-image-renderer if you need Grafana dashboards as images in places Grafana cannot render them itself, such as report emails, chat notifications or a PDF report. Skip it if Grafana's built-in image URL already covers your case, since this service exists to serve render requests Grafana makes against. Verify first which Grafana version and renderer version you pair, since the renderer talks to Grafana over its API and the two move at different cadences, and that your Chromium version still matches the pinned package in the Dockerfile.

## FAQ

### What does grafana-image-renderer do and what can it output?

It is a Go backend service for Grafana that renders panels and dashboards with a headless browser, Chromium or Chrome. You can get dashboards as PDFs and PNGs, and with Grafana Enterprise also CSVs and emailed reports through Grafana Reports. The project is Apache-2.0 licensed and its primary language is Go.

### How do I build and test grafana-image-renderer?

Compile the Go service with go build -buildvcs -o grafana-image-renderer . and build the Docker image with docker build . The README names goimports as the formatter of choice, golangci-lint as the linter, and the acceptance tests as a command taking an IMAGE tag. The Makefile has lint, test, test-acceptance, build-all and a docs target, with an all target chaining lint, build-all, test and test-acceptance.

### How is the Chromium version in grafana-image-renderer kept up to date?

Manually, by looking up the latest Chromium package version on the Debian packages page for the codename the Dockerfile uses, checking that both the amd64 and arm64 builds exist, then updating the CHROMIUM_VERSION argument in the Dockerfile and opening a pull request. Changing the argument also reruns the package update and upgrade step, and the Go and Debian base image versions are pinned by digest and updated the same way.

## Sources

- [grafana/grafana-image-renderer on GitHub](https://github.com/grafana/grafana-image-renderer)
- [Issues](https://github.com/grafana/grafana-image-renderer/issues)
- [License: Apache-2.0](https://github.com/grafana/grafana-image-renderer/blob/master/LICENSE)
- [README](https://github.com/grafana/grafana-image-renderer/blob/master/README.md)
- [Releases](https://github.com/grafana/grafana-image-renderer/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/grafana-grafana-image-renderer
