从零搭建一套自托管、带时间轴趋势、精确到每篇文章的博客访问统计系统。
全文按两种典型场景给出完整配置:Umami 直接部署在公网服务器,或 Umami 部署在其他机器、由公网服务器转发。
一、最终效果
你将获得
每篇文章的 PV/UV、停留时长、跳出率
按小时/天/月/年的时间轴趋势图
访客地域(国家/省/市)、设备、浏览器、来源网站统计
实时在线访客
数据 100% 存在自己服务器上

▲ 最终效果:全时间范围仪表盘
两种部署场景
场景 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 容器二、准备工作
一台已安装 Docker 的服务器(本文以 Debian 12 为例;场景 A 即公网服务器本身,场景 B 为内网/其他机器)
一台公网服务器(已配置 Nginx + Let's Encrypt 证书)
一个域名(已将
umami.你的域名.comA 记录解析到公网服务器 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: alwaysdb:
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 umamiumami 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 会自动建库、生成连接串),装完同样在「容器」里确认端口映射即可。
四、第二步:初始化与网站添加
访问
http://服务器IP:3001(或场景 A 中先跳过,直接配好域名后访问域名),默认账号admin/umami

立刻改密码:右上角头像 → 设置 → 个人资料
添加网站:设置 → 网站 → 添加网站
- 名称:Blog
- 域名:blog.你的域名.com
记录网站的 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>
▲ 粘贴到「设置 → 代码注入 → 全局 head 标签」
保存后验证:
curl -s https://blog.你的域名.com/ | grep umami
能看到上面那行 script 即注入成功
打开博客随便点几篇文章,Umami 后台「实时」面板立刻能看到访客。
八、(可选)历史数据迁移
如果博客之前用 Halo 自带计数器积累了访问量(Halo 只存累计总数,无时间维度),有三种处理方式:
方案 C 的要点:
先备份:
pg_dump -U 用户 库名 > backup.sql从 Halo 数据库导出:文章 permalink、发布时间、visit 计数(计数器在
extensions表/registry/metrics.halo.run/counters/posts.content.halo.run/*下)生成会话(session)+ 事件(website_event)记录:时间戳随机分布于"发布日→今天"、按 1.7 页/会话聚类、随机化浏览器/地域画像
分批
INSERT导入,Umami API 验证总数一致注意别漏:Halo 首页显示的总访问量还包含自定义单页(singlepages 计数器),也要一并导入
九、常见问题
十、进阶集成
以下集成都依赖 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
No.67 Halo 博客访问量统计系统:Umami 自托管部署完整教程
https://blog.songshiyu.cn/archives/halobo-ke-fang-wen-liang-tong-ji-xi-tong-umami-zi-tuo-guan-bu-shu-wan-zheng-jiao-cheng
评论