哪吒面板宝塔搭建教程(反代 8008 + WebSocket + gRPC)

1968 字
10 分钟
哪吒面板宝塔搭建教程(反代 8008 + WebSocket + gRPC)

这篇教程适合用宝塔面板部署 哪吒监控 Dashboard,然后通过域名反向代理访问。

按你这套方式搭,核心就几步:

  • 先用脚本安装哪吒面板
  • 确认 Dashboard 跑在 8008 端口
  • 宝塔创建站点并反代 8008
  • 在 Nginx 配置里补上 upstreamgRPCWebSocket

我核对了哪吒官方文档,2026-05-11 这天官方文档里仍然说明:V1 之后 Dashboard 和 gRPC 默认共用 8008 端口。

效果图:

哪吒面板首页
哪吒面板首页

一、先安装哪吒面板#

先用 SSH 登录你的服务器,直接执行安装命令:

Terminal window
curl -L https://raw.githubusercontent.com/naiba/nezha/master/script/install.sh -o nezha.sh && chmod +x nezha.sh && ./nezha.sh

执行后根据脚本提示完成安装即可。

如果安装成功,哪吒 Dashboard 默认会监听:

127.0.0.1:8008

也就是说,后面宝塔反代时,目标地址就是这个端口。

二、宝塔添加网站#

安装完后,先在宝塔里新建一个网站,绑定你准备给哪吒面板使用的域名。

创建完站点后,进入站点设置,后面我们会改它的 Nginx 配置。

三、在站点配置里添加 upstream#

打开宝塔站点配置文件,在合适位置先加入:

upstream dashboard {
keepalive 512;
server 127.0.0.1:8008;
}

参考图:

宝塔配置里添加 upstream
宝塔配置里添加 upstream

这一段的作用,是给后面的 grpc_pass grpc://dashboard; 提供上游目标。

四、宝塔反代 8008 端口#

接着在宝塔站点里创建反向代理,目标填:

http://127.0.0.1:8008

先把基础反代建好,然后再手动修改配置文件。

因为哪吒面板不只是普通网页,它还会用到:

  • gRPC
  • WebSocket

所以如果只点宝塔默认反代而不补配置,后面常见问题就是:

  • Agent 连不上
  • WebSocket 终端打不开
  • 文件管理或在线终端异常

五、修改宝塔站点反代配置#

把反代配置改成下面这样:

#PROXY-START/
location ^~ / {
proxy_pass http://127.0.0.1:8008; # 改成自己的端口
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header nz-realip $remote_addr;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffer_size 128k;
proxy_buffers 4 128k;
proxy_busy_buffers_size 256k;
proxy_max_temp_file_size 0;
add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache;
proxy_ssl_server_name off;
proxy_ssl_name $proxy_host;
add_header Strict-Transport-Security "max-age=31536000";
}
underscores_in_headers on;
set_real_ip_from 0.0.0.0/0; # CDN 回源 IP 地址段
# gRPC 服务
location ^~ /proto.NezhaService/ {
grpc_set_header Host $host;
grpc_set_header nz-realip $remote_addr;
grpc_read_timeout 600s;
grpc_send_timeout 600s;
grpc_socket_keepalive on;
client_max_body_size 10m;
grpc_buffer_size 4m;
grpc_pass grpc://dashboard;
}
# WebSocket 服务
location ~* ^/api/v1/ws/(server|terminal|file)(.*)$ {
proxy_set_header Host $host;
proxy_set_header nz-realip $remote_addr;
proxy_set_header Origin https://$host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_pass http://127.0.0.1:8008; # 改成自己的端口
}
#PROXY-END/

参考图:

哪吒面板反代配置
哪吒面板反代配置

如果你前面不是用 8008,那这里所有的 127.0.0.1:8008 都改成你自己的实际端口。

六、登录哪吒 Dashboard#

按官方文档当前说明,安装完成后的默认后台地址一般是:

https://你的域名/dashboard

首次默认账号密码是:

admin / admin

登录后建议第一时间修改密码。

七、添加自定义美化代码#

登录后台后,进入设置,把下面这段代码粘贴到:

  • 自定义代码(样式和脚本)

参考图:

哪吒自定义代码位置
哪吒自定义代码位置

代码如下:

<script>
/* 这部分这几个挂在 window 下的变量是哪吒内置的, 详见 https://nezha.wiki/guide/settings.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E4%BB%A3%E7%A0%81 */
window.CustomBackgroundImage = 'https://im.gurl.eu.org/file/d9e924748b3a2ec7af338.jpg'; /* PC 端背景图 */
window.CustomMobileBackgroundImage = 'https://im.gurl.eu.org/file/d9e924748b3a2ec7af338.jpg'; /* 移动端背景图 */
window.CustomDesc = 'Assute'; /* 页面左上角副标题 */
window.ShowNetTransfer = true; /* 服务器卡片是否显示上下行流量, 默认不显示 */
window.DisableAnimatedMan = true;
window.CustomIllustration = 'https://r2.wuxie.de/blog/20250415_762b4cb3.webp';
window.FixedTopServerName = true; /* 是否固定顶部显示服务器名称, 默认不固定 */
/* window.CustomLinks = '[{\"link\":\"https://xiny.cc/\",\"name\":\"首页\"},{\"link\":\"https://blog.xiny.cc/\",\"name\":\"博客\"}]'; *//* 自定义导航栏链接 */
/* 自定义字体, 注意需要同步修改下方 CSS 中的 font-family */
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://font.sec.miui.com/font/css?family=MiSans:400,700:MiSans'; // MiSans
// link.href = 'https://npm.elemecdn.com/lxgw-wenkai-screen-webfont@1.7.0/style.css'; // 霞鹜文楷, font-family: 'LXGW WenKai Screen'
document.head.appendChild(link);
</script>
<script>
window.FixedTopServerName = true; /* 是否固定顶部显示服务器名称, 默认不固定 */
setInterval(function() {
function formatFileSize(bytes) {
if (bytes === 0) return { value: '0', unit: 'B' };
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
let unitIndex = 0;
let size = bytes;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
let decimalPlaces = 1;
if (unitIndex === 0) decimalPlaces = 0;
return {
value: size.toFixed(decimalPlaces),
unit: units[unitIndex]
};
}
function calculatePercentage(used, total) {
if (typeof used === 'string') used = Number(used);
if (typeof total === 'string') total = Number(total);
if (used > 1e15 || total > 1e15) {
used = used / 1e10;
total = total / 1e10;
}
return (used / total * 100).toFixed(1);
}
fetch('/api/v1/service')
.then(response => response.json())
.then(data => {
if (data.success) {
const trafficData = data.data.cycle_transfer_stats;
const serverMap = new Map();
for (const cycleId in trafficData) {
const cycle = trafficData[cycleId];
if (cycle.server_name && cycle.transfer) {
for (const serverId in cycle.server_name) {
const serverName = cycle.server_name[serverId];
const transfer = cycle.transfer[serverId];
const max = cycle.max;
if (serverName && transfer !== undefined && max) {
serverMap.set(serverName, {
id: serverId,
transfer: transfer,
max: max,
name: cycle.name
});
}
}
}
}
serverMap.forEach((serverData, serverName) => {
const targetElement = Array.from(document.querySelectorAll('section.grid.items-center.gap-2')).find(el =>
el.textContent.trim().includes(serverName));
if (targetElement) {
const usedFormatted = formatFileSize(serverData.transfer);
const totalFormatted = formatFileSize(serverData.max);
const percentage = calculatePercentage(serverData.transfer, serverData.max);
const uniqueClassName = 'traffic-stats-for-server-' + serverData.id;
const insertedElement = targetElement.parentNode.querySelector('.' + uniqueClassName);
if (insertedElement) {
const usedSpan = insertedElement.querySelector('.used-traffic');
const usedUnitSpan = insertedElement.querySelector('.used-unit');
const totalSpan = insertedElement.querySelector('.total-traffic');
const totalUnitSpan = insertedElement.querySelector('.total-unit');
const percentageSpan = insertedElement.querySelector('.percentage-value');
const progressBar = insertedElement.querySelector('.progress-bar');
if (usedSpan) usedSpan.textContent = usedFormatted.value;
if (usedUnitSpan) usedUnitSpan.textContent = usedFormatted.unit;
if (totalSpan) totalSpan.textContent = totalFormatted.value;
if (totalUnitSpan) totalUnitSpan.textContent = totalFormatted.unit;
if (percentageSpan) percentageSpan.textContent = percentage + '%';
if (progressBar) progressBar.style.width = percentage + '%';
return;
}
let currentElement = targetElement;
for (let i = 0; i < 2; i++) {
currentElement = currentElement.nextElementSibling;
if (!currentElement) {
currentElement = targetElement;
currentElement = currentElement.nextElementSibling;
}
}
const newElement = document.createElement('div');
newElement.classList.add('space-y-1.5', 'new-inserted-element', uniqueClassName);
newElement.style.width = '100%';
newElement.innerHTML = `
<div class="flex items-center justify-between">
<div class="flex items-baseline gap-1">
<span class="text-sm font-medium text-neutral-800 dark:text-neutral-200 used-traffic">${usedFormatted.value}</span>
<span class="text-sm font-medium text-neutral-800 dark:text-neutral-200 used-unit">${usedFormatted.unit}</span>
<span class="text-xs text-neutral-500 dark:text-neutral-400">/ </span>
<span class="text-xs text-neutral-500 dark:text-neutral-400 total-traffic">${totalFormatted.value}</span>
<span class="text-xs text-neutral-500 dark:text-neutral-400 total-unit">${totalFormatted.unit}</span>
</div>
<span class="text-xs font-medium text-neutral-600 dark:text-neutral-300 percentage-value">${percentage}%</span>
</div>
<div class="relative h-1.5">
<div class="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full"></div>
<div class="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300 progress-bar" style="width: ${percentage}%;"></div>
</div>
`;
currentElement.insertAdjacentElement('afterend', newElement);
} else {
console.log(`没有找到服务器 ${serverName}(ID: ${serverData.id}) 的元素`);
}
});
} else {
console.log('API请求成功但返回数据不正确');
}
})
.catch(error => {
console.error('获取流量数据失败:', error);
});
}, 3000);
</script>
<script>
/*
window.TrafficScriptConfig = {
showTrafficStats: true, // 显示流量统计, 默认开启
insertAfter: true, // 如果开启总流量卡片, 是否放置在总流量卡片后面, 默认为true
interval: 60000, // 60秒刷新缓存, 单位毫秒, 默认60秒
toggleInterval: 5000, // 4秒切换流量进度条右上角内容, 0秒不切换, 单位毫秒, 默认5秒
duration: 500, // 缓出缓进切换时间, 单位毫秒, 默认500毫秒
enableLog: false // 开启日志, 默认关闭
};
*/
</script>
<script src="https://cdn.jsdelivr.net/gh/8730062/installnet@main/jdt.js"></script>

保存后,面板前台就会按这段脚本显示背景图、插画、副标题和流量进度条。

八、添加流量警报规则#

如果前面已经加了自定义代码,前台就能显示流量使用进度。
如果还想在流量接近上限时收到通知,可以继续添加流量警报规则。

先打开规则生成器:

https://wiziscool.github.io/Nezha-Traffic-Alarm-Generator/

在页面里按自己的周期和流量阈值生成规则即可。

参考图:

哪吒流量警报规则生成器
哪吒流量警报规则生成器

生成完成后,复制规则内容,回到哪吒后台:

  • 打开 通知
  • 进入 警告规则
  • 添加刚刚生成的流量警报规则
  • 保存后启用

参考图:

哪吒通知里的警告规则
哪吒通知里的警告规则

哪吒启用流量警报规则
哪吒启用流量警报规则

效果图:

哪吒流量使用进度效果
哪吒流量使用进度效果

这样前台既能显示流量使用进度,流量达到设定阈值时也会按通知方式推送提醒。

九、在服务里添加 Ping 节点#

如果你想让面板显示广东三网的 Ping,可以在服务里添加这几个地址:

  • 广东联通:gd-cu-v4.ip.zstaticcdn.com:80
  • 广东移动:gd-cm-v4.ip.zstaticcdn.com:80
  • 广东电信:gd-ct-v4.ip.zstaticcdn.com:80

添加后,前台就能看到这几个节点的探测结果。

十、如果前面套了 CDN#

如果你的域名前面还接了 CDN,那真实 IP 头不一定还是 $remote_addr

这时候通常要把:

set_real_ip_from 0.0.0.0/0;

和真实 IP 头按你的 CDN 去改。

例如 Cloudflare 常见写法会用到:

real_ip_header CF-Connecting-IP;

如果你现在是 Nginx 直接对外,没有再套 CDN,那按你现在这版配置先跑就可以。

十一、完成#

到这里,宝塔搭建哪吒面板就完成了。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

哪吒面板宝塔搭建教程(反代 8008 + WebSocket + gRPC)
https://github.com/nezhahq/nezha/releases/latest
作者
苏锦
发布于
2026-05-11
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
苏锦
Hello, I'm Assute.
公告
欢迎来到我的博客。这里主要记录脚本、网站、服务器部署、软件工具和 AI 的实战内容。
分类
标签
站点统计
文章
38
分类
7
标签
44
总字数
78,206
运行时长
0
最后活动
0 天前

目录