搭建

搭建

先将域名指定好服务器ip
运行以下脚本安装

curl -L https://raw.githubusercontent.com/nezhahq/scripts/refs/heads/main/install.sh -o nezha.sh && chmod +x nezha.sh && ./nezha.sh

以 Docker 安装为例,安装完成后按提示输入以下信息:

  • 请输入站点标题: - 自定义站点标题。
  • 请输入暴露端口: - 公开访问端口(默认 8008,可自定义)。
  • 请指定后台语言: - 选择语言偏好。

输入完成后,等待拉取 Docker 镜像。安装结束后,如果一切正常,你可以通过域名和端口号访问 Dashboard,例如:
http://dashboard.example.com

宝塔反代:(省略宝塔安装)

创建一个网站,静态解开
打开配置文件菜单,在最后面填上以下代码,然后点保存:(如果在安装哪吒监控时,你自定义了端口,把下面代码中的8008改为你自定义的端口)

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

宝塔反代.jpeg
宝塔反代.jpeg

关键一步
打开反向代理 —— 添加反向代理,127.0.01:8008,保存.
然后点击上图中添加的反向代理目录中的配置文件,将里面的内容全选删除,并替换为以下代码,然后点保存:(如果在安装哪吒监控时,你自定义了端口,把下面代码中的8008改为你自定义的端口)

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 $http_cf_connecting_ip;
    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 地址段
real_ip_header CF-Connecting-IP; # CDN 私有 header,此处为 CloudFlare 默认

# gRPC 服务
location ^~ /proto.NezhaService/ {
    grpc_set_header Host $host;
    grpc_set_header nz-realip $http_CF_Connecting_IP; 
    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 $http_cf_connecting_ip; 
    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;
}

然后开启ssl。

关键步骤

更改面板密码后,进入管理后台-系统设置,Agent对接地址【域名/IP:端口】中更改为: dashboard.example.com:8008 ,前端真实IP请求头更改为:

CF-Connecting-IP

Snipaste_2025-08-19_21-38-16.jpg
Snipaste_2025-08-19_21-38-16.jpg

美化

<script>
  window.TrafficScriptConfig = {
    showTrafficStats: true,    // 显示流量统计
    insertAfter: true,         // 如果开启总流量卡片, 放置在总流量卡片后面
    interval: 60000,           // 60秒刷新缓存, 单位毫秒
    toggleInterval: 4000,      // 4秒切换流量进度条右上角内容, 0秒不切换, 单位毫秒
    duration: 500,             // 缓进缓出切换时间, 单位毫秒
    enableLog: false           // 开启日志
  };
</script>
<script src="https://cdn.jsdelivr.net/gh/ziwiwiz/nezha-ui@main/traffic-progress.js"></script>



<script>
   // window.CustomBackgroundImage = "https://bing.img.run/rand_uhd.php"; /* 页面背景图 */
   // window.CustomMobileBackgroundImage = "https://bing.img.run/rand_m.php"; /* 移动端页面背景图 */
    /* 关掉人物插图 */
   // window.DisableAnimatedMan = "true";
    /* 自定义描述 */
    window.CustomDesc = " ";
    /* 固定顶部服务器名称 */
    window.FixedTopServerName = "true";
    /* 卡片显示上下行流量 */
    // window.ShowNetTransfer = "true";
</script>

哪吒详情页直接展示网络波动卡片

网络波动卡片(网络波动在服务器各项参数下方

/* 源自https://www.nodeseek.com/post-349102-1 */
<script src="https://cdn.jsdelivr.net/gh/ziwiwiz/nezha-ui@main/netstatus-autoshow.js"></script>

或(网络波动在服务器各项参数下方和上述链接的效果一致)

<script>
const selectorButton = '#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > section > div.flex.justify-center.w-full.max-w-\\[200px\\] > div > div > div.relative.cursor-pointer.rounded-3xl.px-2\\.5.py-\\[8px\\].text-\\[13px\\].font-\\[600\\].transition-all.duration-500.text-stone-400.dark\\:text-stone-500';
const selectorSection = '#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > section';

const selector3 = '#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > div:nth-child(3)';
const selector4 = '#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > div:nth-child(4)';

let hasClicked = false;
let divVisible = false;

function forceBothVisible() {
  const div3 = document.querySelector(selector3);
  const div4 = document.querySelector(selector4);
  if (div3 && div4) {
    div3.style.display = 'block';
    div4.style.display = 'block';
  }
}

function hideSection() {
  const section = document.querySelector(selectorSection);
  if (section) {
    section.style.display = 'none';
  }
}

function tryClickButton() {
  const btn = document.querySelector(selectorButton);
  if (btn && !hasClicked) {
    btn.click();
    hasClicked = true;
    setTimeout(forceBothVisible, 500);
  }
}

const observer = new MutationObserver(() => {
  const div3 = document.querySelector(selector3);
  const div4 = document.querySelector(selector4);

  const isDiv3Visible = div3 && getComputedStyle(div3).display !== 'none';
  const isDiv4Visible = div4 && getComputedStyle(div4).display !== 'none';

  const isAnyDivVisible = isDiv3Visible || isDiv4Visible;

  if (isAnyDivVisible && !divVisible) {
    hideSection();
    tryClickButton();
  } else if (!isAnyDivVisible && divVisible) {
    hasClicked = false;
  }

  divVisible = isAnyDivVisible;

  if (div3 && div4) {
    if (!isDiv3Visible || !isDiv4Visible) {
      forceBothVisible();
    }
  }
});

const root = document.querySelector('#root');
if (root) {
  observer.observe(root, {
    childList: true,
    attributes: true,
    subtree: true,
    attributeFilter: ['style', 'class']
  });
}
</script>

或(网络波动在服务器各项参数上方

<script>
const selectorButton = '#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > section > div.flex.justify-center.w-full.max-w-\\[200px\\] > div > div > div.relative.cursor-pointer.rounded-3xl.px-2\\.5.py-\\[8px\\].text-\\[13px\\].font-\\[600\\].transition-all.duration-500.text-stone-400.dark\\:text-stone-500';
const selectorSection = '#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > section';
const selector3 = '#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > div:nth-child(3)';
const selector4 = '#root > div > main > div.mx-auto.w-full.max-w-5xl.px-0.flex.flex-col.gap-4.server-info > div:nth-child(4)';

let hasClicked = false;
let divVisible = false;
let swapping = false;

function forceBothVisible() {
  const div3 = document.querySelector(selector3);
  const div4 = document.querySelector(selector4);
  if (div3 && div4) {
    div3.style.display = 'block';
    div4.style.display = 'block';
  }
}

function hideSection() {
  const section = document.querySelector(selectorSection);
  if (section) {
    section.style.display = 'none';
  }
}

function tryClickButton() {
  const btn = document.querySelector(selectorButton);
  if (btn && !hasClicked) {
    btn.click();
    hasClicked = true;
    setTimeout(forceBothVisible, 500);
  }
}

function swapDiv3AndDiv4() {
  if (swapping) return;
  swapping = true;

  const div3 = document.querySelector(selector3);
  const div4 = document.querySelector(selector4);
  if (!div3 || !div4) {
    swapping = false;
    return;
  }
  const parent = div3.parentNode;
  if (parent !== div4.parentNode) {
    swapping = false;
    return;
  }

  // 交换 div3 和 div4 的位置
  parent.insertBefore(div4, div3);
  parent.insertBefore(div3, div4.nextSibling);

  swapping = false;
}

const observer = new MutationObserver(() => {
  const div3 = document.querySelector(selector3);
  const div4 = document.querySelector(selector4);

  const isDiv3Visible = div3 && getComputedStyle(div3).display !== 'none';
  const isDiv4Visible = div4 && getComputedStyle(div4).display !== 'none';

  const isAnyDivVisible = isDiv3Visible || isDiv4Visible;

  if (isAnyDivVisible && !divVisible) {
    hideSection();
    tryClickButton();
    setTimeout(swapDiv3AndDiv4, 100); 
  } else if (!isAnyDivVisible && divVisible) {
    hasClicked = false;
  }

  divVisible = isAnyDivVisible;

  if (div3 && div4) {
    if (!isDiv3Visible || !isDiv4Visible) {
      forceBothVisible();
    }
  }
});

const root = document.querySelector('#root');
if (root) {
  observer.observe(root, {
    childList: true,
    attributes: true,
    subtree: true,
    attributeFilter: ['style', 'class']
  });
}
</script>