
1. 先说清楚为什么要做 Grafana 二次开发和自制镜像做 Grafana 二次开发这事儿很多团队一开始其实是被逼的。等到监控看板铺开之后你总会遇到几个绕不开的需求登录页要换成公司自己的品牌、要接入公司统一的单点登录、内置几个常用数据源、预置好默认目录和看板、甚至要改掉某些面板的行为或样式。这些问题靠点配置不一定能全解决于是就得动源码。动完源码之后你会发现开发和测试环境好不容易跑通了到了生产环境又不能直接把源码扔上去跑这时候“制作镜像”就成了收尾的必经一环。把二次开发的成果固化成一个干净的镜像既是交付物也是版本锁。这个流程听起来就是“前端编译 后端编译 打镜像”三步但真正上手过的朋友应该都有体会坑几乎全藏在细节里Node 版本不对前端直接报 OpenSSL 错误Go 的 CGO 没开导致 SQLite 起不来public 静态资源没拷进镜像导致页面白屏还有数据目录权限问题。这篇文章我把完整流程从头到尾捋一遍适合要给团队做私有化交付、想定制登录页和品牌、或者需要把数据源和仪表盘预置进镜像的开发和运维同学参考。内容不追求“背文档”而是把我实际构建时的做法、踩过的坑和验证方法直接写出来。2. 动手之前环境准备与源码结构认知2.1 工具链和版本怎么选二次开发的第一步不是打开编辑器而是把构建环境和目标版本定死。Grafana 是个典型的前后端分离项目——前端是 TypeScript React老版本里还有不少 Angular 遗产后端是 Go两套编译链完全独立缺一不可。以 Grafana 10.x 举例我建议的环境组合是这样go version # 1.21.x node -v # 18.x yarn --version # 1.22.x别用最新版Grafana 10 用的是 yarn classic docker --version git --version版本这事真的不能凭感觉来。Grafana 官方仓库的 package.json、go.mod 里都写死了依赖范围你拿 Node 20 去编 Grafana 9 或 10 的前端大概率会撞上 Webpack 和 OpenSSL 的兼容问题Go 版本太新也可能让某些老依赖编译不过。最稳妥的做法是先克隆源码切到你准备基于的 release tag然后看一眼仓库里的package.json和go.mod里要求的版本范围再按那个范围装本机工具。我自己的习惯是给每个 Grafana 大版本单独维护一个构建机或 CI 环境避免全局 Node、Go 版本互相污染。2.2 源码里你最需要关心的几个目录Grafana 的源码仓库非常大不用全读但要清楚每个关键目录是干嘛的。否则你改完代码都不知道该把构建产物往哪里对。conf/ # 默认配置 defaults.ini、provisioning 样例 pkg/ # Go 后端源码 cmd/grafana-server/ # 服务端主程序入口 public/ # 前端源码目录也是前端编译产物输出目录 app/ # 前端业务代码登录页、导航、面板都在这里 plugins-bundled/ # 内置插件编译期会被处理进最终产物 scripts/ # 官方容器启动脚本、打包辅助脚本这里最关键的是public/目录。Grafana 后端运行的时候是把public/作为静态资源目录来提供 Web 界面的。也就是说你改了前端源码之后编译会刷新public/下的产物最终打镜像时这个目录必须原封不动地放进镜像。很多人镜像起得来、接口通但页面白屏十有八九就是public/没拷对。2.3 版本锁死永远基于 release tag 建分支二次开发最容易犯的错误是直接git clone默认主干分支然后开改。主干分支每天都在变今天编出来的镜像和下周编出来的完全不是一回事出了问题根本没法回溯。我建议这样处理先 clone 仓库然后切换到指定的 release tag。git clone https://github.com/grafana/grafana.git cd grafana git checkout v10.4.2 git checkout -b dev/custom-10.4.2后续所有改动提交在这个分支上镜像 TAG 也跟这个版本号走例如grafana-custom:10.4.2。这样开发、测试、生产的代码和镜像是一一对应的排查问题的时候能少掉一大半麻烦。凡是跟我说“我用的最新 master 改的”的同事最后基本都在为版本漂移买单。3. 一个最小改造示例把登录页改出自己的品牌3.1 从哪个文件下手二次开发的范围可以很大但最典型、最能说明问题的是登录页品牌定制。Grafana 的登录页在public/app/features/login/LoginPage.tsx页面里的 logo 和标题会引用公共的 Branding 组件。如果你只是想把 logo 换成自家公司的可以直接在public/img/下放一张图然后改组件里的引用。// public/app/core/components/Branding/BrandLogo.tsx10.x 路径可能有微调 // 把默认 logo 引用换成自己的资源 img src/public/img/custom-logo.png altcompany-logo classNamelogin-logo /同理想加个副标题、改登录按钮下方的提示文字直接在LoginPage.tsx里找到对应的 JSX 节点修改即可。这种改动属于“表层定制”工作量小但对内对外展示的效果立竿见影。3.2 更深的改造点认证、面板、内置数据源除了换皮Grafana 二次开发更常见的是这几类需求认证对接公司内部有统一登录系统但标准 OAuth/SSO 协议对不上需要在pkg/api或pkg/login里增加认证逻辑。面板行为定制默认图表满足不了需求自己写插件当然是正道但有时候只是想让某个已有面板默认展示方式变一下直接改源码里的面板组件反而更快。预置数据源和看板这是最“轻”的开发严格说不用改源码把 provisioning 配置文件打进镜像就行后面第 6 节专门说。我个人的判断标准是能通过 provisioning、ini 配置、环境变量解决的坚决不动源码凡是配置解决不了的才走源码改造。源码改动越多你后续升级 Grafana 版本的成本就越高这个账要提前算清楚。3.3 本地编译验证别直接进 Docker改完代码先别急着打镜像在本地把前后端编译跑通验证逻辑没问题再进容器。前端编译yarn install --immutable yarn build后端编译。如果你是想快速验证推荐用官方构建脚本它会帮你处理好 sqlite 等构建标签go mod download go run mage.go -v build编出来的二进制通常在bin/目录下具体路径不同版本不太一样。然后本地起服务./bin/grafana-server --homepath$PWD浏览器访问http://localhost:3000确认登录页改成你自己的品牌了再继续后面的镜像制作。这一步能省掉你大量在容器里反复构建的等待时间。4. 制作镜像一份可以直接改的三阶段 Dockerfile4.1 为什么要用三阶段构建Grafana 构建链非常重前端要完整的 Node 环境后端要完整的 Go 工具链这两套环境如果直接塞进运行镜像镜像体积会膨胀到 2GB 以上而且漏洞扫描会非常难看。三阶段构建的思路是每个阶段只用自己需要的基础镜像最终运行镜像只保留二进制和必要的静态资源。通俗点说就是让 Node 和 Go 各自在“工地”上干完活最后只把“成品家具”搬进“入住房间”施工工具全部扔掉。这样最终镜像能压缩到两百多兆同时因为不携带编译器和源码安全性也好很多。4.2 完整 Dockerfile 与逐段解读下面这份 Dockerfile 我按 Grafana 10.x 验证过可以直接抄但要注意根据自己的版本调整版本号、二进制路径这些细节。# ------------------------------------------------ # 三阶段构建 # 1. node 环境编译前端产出 public/ 静态资源 # 2. golang 环境编译后端产出 grafana-server 二进制 # 3. 最小运行镜像只放运行必需的文件 # ------------------------------------------------ FROM node:18-bookworm AS frontend WORKDIR /src COPY . . RUN yarn install --immutable RUN yarn build FROM golang:1.21-bookworm AS backend WORKDIR /src COPY . . ENV CGO_ENABLED1 RUN go mod download # sqlite 默认是本地开发数据库必须带上 RUN go build -tags sqlite -o bin/grafana-server ./pkg/cmd/grafana-server FROM debian:bookworm-slim AS runtime ENV TZAsia/Shanghai \ GF_PATHS_HOME/usr/share/grafana \ GF_PATHS_CONFIG/etc/grafana/grafana.ini \ GF_PATHS_DATA/var/lib/grafana \ GF_PATHS_LOGS/var/log/grafana \ GF_PATHS_PLUGINS/var/lib/grafana/plugins \ GF_PATHS_PROVISIONING/etc/grafana/provisioning \ PATH/usr/share/grafana/bin:$PATH RUN apt-get update \ apt-get install -y --no-install-recommends ca-certificates tzdata curl \ rm -rf /var/lib/apt/lists/* \ useradd --system --uid 472 grafana WORKDIR /usr/share/grafana COPY --fromfrontend /src/public /usr/share/grafana/public COPY --frombackend /src/conf /usr/share/grafana/conf COPY --frombackend /src/plugins-bundled /usr/share/grafana/plugins-bundled COPY --frombackend /src/bin /usr/share/grafana/bin RUN mkdir -p /etc/grafana /var/lib/grafana /var/log/grafana \ chown -R grafana:grafana /etc/grafana /var/lib/grafana /var/log/grafana /usr/share/grafana USER grafana EXPOSE 3000 HEALTHCHECK --interval30s --timeout3s --start-period10s --retries3 \ CMD curl -fs http://127.0.0.1:3000/api/health || exit 1 ENTRYPOINT [/usr/share/grafana/bin/grafana-server] CMD [--homepath/usr/share/grafana]几个关键点单独说一下。第一COPY . .会把整个源码目录塞进临时构建阶段所以项目根目录必须放一个.dockerignore至少忽略这些.git node_modules bin dist data public/vendor不忽略的话前端构建阶段会把本机的node_modules也拷进容器既慢又可能与 Linux 下的构建缓存冲突。第二后端编译我用了直接go build而不是 mage是因为在 Docker 多阶段构建里每个阶段职责越单一越好。如果你在 backend 阶段跑go run mage.go -v build它有可能又去触发前端构建而这个阶段根本没有 Node 环境白白增加失败概率。直接指定pkg/cmd/grafana-server入口干净利落。第三CGO_ENABLED1这个环境变量非常关键。Grafana 默认支持 SQLite 作为内置数据库而 SQLite 的 Go 驱动需要 CGO。官方构建脚本里会处理这件事你自己直接go build就很容易漏掉。漏掉之后的表现是镜像能构建成功启动时只要数据库类型是 sqlite3 就直接报错退出。记住要么在 golang 基础镜像里把 gcc 装好并开启 CGO要么去掉 sqlite 相关功能二者只能选一个。第四grafana用户选择--uid 472是刻意为之因为官方镜像就用这个 UID生产环境如果已经把/var/lib/grafana作为命名卷挂载出来权限保持一致才不会出现“容器能建目录挂载卷后没权限写”的诡异问题。4.3 构建镜像与推送仓库Dockerfile 放好后构建命令很简单docker build -t registry.example.com/monitor/grafana-custom:10.4.2 .构建完先本地跑一次验证无误再推送docker run -d --name grafana-test -p 3000:3000 \ -v grafana-data:/var/lib/grafana \ registry.example.com/monitor/grafana-custom:10.4.2 # 验证健康检查 curl -s http://localhost:3000/api/health如果返回的 JSON 里有database:ok版本号也对得上说明这个镜像的“地基”是稳的。之后再把监控数据源、看板、公告这些内容接进来。推送私有仓库的命令用的是标准docker push这里就不再赘述了。5. 镜像跑起来之后验证、配置与高频排障5.1 环境变量覆盖配置的用法镜像做好之后你会发现 Grafana 一个很方便的设计几乎每个配置项都能用GF_前缀的环境变量覆盖。比如部署到生产环境时不需要改镜像里的 ini直接用环境变量注入docker run -d -p 3000:3000 \ -e GF_SERVER_ROOT_URLhttps://monitor.example.com \ -e GF_USERS_ALLOW_SIGN_UPfalse \ -e GF_AUTH_ANONYMOUS_ENABLEDfalse \ -e GF_SECURITY_ADMIN_PASSWORDyour-strong-password \ -v grafana-data:/var/lib/grafana \ registry.example.com/monitor/grafana-custom:10.4.2规则很简单GF_SERVER_ROOT_URL对应 ini 里的[server] root_urlGF_USERS_ALLOW_SIGN_UP对应[users] allow_sign_up就是把配置段名和键名用下划线连起来、全部大写。用这种方式同一个镜像可以部署出开发、测试、生产完全不同的实例配置和镜像彻底解耦这是镜像交付的核心价值之一。5.2 三个高频坑基本每个团队都会踩第一个坑是前端编译时 Node 版本不对。如果你拿 Node 17 以上的版本去编译 Grafana 8.x 或早期 9.x经常报error:0308010C:digital envelope routines::unsupported原因是旧版 Webpack 不兼容新版 OpenSSL。处理办法有两种一是把 Node 版本降到项目要求的版本二是临时加环境变量NODE_OPTIONS--openssl-legacy-provider yarn build第二种办法能过编译但我更推荐直接匹配项目要求的 Node 版本毕竟生产级镜像构建不应该依赖这种“绕过”式的参数。第二个坑是内存不足。yarn build是一个非常重的 Webpack 打包过程小内存机器经常 OOM。如果构建日志里出现JavaScript heap out of memory加一句NODE_OPTIONS--max-old-space-size4096 yarn build在 CI 里构建时尤其要提前把这个参数写进 Dockerfile别等崩了再改。第三个坑是数据目录权限。如果运行时把宿主机目录直接挂载进/var/lib/grafana宿主机目录的属主和容器内grafana用户的 UID 对不上启动时就会报mkdir /var/lib/grafana: permission denied。解决办法是在宿主机上先执行chown -R 472:472 /your/data/dir或者用命名卷让 Docker 自动处理初始化。5.3 常见问题速查表现象原因处理方式前端编译报 digital envelope routines 错误Node 版本过高与旧 Webpack/OpenSSL 冲突降到项目要求的 Node 版本或加--openssl-legacy-provider前端构建进程崩报 heap out of memoryWebpack 默认堆内存不够设置NODE_OPTIONS--max-old-space-size4096后端编译时 sqlite 相关文件被排除CGO 被禁用或缺少 sqlite 构建标签开CGO_ENABLED1编译时带-tags sqlite容器能启动但访问页面白屏public 静态资源没有拷进镜像或产物不是本次编译的确认运行时目录下有 public/且与二进制版本一致挂载数据卷后写权限报错宿主机目录属主与容器 UID 不一致用chown -R 472:472处理或用命名卷日志、证书、时区异常运行镜像缺少必要系统包runtime 阶段安装ca-certificates和tzdata自定义插件不生效插件目录位置和GF_PATHS_PLUGINS对不上确认插件放在环境变量指定的目录里这些坑不是理论推演基本是我和身边团队一轮轮踩出来的。每次排查到最后原因往往都很简单但因为涉及多阶段、多目录、多权限从现象定位到根因反而最花时间。所以我个人建议把这份表格直接贴在你的项目 README 里团队内会省很多沟通成本。6. 进阶不改源码也能定制的“黄金镜像”6.1 用 provisioning 预置数据源和看板如果需求只是“让新环境部署完就有监控看板”其实根本不需要动源码。Grafana 的 provisioning 机制就是为这个场景设计的镜像里预置配置文件容器启动时自动加载数据源和看板。数据源预置写一个 YAML 放进provisioning/datasources/# provisioning/datasources/prometheus.yaml apiVersion: 1 datasources: - name: Prometheus type: prometheus access: proxy url: http://prometheus:9090 isDefault: true editable: false看板预置同样简单。提前把做好的 dashboard JSON 导出放进项目里的dashboards/目录再配一个 provider# provisioning/dashboards/dashboards.yaml apiVersion: 1 providers: - name: default orgId: 1 folder: 预置看板 type: file options: path: /var/lib/grafana/dashboards然后在 Dockerfile 里把这两个目录拷贝进去COPY provisioning /etc/grafana/provisioning COPY dashboards /var/lib/grafana/dashboards这样镜像一启动数据源和看板就已经就位交付给任何团队都只需要关心连接地址。这种方式的维护成本远低于源码改造强烈建议能走 provisioning 的统统走 provisioning。6.2 预装插件和二进制扩展Grafana 插件生态很丰富但很多场景网络受限靠界面在线安装不现实。做镜像时直接把插件目录放进镜像里即可COPY plugins/ /var/lib/grafana/plugins/需要注意两个点。第一/var/lib/grafana是数据目录生产环境通常会用命名卷挂载Docker 在命名卷首次创建时会把镜像里的内容复制进卷所以预置插件能生效但如果你用的是 bind mount 挂宿主机目录镜像里预置的插件会被宿主机空目录盖住。第二插件分两类装在plugins-bundled/下的随二进制分发装在数据目录下的运行时加载。前者参与编译流程适合严格控制版本和场景后者更新灵活适合日常扩展。6.3 配置定制和源码定制到底怎么选这可能是整个二次开发项目里最值得提前想清楚的问题。我的判断标准很简单只是换个 logo、改个名字、预置数据源看板、调登录限制、配权限模式——全部走 ini 配置、环境变量、provisioning不动源码。要改登录流程、对接私有协议、改面板默认行为、增加后端接口——才走源码二次开发。两者都搞不定但你只是想要显眼的新面板——优先写 Grafana 插件插件的生命周期管理和版本升级都比改源码友好得多。源码改造一时爽升级火葬场。每次 Grafana 大版本升级你都要重新 merge 自己的改动冲突解决成本会随改动范围线性上升。所以哪怕是做源码级定制也尽量把改动收敛在少数的组件和包内并做好注释和提交信息给自己留一条能“跑得掉”的路。7. 我实测下来的一些体会构建 Grafana 自定义镜像这件事做到能跑不难做到能交付、能长期维护才是真功夫。我个人强烈建议把 Dockerfile、provisioning、源码改动点都放在同一个项目仓库里用 CI 自动触发构建镜像 TAG 直接关联 commit——比如grafana-custom:10.4.2-$(git rev-parse --short HEAD)。这样任何人拿到一个镜像 tag都能立刻定位到对应的源码版本排查线上问题的速度会快很多。另外还有一个容易被忽略的小技巧每次构建前在本地先跑一次go test和前端 lint别把编译通过当成质量合格。Grafana 这种体量的项目编译只能证明语法没错不能证明你没有把某个 import 改错。我在实际项目中吃过一次亏——只是改动了一个配置读取函数编译全通过启动时报 nil pointer查了半天才发现是某个初始化顺序被我不小心动到了。从那以后任何源码改动合并前必须至少跑一遍后端相关的测试用例。最后再分享一个习惯镜像推送到私有仓库前用 Trivy 之类的工具扫一遍漏洞尤其是运行时那个 Debian 基础镜像。很多人把精力都花在编译阶段最后挂在“镜像体积大”“漏洞多”这类收尾问题上反而返工。整体流程按“版本锁死—源码改造—本地编译—三阶段构建—配置验证—扫描推送”的顺序走下来你会发现自己也能稳稳地交付一个生产级的 Grafana 定制镜像。