从零搭建一套自托管、带时间轴趋势、精确到每篇文章的博客访问统计系统。
全文按两种典型场景给出完整配置:Umami 直接部署在公网服务器,或 Umami 部署在其他机器、由公网服务器转发

一、最终效果

你将获得

  • 每篇文章的 PV/UV、停留时长、跳出率

  • 按小时/天/月/年的时间轴趋势图

  • 访客地域(国家/省/市)、设备、浏览器、来源网站统计

  • 实时在线访客

  • 数据 100% 存在自己服务器上

Umami 全时间统计仪表盘

▲ 最终效果:全时间范围仪表盘

两种部署场景

场景 A:公网服务器直接部署——Umami 和 Nginx 在同一台公网服务器:

访客浏览器
   │  ① 加载跟踪脚本 script.js(HTTPS)
   ▼
umami.example.com(公网服务器)
   │  ② Nginx SSL 终止 + 反向代理
   ▼
127.0.0.1:3001(Umami 容器)──► PostgreSQL 容器

场景 B:公网服务器转发部署——Umami 在另一台机器(如家里/内网的服务器),公网服务器只做入口:

访客浏览器
   │  ① 加载跟踪脚本 script.js(HTTPS)
   ▼
umami.example.com(公网服务器)
   │  ② Nginx SSL 终止
   ▼
127.0.0.1:13001
   │  ③ 内网穿透隧道(frp / NPS / Tailscale 等均可)
   ▼
后端机器 Umami 容器 ──► PostgreSQL 容器

组件

作用

Umami

统计核心(Docker 部署)

PostgreSQL

数据存储(Docker 部署)

Nginx

SSL 证书 + 反向代理(公网服务器)

内网穿透工具

仅场景 B 需要,把 Umami 端口映射到公网服务器

二、准备工作

  • 一台已安装 Docker 的服务器(本文以 Debian 12 为例;场景 A 即公网服务器本身,场景 B 为内网/其他机器)

  • 一台公网服务器(已配置 Nginx + Let's Encrypt 证书)

  • 一个域名(已将 umami.你的域名.com A 记录解析到公网服务器 IP)

三、第一步:Docker 部署 Umami

Docker Compose(推荐)

创建 docker-compose.yaml

services:
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    ports:
      - "127.0.0.1:3001:3000"
    environment:
      DATABASE_URL: postgresql://umami:换成强密码@db:5432/umami
      DATABASE_TYPE: postgresql
      APP_SECRET: 换成随机字符串
    depends_on:
      - db
    restart: always

db:
image: postgres:16
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: 换成强密码
volumes:
- pg-data:/var/lib/postgresql/data
restart: always

volumes:
pg-data:

启动并验证:

docker compose up -d
docker ps | grep umami

umami ghcr.io/umami-software/umami:postgresql-latest Up (healthy) 127.0.0.1:3001->3000/tcp

curl -o /dev/null -w “%{http_code}\n” http://127.0.0.1:3001 # 200

端口绑定的讲究:场景 A 中 Umami 只服务本机 Nginx,绑 127.0.0.1:3001 即可,不对外暴露;场景 B 中隧道工具可能运行在另一台机器上,需要改成 “3001:3000”(监听 0.0.0.0),并注意用防火墙限制来源。

Umami 支持 PostgreSQL 和 MySQL(MariaDB),二选一即可。首次启动自动建表。

也可以用 1Panel 应用商店

如果你用 1Panel 管理服务器:应用商店 → 搜索 Umami → 安装,数据库选择已有的 PostgreSQL(1Panel 会自动建库、生成连接串),装完同样在「容器」里确认端口映射即可。

四、第二步:初始化与网站添加

  1. 访问 http://服务器IP:3001(或场景 A 中先跳过,直接配好域名后访问域名),默认账号 admin / umami

Umami 登录页
  1. 立刻改密码:右上角头像 → 设置 → 个人资料

  2. 添加网站:设置 → 网站 → 添加网站

- 名称:Blog

- 域名:blog.你的域名.com

  1. 记录网站的 Website ID(UUID 格式,形如 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx),后面要用

网站列表

▲ 添加完成后的网站列表

五、场景 A:公网服务器直接部署(Nginx + SSL)

Umami 就在公网服务器上时,只需让 Nginx 把域名流量转给本机的 Umami 端口。新建 /etc/nginx/conf.d/umami.conf

server {
listen 443 ssl;
server_name umami.example.com;

ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

location / {
proxy_pass http://127.0.0.1:3001;
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 X-Forwarded-Proto $scheme;
# X-Forwarded-For 必须透传:Umami 靠它识别访客真实 IP 和地域

# 【特殊处理①】允许 Halo 后台 / 博客页面以 iframe 嵌入 Umami
# 缺少 frame-ancestors 时,Halo Umami 插件、数据看板的嵌入式面板会打不开
# 把下面两个来源换成你的 Halo 后台地址和博客地址
proxy_hide_header X-Frame-Options;
add_header Content-Security-Policy "frame-ancestors 'self' https://blog.example.com https://console.example.com" always;

# 【特殊处理②】允许博客页面跨域调用 Umami API
# 数据看板插件的前端脚本(如首页热力图)在博客域名下直接请求 Umami API,需要 CORS 放行
add_header Access-Control-Allow-Origin "*" always;

}

}

server {
listen 80;
server_name umami.example.com;
return 301 https://$host$request_uri;
}

nginx -t && nginx -s reload

六、场景 B:公网服务器转发部署

Umami 在另一台机器(内网服务器、家里的 NAS 等)时,链路多一段:先用内网穿透工具把 Umami 端口映射到公网服务器的本地端口,Nginx 再反代这个本地端口。

6.1 打通隧道

任选一种内网穿透方案(frp、NPS、Tailscale 等都可以),目标只有一个:在公网服务器上能通过 127.0.0.1:13001 访问到后端机器的 Umami。以 frp 为例,在后端机器运行 frpc,配置一条 TCP 隧道:

[[proxies]]
name = “umami”
type = “tcp”
localIP = “后端机器IP”
localPort = 3001 # Umami 端口
remotePort = 13001 # 映射到公网服务器的本地端口(未被占用即可)

隧道建立后,在公网服务器上验证:

curl -o /dev/null -w “%{http_code}\n” http://127.0.0.1:13001   # 200

注意用隧道工具自带的功能(如 frp 的 stcp/访客模式,或云安全组不放行 13001)避免该端口直接暴露公网——Nginx 走 127.0.0.1 就够了。

6.2 Nginx 配置

与场景 A 几乎相同,仅 proxy_pass 指向隧道端口:

server {
listen 443 ssl;
server_name umami.example.com;

ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

location / {
proxy_pass http://127.0.0.1:13001;
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 X-Forwarded-Proto $scheme;

# 特殊处理①②与场景 A 相同:frame-ancestors 放行嵌入来源、CORS 放行 API 跨域
proxy_hide_header X-Frame-Options;
add_header Content-Security-Policy "frame-ancestors 'self' https://blog.example.com https://console.example.com" always;
add_header Access-Control-Allow-Origin "*" always;

}

}

server {
listen 80;
server_name umami.example.com;
return 301 https://$host$request_uri;
}

验证整条链路(两个场景通用)

curl -o /dev/null -w “%{http_code}\n” https://umami.example.com/           # 200
curl -o /dev/null -w “%{http_code}\n” https://umami.example.com/script.js # 200(关键:跟踪脚本公网可加载)
curl -o /dev/null -w “%{http_code}\n” https://umami.example.com/api/heartbeat # 200

七、第三步:Halo 注入跟踪代码

Halo 后台 → 设置 → 代码注入 → 全局 head 标签,粘贴:

<script defer src=“https://umami.example.com/script.js
data-website-id=“你的Website-ID”></script>
Halo 代码注入页面

▲ 粘贴到「设置 → 代码注入 → 全局 head 标签」

保存后验证:

curl -s https://blog.你的域名.com/ | grep umami

能看到上面那行 script 即注入成功

打开博客随便点几篇文章,Umami 后台「实时」面板立刻能看到访客。

八、(可选)历史数据迁移

如果博客之前用 Halo 自带计数器积累了访问量(Halo 只存累计总数,无时间维度),有三种处理方式:

方案

做法

效果

A 双轨制

Umami 从零开始,老数据留在 Halo 计数器展示

零风险,Umami 无历史

B 重放到今天

脚本按每篇文章的计数调 /api/send 重放

数字对齐,但全部记为今天

C 直写数据库

按文章发布时间把 PV 分布到历史时间轴,直接 INSERT 进 PostgreSQL

有完整"历史趋势",但分布是模拟的

方案 C 的要点:

  1. 先备份pg_dump -U 用户 库名 > backup.sql

  2. 从 Halo 数据库导出:文章 permalink、发布时间、visit 计数(计数器在 extensions/registry/metrics.halo.run/counters/posts.content.halo.run/* 下)

  3. 生成会话(session)+ 事件(website_event)记录:时间戳随机分布于"发布日→今天"、按 1.7 页/会话聚类、随机化浏览器/地域画像

  4. 分批 INSERT 导入,Umami API 验证总数一致

  5. 注意别漏:Halo 首页显示的总访问量还包含自定义单页(singlepages 计数器),也要一并导入

九、常见问题

问题

原因

解决

后台网站图标裂图

favicon 走 icons.duckduckgo.com,部分网络不可达

纯装饰,不影响功能

Umami 统计全是 0

只建了网站没注入跟踪代码

检查博客页面源码有无 script.js

访客地域全是内网 IP

Nginx 没透传 X-Forwarded-For

检查 proxy_set_header

Halo 插件里嵌入的 Umami 页面打不开

缺少 frame-ancestors 或存在 X-Frame-Options 拦截

按 Nginx 配置中的【特殊处理①】放行嵌入来源

热力图 / 看板图表一片空白

博客页面跨域调 Umami API 被 CORS 拦截

按 Nginx 配置中的【特殊处理②】放行跨域

script.js 加载失败

域名未解析 / SSL 未配好 / 端口未通

按第五、六章的 curl 逐段排查

升级 Umami 后白屏

浏览器缓存了旧版静态资源

强制刷新(Ctrl/Cmd+Shift+R)

十、进阶集成

以下集成都依赖 Halo 插件,先从 Halo 后台「应用市场」安装对应插件:

  • 数据看板插件(data-statistics)——热力图就靠它。应用市场搜索「数据看板」安装后,在插件设置里填入 Umami 地址 + Website ID + 账号密码,即可在文章里插入访问量趋势、热门文章排行等图表。注意:它的前端脚本在博客域名下跨域请求 Umami API,必须完成 Nginx【特殊处理②】的 CORS 配置,否则图表空白

  • 首页热力图:数据看板插件的前台脚本全局加载后,在页面任意位置放 <div class=“xhhaocom-chartboard” data-types=“articles”></div> 即可自动渲染年度热力图

博客首页的年度文章热力图

▲ 首页文章列表上方嵌入的年度热力图(数据看板插件渲染)

  • Halo Umami 插件:把 Umami 面板直接嵌入 Halo 后台查看。必须完成 Nginx【特殊处理①】的 frame-ancestors 配置,否则嵌入页无法打开

  • API 玩法:Umami 提供完整 REST API(/api/websites/{id}/stats/pageviews?unit=month),可自绘任意统计图

环境信息:Halo 2.25 / Umami 3.x / PostgreSQL 16 / Debian 12