Umami 自托管:给静态博客加访问统计的完整复盘

2026-06-23

这个博客一直没有任何访问统计——不知道哪些文章有人读、来自哪个渠道、用什么设备看的。不是不需要,而是没有找到一个合适的方案。

Google Analytics 不在考虑范围。剩下四个选项:GoatCounter、Plausible、Umami、Matomo。我花了一晚选型,又花了一晚部署。以下是整个过程——从决策到上线。

选型:四选一

四个候选都是隐私优先、自托管、无 cookie。快速对照:

GoatCounterPlausibleUmamiMatomo
语言Go 单二进制ElixirNode.jsPHP
数据库SQLitePG + ClickHousePostgreSQLMySQL
内存~30 MB>2 GB~300 MB~800 MB
GitHub5.8k ★27.3k ★37.3k ★21.6k ★
Docker🚫 无官方镜像✅ Compose✅ Compose✅ 镜像
脚本3.5 KB<1 KB2 KB22 KB
许可证EUPLAGPLv3MITGPLv3

淘汰逻辑很直接:

  • Matomo 太重,PHP + MySQL 体系,功能过剩
  • Plausible CE 社区版半年一发、砍高级功能,自托管需要 ClickHouse(2GB 起步)
  • GoatCounter 最轻量,但没有开箱的看板系统,API 偏弱

Umami 在三个维度上最合适:社区最大(37.3k stars)、MIT 许可证最开放、Docker Compose 一键部署,资源和功能之间平衡最好。

部署

架构

analytics.example.com (Caddy 反代) → 127.0.0.1:3007 (Umami)
                           → 127.0.0.1:5432 (PostgreSQL 17)

Umami 需要 PostgreSQL 12.14+。服务器上已有一个 PG 10(给 Miniflux 用),版本不够,直接起了一个新的 PostgreSQL 17 Alpine 容器。

Docker Compose

/opt/stacks/umami/docker-compose.yml

services:
  umami:
    image: docker.umami.is/umami-software/umami:postgresql-latest
    ports:
      - "127.0.0.1:3007:3000"
    environment:
      DATABASE_URL: postgresql://umami:<密码>@umami-db:5432/umami
      APP_SECRET: <随机 hex 64>
      CLIENT_IP_HEADER: X-Forwarded-For
      TRACKER_SCRIPT_NAME: u
    restart: unless-stopped
    depends_on:
      umami-db:
        condition: service_healthy

  umami-db:
    image: postgres:17-alpine
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: <密码>
    volumes:
      - ./pgdata:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami"]
      interval: 5s
      timeout: 5s
      retries: 5

三个环境变量的用意:

变量原因
CLIENT_IP_HEADERX-Forwarded-ForCaddy 反代后正确记录访客 IP,否则所有 IP 都显示 127.0.0.1
APP_SECRETopenssl rand -hex 32每个安装需要独立的密钥,用于加密认证令牌
TRACKER_SCRIPT_NAMEu追踪脚本名从默认的 script.js 改成 u,减少被广告拦截器屏蔽的概率

Caddy 反代

/etc/caddy/Caddyfile 追加:

analytics.example.com {
	encode zstd gzip
	reverse_proxy 127.0.0.1:3007
	tls lss6378@gmail.com
}

重载 Caddy 时发现一个意外:token.logfun.xyz 那段语法早就有问题——多了两个多余的 },被 caddy fmt 修掉了。之前居然能跑,可能是 Caddy 对这种情况有一定容错。

部署细节

docker-compose(注意这个系统是独立包不是 Docker 插件),起容器后默认登录 admin / umami,登录后第一时间改了密码。

第一次 docker pull 超慢,Docker Hub 在国内被墙。Docker 守护进程已配了 mihomo 代理(http://127.0.0.1:7890),但 docker-compose 运行时没有继承代理环境变量。加 HTTPS_PROXYHTTP_PROXY 后重拉很快。

端口选了 3007——3000 是 new-api、3001 是 uptime-kuma、3002 是 headplane,得避开。

配置

创建网站和看板

Umami 支持通过 REST API 完成所有 UI 操作。创建网站:

curl -s https://analytics.example.com/api/websites \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"lls_blog","domain":"blog.logfun.xyz"}'

拿到了 websiteId

看板(Boards)是 v3.1.0 的新功能——可以拖拽组件组成自定义仪表盘。API 创建看板时注意两点:

  1. websiteId 要嵌套在 parameters 对象里,不是顶层字段
  2. 更新看板用 POST /api/boards/:id(不是 PUT 或 PATCH)

看板的组件布局同样通过 parameters.rows 字段写入。每个 row 包含 columns,每个 column 有一个 component。我建了四个看板:

看板组件
概览website-metrics-bar(指标栏)+ website-chart(趋势图)+ metrics-table(页面浏览)
热门文章metrics-table × 2(页面访问排行、页面标题排行)
来源metrics-table × 2(来源域名、UTM 来源)
访客world-map + metrics-table × 3(国家/地区、浏览器、设备)

可用组件类型:

组件说明
website-metrics-bar汇总指标卡片
website-chart时序折线图
metrics-table可配置维度表格
world-map地理分布图
weekly-traffic周流量热力图
events-chart自定义事件图
text-block自由文本

注入追踪脚本

Zola 的 serene 主题通过 templates/_head_extend.html 注入 JS。在文件末尾加一行:

<script defer src="https://analytics.example.com/u" data-website-id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></script>

注意这里是 /u 不是 /u.js——TRACKER_SCRIPT_NAME: u 会让 Umami 把追踪脚本挂在 https://analytics.example.com/u,但如果请求 u.js 会 404。源码里 TRACKER_SCRIPT_NAME 的值和请求路径直接对应,不加后缀名。

defer 属性保证脚本不阻塞页面渲染。

踩过的坑

几个卡住超过五分钟的问题:

  1. Docker pull 超时docker-compose 不继承守护进程的代理环变,需要显式传 HTTPS_PROXY。或者直接用 docker compose 插件版会好一些。

  2. 端口冲突:选端口前先用 ss -tlnp 扫一遍,别猜。

  3. API 方法搞错:看板更新是 POST 不是 PUT/PATCH,Next.js 路由里 POST handler 同时处理创建和更新。第一次用 PUT 试了几次才发现。

  4. Prisma 报 description 缺失:创建看板时 description 字段在 API 里是可选的,但 Prisma 校验时会报缺失。填上内容就过。

  5. Caddyfile 格式问题caddy validate 在重载前一定要跑,能发现隐藏的语法问题。

效果

部署完成后,analytics.example.com 访问看板即可查看所有统计数据。追踪脚本 2KB,defer 加载不阻塞页面,不影响 Lighthouse 评分。数据存在自己的 PG 里,不经第三方。

整个流程涉及的工具层:

选型 → Docker → Caddy → API → Zola 模板

每一步都有现成的工具和文档,唯一花时间的是弄清楚 Umami 的 API 行为和那些不起眼的参数细节。

参考

https://blog.logfun.xyz/blog/feed.xml