Umami 是一个开源网页访问统计服务,类似Google Analytics。专注于简单,快速,隐私安全。相对于把数据交给大公司,收到各种各样的限制,各种付费解锁功能,自建访问统计,自己掌握访问统计数据更好!还可以统计网页的PV, UV,功能定制化更高!

Umami 统计
本文同时包含 Umami v2(旧版)和 v3(最新版)的说明。v2 内容已标注,如果你是全新安装请直接看 v3 部分。
安装(v2 版本)
创建下面这些文件
# .env
UMAMI_DB_USER=umami
UMAMI_DB_PASSWORD=password
UMAMI_DB_NAME=umami
UMAMI_APP_SECRET=vwVXxxxxxLLJWSC# docker-compose.yaml
services:
umami:
build:
context: https://github.com/umami-software/umami.git#v2.15.1
args:
BASE_PATH: /umami
DATABASE_TYPE: postgresql
environment:
PORT: "4000"
DATABASE_URL: postgres://${UMAMI_DB_USER}:${UMAMI_DB_PASSWORD}@umami-db:5432/${UMAMI_DB_NAME}
DATABASE_TYPE: postgresql
APP_SECRET: ${UMAMI_APP_SECRET}
DISABLE_TELEMETRY: 1
healthcheck:
test: ["CMD-SHELL", "curl http://localhost:4000/api/heartbeat"]
interval: 60s
timeout: 15s
retries: 5
depends_on:
umami-db:
condition: service_healthy
deploy:
resources:
limits:
memory: 2G
cpus: "1"
umami-db:
image: postgres:15-alpine
environment:
- POSTGRES_PASSWORD=${UMAMI_DB_PASSWORD}
- POSTGRES_USER=${UMAMI_DB_USER}
- POSTGRES_DB=${UMAMI_DB_NAME}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 60s
timeout: 15s
retries: 5
volumes:
- umami_pg_data:/var/lib/postgresql/data
volumes:
umami_pg_data:# docker build and install
$ docker compose up --wait --wait-timeout 360
# docker umami logs 查看
$ docker compose logs umami迁移到 Umami v3
Umami 从 v3 开始移除了 MySQL 支持,仅保留 PostgreSQL。同时引入了更安全的 Share Token 机制,用于前端公开统计展示,无需再暴露任何用户凭据。
v2 → v3 主要变化
| 项目 | v2 | v3 |
|---|---|---|
| 数据库 | MySQL / PostgreSQL | 仅 PostgreSQL |
| 前端认证 | Authorization: Bearer(用户 JWT) | X-Umami-Share-Token(网站级只读 token) |
| 响应格式 | {"pageviews":{"value":N}} | {"pageviews": N}(直接数字) |
| 页面过滤参数 | url | path |
| 时间戳单位 | 毫秒 | 毫秒(不变) |
安装 v3(全新安装)
# docker-compose.yaml (v3)
services:
umami:
image: ghcr.io/umami-software/umami:latest
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://${UMAMI_DB_USER}:${UMAMI_DB_PASSWORD}@umami-db:5432/${UMAMI_DB_NAME}
DATABASE_TYPE: postgresql
APP_SECRET: ${UMAMI_APP_SECRET}
depends_on:
umami-db:
condition: service_healthy
umami-db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${UMAMI_DB_PASSWORD}
POSTGRES_USER: ${UMAMI_DB_USER}
POSTGRES_DB: ${UMAMI_DB_NAME}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
volumes:
- umami_pg_data:/var/lib/postgresql/data
volumes:
umami_pg_data:MySQL → PostgreSQL 迁移(v2 升级 v3)
如果已有 v2 MySQL 数据需要迁移,官方推荐步骤如下:
- 先安装 v2 PostgreSQL 版本,让 Prisma 创建好正确的表结构
- 清空
_prisma_migrations和user表 - 用 pgloader 将 MySQL 数据导入 PostgreSQL
- 升级到 v3 镜像
具体迁移步骤可参考 官方迁移文档。
Umami UV / PV 统计显示(v3 方案)
Umami 统计了 UV PV 等各种访问数据。v3 提供了 Share Token 机制,可以为单个网站生成一个只读的 JWT token,暴露在前端也没有安全风险。
方案对比
| 方案 | 安全性 | 说明 |
|---|---|---|
| ❌ 泄露可查看所有网站 | v3 中已失效(权限检查变更) | |
| v3: Share Token | ✅ 泄露仅限单个网站 stats | 本文推荐方案 |
Umami UV / PV 统计显示(v2 方案)
以下为 v2 版本的配置方式,v3 请参考上方。
新建 View only 权限的用户(v2)
Settings` -> `Users` -> `Create user` -> 填写账号密码,`Role` 选择 `View only` -> `Save
Q: 为什么不直接调用 Umami 的 API 获取数据,而是要额外创建一个账户?
A: 博客是 静态开源无服务器 的,所有代码都展示在前端,包括 API 调用。而 Umami 的
adminAPI 权限太大了,如果使用admin权限的 API Token,那么这个 token 可以获取、修改、删除所有网站的数据,会有严重的安全隐患。所以我们需要创建一个
View only权限的用户,使用这个低权限的用户的 API Token 来访问我们的浏览量等数据。
新建 Team 并添加用户和网站
Settings -> Teams -> Create team -> 填写名称 -> Save -> 找到刚刚创建的 Team -> Edit -> 复制 Access code,点击 Websites 中点击 Add website 选中你想共享的网站
换一个浏览器登录 Umami(使用View only 权限的用户) -> Settings -> Teams -> Join team -> 输入 Access code -> Join -> 如果没有出错的话,点击 Dashboard 就可以看到你刚刚添加的网站了
获取 View only 用户的 API Token
根据 Umami 的文档,我们可以通过以下方式获取 API Token:
POST /api/auth/login例如 你的网站地址为 example.com,那么你需要使用 View only 的账户密码向 https://example.com/api/auth/login 发送一个 POST 请求,请求体为:
{
"username": "your-username",
"password": "your-password"
}如果成功,你应该会得到以下的结果:
{
"token": "eyTMjU2IiwiY...4Q0JDLUhWxnIjoiUE_A",
"user": {
"id": "cd33a605-d785-42a1-9365-d6cad3b7befd",
"username": "your-username",
"createdAt": "2020-04-20 01:00:00"
}
}保存 token 值,并在所有请求中发送带 Bearer <token> 值的 Authorization 标头。请求标头应该如下所示:
Authorization: Bearer eyTMjU2IiwiY...4Q0JDLUhWxnIjoiUE_A发送请求获取数据
这里要用到类似于 postman 的 API 测试工具,可以使用的是开源的 hoppscotch,你也可以使用 curl 或者其他工具。
先分析一下官方文档的 API 接口:GET /api/websites/{websiteId}/stats
有两个必填的 查询参数:startAt 和 endAt,都是 Unix 毫秒时间戳,表示开始时间和结束时间
websiteId 和 startAt 需要我们自己获取
websiteId 可以在 Dashboard -> View details -> 看浏览器栏的地址 https://example.com/websites/{websiteId} 中找到
startAt 可发送 GET 请求到 https://example.com/api/websites/{websiteId},请求头为
Authorization: Bearer eyTMjU2IiwiY...4Q0JDLUhWxnIjoiUE_A在返回结果中找到 createdAt 字段,这个字段就是 startAt 的值,也就是你的网站创建时间,数据的开始时间
编写页面

代码如下,修改你对应的参数即可运行:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div style="text-align:center;">
<h1>Umami 网站统计</h1>
<span>总访问量 <span id="umami-site-pv"></span> 次</span>
<span>总访客数 <span id="umami-site-uv"></span> 人</span>
</div>
<script>
// 从配置文件中获取 umami 的配置
const website_id = 'xxx';
// 拼接请求地址
const request_url = 'https://xxx.com' + '/api/websites/' + website_id + '/stats';
const start_time = new Date('2024-01-01').getTime();
const end_time = new Date().getTime();
const token = 'xxxxxx';
// 检查配置是否为空
if (!website_id) {
throw new Error("Umami website_id is empty");
}
if (!request_url) {
throw new Error("Umami request_url is empty");
}
if (!start_time) {
throw new Error("Umami start_time is empty");
}
if (!token) {
throw new Error("Umami token is empty");
}
const params = new URLSearchParams({
startAt: start_time,
endAt: end_time,
});
const request_header = {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
};
async function allStats() {
try {
const response = await fetch(`${request_url}?${params}`, request_header);
const data = await response.json();
const uniqueVisitors = data.pageviews.value; // 获取独立访客数
const pageViews = data.pageviews.value; // 获取页面浏览量
let ele1 = document.querySelector("#umami-site-pv")
if (ele1) {
ele1.textContent = pageViews; // 设置页面浏览量
}
let ele2 = document.querySelector("#umami-site-uv")
if (ele2) {
ele2.textContent = uniqueVisitors;
}
console.log(uniqueVisitors, pageViews);
console.log(data);
} catch (error) {
console.error(error);
return "-1";
}
}
allStats();
</script>
</body>
</html>Umami UV / PV 统计显示(v3 Share Token 方案)
v3 推荐的公开统计方案 — 为每个网站生成独立的 Share Token,暴露在前端也安全。
1. 创建 Share
登录 Umami 后,通过 API 为你的博客网站创建一个 Share(只需做一次):
# 登录获取 admin token
TOKEN=$(curl -s -X POST https://你的umami域名/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"你的密码"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
# 为博客网站创建 share(websiteId 替换成你的)
SHARE_RESP=$(curl -s -X POST https://你的umami域名/api/websites/{websiteId}/shares \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"blog-stats","parameters":{"overview":true,"events":true}}')
echo $SHARE_RESP记录返回的 slug 值,然后通过 slug 获取 Share Token:
# 获取 Share JWT token(只读,仅限该网站)
curl https://你的umami域名/api/share/{slug}返回:
{
"shareId": "...",
"websiteId": "...",
"token": "eyJhbGciOiJIUzI1NiIs...",
"parameters": { "overview": true, "events": true }
}token 字段的值就是 Share Token,保存到前端配置中。
2. API 差异说明
与 v2 相比,v3 有以下变化:
| 项目 | v2 | v3 |
|---|---|---|
| 认证 Header | Authorization: Bearer <token> | X-Umami-Share-Token: <token> + X-Umami-Share-Context: share |
| 响应格式 | {"pageviews":{"value":N}} | {"pageviews": N} |
| 页面过滤参数 | ?url=/path/ | ?path=/path/ |
| 时间戳 | 毫秒(不变) | 毫秒 |
3. 编写前端代码(v3)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Umami 网站统计 - v3</title>
</head>
<body>
<div style="text-align:center;">
<h1>Umami 网站统计</h1>
<span>总访问量 <span id="umami-site-pv"></span> 次</span>
<span>总访客数 <span id="umami-site-uv"></span> 人</span>
</div>
<script>
const website_id = 'xxx'; // 你的网站 UUID
const share_token = 'xxx'; // 上一步获取的 Share JWT
const api_server = 'https://你的umami域名';
const start_time = new Date('2024-01-01').getTime();
const request_url = api_server + '/api/websites/' + website_id + '/stats';
const params = new URLSearchParams({
startAt: start_time,
endAt: new Date().getTime(),
});
async function fetchStats() {
const resp = await fetch(request_url + '?' + params, {
headers: {
'X-Umami-Share-Token': share_token,
'X-Umami-Share-Context': 'share',
},
});
const data = await resp.json();
// v3 直接返回数字,无需 .value
document.querySelector('#umami-site-pv').textContent = data.pageviews;
document.querySelector('#umami-site-uv').textContent = data.visitors;
}
fetchStats();
</script>
</body>
</html>4. 获取单页浏览数
v3 的过滤参数从 url 改为了 path:
// v2: ?url=/posts/hello-world/
// v3: ?path=/posts/hello-world/
async function pageStats(path) {
const resp = await fetch(request_url + '?' + params + '&path=' + path, {
headers: {
'X-Umami-Share-Token': share_token,
'X-Umami-Share-Context': 'share',
},
});
const data = await resp.json();
// data.pageviews 就是该页面的浏览量
console.log(path, 'pageviews:', data.pageviews);
}5. 安全性说明
| 方案 | 泄露影响 |
|---|---|
| v2: View only 用户 JWT | 可访问所有网站 stats |
| v2: Admin 用户 JWT | 完全控制权(最危险) |
| v3: Share Token | 仅限单个网站 stats,只读 |
Share Token 的 JWT payload 只包含:
{ "shareId": "...", "websiteId": "...", "shareType": 1 }没有用户身份,没有管理权限。即使完全公开在浏览器中,攻击者也只能看到这一个网站的访问统计数据,而这些数据在页面上本来就已经公开显示。
6. Token 过期处理
Share Token 永不过期(除非你删除对应的 Share)。如果需要更换,只需:
# 列出所有 shares
curl https://你的umami域名/api/websites/{websiteId}/shares \
-H "Authorization: Bearer $TOKEN"
# 删除旧的
curl -X DELETE https://你的umami域名/api/share/id/{shareId} \
-H "Authorization: Bearer $TOKEN"
# 重新创建
curl -X POST https://你的umami域名/api/websites/{websiteId}/shares \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"blog-stats","parameters":{"overview":true,"events":true}}'参考&致谢
- umami-software/umami: Umami is a simple, fast, privacy-focused alternative to Google Analytics.
- Umami UV / PV 统计显示
- https://umami.is/docs/websites-api
- https://umami.is/docs/authentication
- https://umami.is/docs/website-stats
雷打真孝子,财发狠心,麻绳专挑细处断,恶运专找苦命人!
——俗语赏析
系列教程
Hexo系列
[十万字图文教程]基于Hexo的matery主题搭建博客并深度优化完全一站式教程
- Hexo Docker环境与Hexo基础配置篇
- hexo博客自定义修改篇
- hexo博客网络优化篇
- hexo博客增强部署篇
- hexo博客个性定制篇
- hexo博客常见问题篇
- hexo博客博文撰写篇之完美笔记大攻略终极完全版
- Hexo Markdown以及各种插件功能测试
- markdown 各种其它语法插件,latex公式支持,mermaid图表,plant uml图表,URL卡片,bilibili卡片,github卡片,豆瓣卡片,插入音乐和视频,插入脑图,插入PDF,嵌入iframe
- 在 Hexo 博客中插入 ECharts 动态图表
- 使用nodeppt给hexo博客嵌入PPT演示
- GithubProfile美化与自动获取RSS文章教程
- Vercel部署高级用法教程
- webhook部署Hexo静态博客指南
- 在宝塔VPS上面采用docker部署waline全流程图解教程
- 自建Umami访问统计服务并统计静态博客UV/PV
Docker系列
- Docker使用简明教程
- 使用jeckett,sonarr,iyuu,qt,emby打造全自动追剧流程
- 为知笔记私有化Docker部署
- Earthly 一个更加强大的镜像构建工具
- 使用 Shell 脚本实现一个简单 Docker
- 如何使用Traefik V2 在Ubuntu20.04 上面来做 Dockers
- 通过IPV6访问Qnap NAS中Docker的服务
笔记系列
- 完美笔记进化论
- hexo博客博文撰写篇之完美笔记大攻略终极完全版
- Joplin入门指南&实践方案
- 替代Evernote免费开源笔记Joplin-网盘同步笔记历史版本Markdown可视化
- Joplin 插件以及其Markdown语法。All in One!
- Joplin 插件使用推荐
- 为知笔记私有化Docker部署
Gitbook使用系列
- GitBook+GitLab撰写发布技术文档-Part1:GitBook篇
- GitBook+GitLab撰写发布技术文档-Part2:GitLab篇
- 自己动手制作电子书的最佳方式(支持PDF、ePub、mobi等格式)

