Grafana PDF导出:Docker部署Image Renderer指南 做运维的同学应该都经历过这个场景领导一句“把昨天那几个监控大屏整理成 PDF 发我”你在 Grafana 里翻了一圈发现开源版居然没有一键导出 PDF 的按钮。撑死了能截个 PNG分辨率一放大全是马赛克数据标得密一点根本没法看。Grafana 开源版导出 Dashboard PDF官方给出的标准路径是部署 Grafana Image Renderer。这个插件本质上是一个基于 headless Chrome 的渲染服务Grafana 把页面请求丢给它它打开页面、等图表画完、再给你生成图片或者 PDF。今天这篇就聊聊怎么用 Docker 把它完整地跑起来从架构原理到部署参数再到实际导出和排坑一次说清楚。适合正在用 Grafana 开源版做监控展示、需要把面板归档成报告或定期发给相关方的人参考。1. 方案背景与选型思路1.1 为什么开源版导出 PDF 要单独部署一个服务最早期的 Grafana 确实自带 PDF 导出功能但当时底层依赖的是 PhantomJS一个已经停止维护的浏览器内核项目。后来 Grafana 官方考虑到安全性和维护成本直接把内置导出能力摘掉了并给出的替代方案就是 Grafana Image Renderer。这里要理清一个概念Grafana 和 Image Renderer 是两个独立的进程。Grafana 负责页面展示、权限控制和数据查询Image Renderer 负责“把页面画出来”。你点击导出 PDF 时Grafana 会像用户一样请求内部页面地址把这个地址发给 Image Renderer渲染器启动无头浏览器打开页面等待图表和网络请求完成然后将整页内容打印成 PDF 文件返回。所以当你在 Grafana 菜单里找不到 PDF 选项时不是配置漏了哪里而是确实缺少了 Image Renderer 这个执行端。这个设计其实挺合理的把“界面程序”和“渲染工人”分开Grafana 主程序保持轻量渲染这种重活可以独立部署、独立扩容。1.2 Docker 部署 vs 二进制安装Image Renderer 的安装方式有二进制和 Docker 两种。二进制方式看起来简单——下载一个可执行文件配上系统里已有的 Chrome启动就行。但实际操作中很容易踩坑系统缺少 Chromium 依赖库、版本不匹配导致黑屏、字体缺失导致中文乱码、glibc 版本不对导致进程直接崩溃。所以我的建议是无脑选 Docker。官方镜像grafana/grafana-image-renderer已经把 Chromium、系统依赖、字体环境都集成好了你不需要在宿主系统上装任何浏览器相关的东西。用 Docker Compose 把 Grafana 和 Image Renderer 放在同一个自定义网络里两个容器用服务名互相访问既解决了网络互通又便于版本升级和回滚。1.3 版本兼容与锁定策略Grafana Image Renderer 对 Grafana 主版本有兼容要求。以当前主流版本为例Grafana 9.x、10.x、11.x 搭配 renderer 3.x 都是可行组合但我建议两个镜像都锁定具体 tag而不是用latest。渲染器跟随的 Chromium 升级频率不低一次小版本升级可能改变页面的渲染行为最终导出的 PDF 排版和之前不一样排查起来很痛苦。我在实际项目中会把grafana/grafana:11.1.0和grafana/grafana-image-renderer:3.10.0同时写进 Compose 文件并且把镜像版本记录在项目的 README 里。这样无论过去多久新同事一把梭部署出来的效果和当初调试通过的版本完全一致。2. Docker 部署 Image Renderer 完整步骤2.1 编写 Docker Compose 文件下面是完整可用的 Compose 配置核心是两个服务、一个自定义网络、两个数据卷version: 3.8 services: grafana: image: grafana/grafana:11.1.0 container_name: grafana restart: unless-stopped ports: - 3000:3000 environment: - GF_RENDERING_SERVER_URLhttp://renderer:8081/render - GF_RENDERING_CALLBACK_URLhttp://grafana:3000/ - GF_RENDERING_MODEsync - GF_RENDERING_IGNORE_HTTPS_ERRORStrue volumes: - grafana-storage:/var/lib/grafana networks: - grafana-net renderer: image: grafana/grafana-image-renderer:3.10.0 container_name: grafana-image-renderer restart: unless-stopped environment: - ENABLE_HTTP_SERVICEtrue - HTTP_PORT8081 - RENDERER_CHROME_NO_SANDBOXtrue - IGNORE_HTTPS_ERRORStrue volumes: - renderer-fonts:/usr/share/fonts networks: - grafana-net volumes: grafana-storage: renderer-fonts: networks: grafana-net:逐行解释一下关键配置。GF_RENDERING_SERVER_URL是 Grafana 调用渲染服务的地址写的是http://renderer:8081/render。这里用的是 Compose 服务名renderer不是 IP也不是 localhost。因为两个容器在同一个自定义网络grafana-net里Docker 内置 DNS 会把服务名解析成对应容器 IP。GF_RENDERING_CALLBACK_URL是给 renderer 回调 Grafana 用的地址。很多教程会漏掉这个配置导致异步渲染一直失败。它的含义是renderer 渲染完成后需要通知 Grafana“我做完了”所以 Grafana 必须给它一个自己能访问到的内部地址。这里填http://grafana:3000/同样用服务名。GF_RENDERING_MODEsync表示同步渲染模式适合单机或小规模场景请求发出后等待结果返回即可。如果你的 Grafana 是多节点集群后续可以考虑切换到cluster模式并引入 Redis 做队列不过那就是进阶话题了。RENDERER_CHROME_NO_SANDBOXtrue是因为官方镜像默认以 root 身份运行容器Chromium 的 sandbox 机制在 root 下会报错必须显式关掉。这也是为什么我强烈推荐 Docker 而不是二进制安装的原因之一这类与宿主环境强相关的参数已经被官方预设好了。IGNORE_HTTPS_ERRORStrue解决的是 Grafana 使用了自签名证书时的证书校验问题。如果 Grafana 只在内网跑没有对外暴露 HTTPS这个可以留空或设为 false。但如果你后续给 Grafana 配了 Nginx 反代并加上了 HTTPS务必打开这一项否则渲染器访问回调地址会因为证书不受信任直接拒绝打开页面。2.2 Grafana 容器如何对接渲染服务上面的配置用了环境变量方式。Grafana 官方支持通过GF_前缀的环境变量覆盖grafana.ini中的配置规则是把 ini 文件的 section 和 key 用下划线拼接并转大写。比如[rendering] server_url对应GF_RENDERING_SERVER_URL。这种方式比直接修改grafana.ini更适合容器部署因为配置随 Compose 文件走可审计、可迁移。如果你更习惯传统方式也可以在 Grafana 的grafana.ini中写[rendering] server_url http://renderer:8081/render callback_url http://grafana:3000/ mode sync ignore_https_errors true效果一致。但要注意一个关键点如果你的 Grafana 是宿主机原生安装而非容器而 Image Renderer 是 Docker 部署那么server_url要填宿主机的局域网 IP例如http://192.168.1.10:8081/rendercallback_url要填 Grafana 能被 renderer 访问到的地址。因为此时 Grafana 和 renderer 不在同一个 Docker 网络中用 localhost 是互相找不到的。另外如果启用了GF_RENDERING_MODEcluster异步模式callback_url必须是 renderer 容器内可访问的地址因为它实际是 renderer 容器向这个地址发回调请求。很多人在这一步踩坑排查半天最后发现是回调地址写成了宿主机 localhost两个容器互相不通。2.3 启动服务与连通性验证配置完成后执行docker compose up -d查看容器状态docker ps正常情况下两个容器都处于Up状态。接着看渲染服务日志有没有报错docker logs -f grafana-image-renderer日志中出现类似HTTP server listening on :8081的内容说明服务已经起来了。然后从 Grafana 容器内部测试网络连通性docker exec -it grafana curl -I http://renderer:8081/render如果返回 HTTP 400 或 403这是正常的因为渲染接口需要带认证参数但连接本身是通的。如果返回connection refused或者Could not resolve host说明网络或者服务名解析有问题回到 Compose 文件检查两个服务是否在同一个网络里。最后打开 Grafana 界面进入任意 Dashboard点击右上角的 Share 按钮如果菜单里出现了 PDF 标签说明 Grafana 已经成功感知到了渲染服务部署环节就基本完成了。3. 导出 PDF 的三种实操方式3.1 从 Grafana 界面直接下载部署成功之后最直观的导出方式就是图形界面操作。进入 Dashboard 右上角的 Share dashboard or panel 按钮切到 PDF 标签页。这里有几个选项需要说明Output format 通常固定为 PDF不需要改Layout 有 Landscape横向和 Portrait纵向两种监控面板一般数据表多、列多选 Landscape 更合适横向排版能让每张图更宽文字也不容易挤在一起Time range 默认是当前面板时间范围如果你要导出的时间不是当前视图在这里手动选导出的 PDF 会使用这个时间范围重新查询数据图表会按新时间重画。点击 Download PDF 后Grafana 会向 Image Renderer 发出渲染请求经过几秒到几十秒不等的等待时间浏览器会收到一个 PDF 文件并自动开始下载。如果界面里没有出现 PDF 标签说明GF_RENDERING_SERVER_URL配置没有生效或者 renderer 服务实际不可达。如果出现了标签但点击下载后一直转圈通常是渲染超时或渲染服务日志中有报错这个在下一章详细说。3.2 用 curl 调用渲染接口图形界面适合临时导出但如果你需要把导出动作集成到脚本里就得走 HTTP 接口。单个面板的 PDF 导出curl -u admin:admin \ http://localhost:3000/render/d-solo/abc123?panelId2from1700000000000to1700003600000width1000height600 \ -o panel.pdf这里解释几个参数d-solo表示只渲染单个面板而不是整个 Dashboardabc123是 Dashboard 的 UID可以从 Dashboard 的 URL 里获取panelId是面板 ID在 Dashboard JSON 中可以查到也可以在编辑面板时的 URL 参数里看到from和to是毫秒级时间戳用于控制数据查询范围width和height是渲染的像素尺寸PDF 页面会依据这个比例排版。如果要导出整个 Dashboard把路径换成/render/d/abc123同时建议加上kiosk参数它会隐藏 Grafana 的侧边栏和顶栏让 PDF 内容更干净curl -u admin:admin \ http://localhost:3000/render/d/abc123?from1700000000000to1700003600000kiosk \ -o dashboard.pdf注意这里的请求地址是 Grafana 的/render端点Grafana 收到请求后会校验用户权限然后把渲染任务转发给 Image Renderer。所以这个接口必须带认证信息可以是用户名密码也可以是 API Token。实践中我建议生成一个只读的 Service Account Token避免在脚本里明文保存管理员密码。3.3 脚本化批量导出实现自动化巡检报表前面两种方式解决的是手动需求但很多场景是每天定时生成运营巡检报告把所有核心面板在凌晨导出成 PDF存档或者发给相关人员。这里提供一个可行的 Bash 脚本思路#!/bin/bash TOKENglsa_xxxxxxxxxxxx BASEhttp://localhost:3000 TS_BEGIN$(date -d yesterday 00:00:00 %s)000 TS_END$(date -d yesterday 23:59:59 %s)000 for panel in 2 4 6 8; do curl -s -H Authorization: Bearer $TOKEN \ $BASE/render/d-solo/abc123?panelId$panelfrom$TS_BEGINto$TS_ENDwidth1200height500 \ -o /tmp/report-panel-$panel.png done # 使用 ImageMagick 将多张图合并成一份 PDF convert /tmp/report-panel-*.png /tmp/report-$(date %F).pdf关键点是from和to的时间戳计算。用date -d yesterday 00:00:00 %s获取的是“昨天零点”的 Unix 秒数乘以 1000 才是 Grafana 需要的毫秒单位。这里用了$(date -d ... %s)000的方式在字符串后面拼接 000实现了秒转毫秒不需要额外写复杂的算术逻辑。将脚本加入 crontab0 1 * * * /opt/scripts/export-grafana-report.sh每天凌晨 1 点执行上班前就能看到昨日的巡检报告。如果想直接发送邮件或推送到内部系统可以在脚本末尾追加发送命令比如通过mailx发送附件或者调用企业微信机器人的 Webhook 上传文件这里就根据自己的环境扩展了。4. 实践中的问题排查与调优4.1 PDF 下载一直转圈 / 提示 Rendering failed这是最常遇到的一类问题。界面点击 Download PDF 后页面一直显示“正在渲染”或者弹出错误提示。遇到这种情况第一步永远是看渲染服务的日志而不是反复点击重试docker logs -f grafana-image-renderer如果日志中出现context deadline exceeded或timeout相关字眼说明面板数据量太大headless Chrome 在默认超时时间内没有画完图表。解决办法是调大渲染超时时间在 Grafana 容器上加环境变量- GF_RENDERING_RENDERING_TIMEOUT90s默认值通常是 30 秒调整为 60 到 90 秒一般就能覆盖大部分情况。如果面板确实特别重比如图表数量极多或依赖的外部数据接口响应很慢还可以继续往上加但这时候更应该考虑优化面板本身而不是无限放宽超时。如果是多用户同时使用导出功能可能触发并发限制。可以把并发数调低一些避免请求全部堆积- GF_RENDERING_CONCURRENT_RENDER_REQUEST_LIMIT5这个值并不是越大越好。每个渲染任务都会启动一个 headless Chrome 进程内存消耗相当可观并发太高会导致容器 OOM整个服务直接挂掉反而影响所有用户。4.2 中文乱码或文字变成方块这是 Docker 部署方案中一个非常典型的坑。官方镜像默认只带了基础英文字体没有中文字体当面板标题、图例、坐标轴标签里有中文时pdf 里的中文字符会变成方框或乱码。我的处理方案是给 renderer 容器挂载一个字体目录。在宿主机上准备一个包含中文字体的目录然后在 Compose 文件中挂载覆盖volumes: - /opt/fonts:/usr/share/fonts/custom:ro字体文件可以从fonts-noto-cjk包中提取或者直接复制 Windows 系统的msyh.ttc微软雅黑到该目录。挂载完成后重启容器docker compose restart renderer然后重新导出 PDF中文就能正常显示了。需要注意Grafana 侧配置的主题和字体也会影响渲染结果但如果面板在网页上显示正常而 PDF 乱码问题基本就在 renderer 容器的字体环境上。4.3 导出的 PDF 页面空白或只有部分图表这种问题通常不是渲染服务挂掉而是 headless Chrome 没有等到图表完全画出来就执行了打印。Grafana 图表依赖数据请求和 JavaScript 渲染如果网络慢或数据查询时间长Chrome 可能在空页面状态下就抓取了内容。先检查 Grafana 的查询响应时间。如果某个面板的数据源是外部数据库查询耗时本身就超过 3 秒那么渲染器默认的等待时间可能不够。除了调大GF_RENDERING_RENDERING_TIMEOUT还可以在 Dashboard 的设置里把面板的查询缓存时间调短或者间接降低数据量。另一种更隐蔽的情况是 Dashboard 使用了“动态变量”例如时间范围选择器或模板变量渲染器在无人工操作的情况下无法自动切换这些变量导出的 PDF 可能显示的是默认状态。因此在导出前要确认面板的时间范围和变量状态是否符合要求。4.4 自签名 HTTPS 导致的渲染失败如果你的 Grafana 通过 Nginx 反代暴露成了 HTTPS 域名但证书是自签的renderer 访问 Grafana 回调地址时会在证书校验阶段失败表现为日志中频繁出现类似于net::ERR_CERT_AUTHORITY_INVALID的错误。解决办法是在两侧同时设置忽略证书错误的参数。Grafana 侧- GF_RENDERING_IGNORE_HTTPS_ERRORStruerenderer 侧- IGNORE_HTTPS_ERRORStrue两个都要配因为它们分别负责不同的请求方向。4.5 渲染服务的性能与资源规划Image Renderer 是一个重量级服务每个并发渲染任务都会拉起一个 Chromium 进程内存占用随页面复杂度线性增长。我在实际使用中观察过一个中等复杂度的 Dashboard四行图表每行四个面板渲染一次大约需要 2GB 左右的内存峰值。因此部署 renderer 的宿主机至少要给容器预留 2GB 可用内存建议在 Compose 中显式声明资源限制deploy: resources: limits: memory: 2G cpus: 2.0当然这个配置在 Docker Compose v2 中属于 Swarm 模式专属单机使用docker compose up时不一定生效更通用的做法是用docker run --memory2g --cpus2或直接在 Compose 文件中写mem_limit。但不管用哪种方式核心思路是别让渲染服务把宿主资源吃干抹净否则 Grafana 本身也会受到影响。4.6 时间范围与时区问题定时导出报表时最容易出现的是日期边界错误。比如你想导出“昨天”的数据但脚本跑在凌晨 1 点如果用$(date %s)取当前时间作为结束时间就会把今天凌晨到现在的空数据段也包含进去。所以我上一章的脚本特意用了yesterday 00:00:00和yesterday 23:59:59来限定范围这在生成日报场景中非常实用。时区方面Grafana 默认使用用户偏好时区renderer 在打开页面时也会继承 Grafana 会话的时区设置。如果你的服务器是 UTC 时区而业务数据基于北京时间导出的 PDF 上时间轴显示的是 UTC 时间看起来会很别扭。建议在 Grafana 的用户配置中将默认时区设置为Asia/Shanghai或者在导出脚本中通过 URL 参数显式指定时区timezoneAsia%2FShanghai5. 几个实操层面的小建议部署 Image Renderer 这件事本身不复杂复杂的是让它稳定、持续地工作。我个人实践下来有几个体会。第一日志是你的第一手证据。任何导出失败不要猜先docker logs -f grafana-image-renderer看输出。渲染服务会把具体失败的步骤和 URL 打出来很多时候问题一眼就能定位。第二版本锁定比“跟着最新走”更让人安心。Grafana Image Renderer 升级带来的行为变化是隐性的外观和排版可能在某个版本后悄悄变化。在非必要情况下固定版本长期运行只在确认新版本兼容后再手动升级。第三PDF 标签能否在 Share 菜单中出现是判断配置是否生效的快速探针。如果标签没出现优先排查GF_RENDERING_SERVER_URL是否正确如果标签出现了但下载失败再看渲染服务日志。这个排查顺序能帮你省下大量时间。最后再分享一个扩展方向导出的 PDF 文件可以直接作为邮件附件发送或者归档到对象存储。我目前的做法是每天晚上定时把核心业务面板渲染成 PDF脚本结束后通过邮件发送给当天值班人员全程无需人工参与。Grafana 开源版配上 Image Renderer 之后整个监控报告的自动化闭环就完整了这也是这个方案最值得投入时间打磨的地方。