这是「Docker 好项目」系列的第 6 期。每期挑一个真正能在自己服务器上跑起来、并且能解决具体问题的项目,给出能直接复制的配置。

博客写了几十篇,你大概知道哪篇被看得最多——但你真的知道吗?还是只靠评论数猜?

多数人的第一反应是装 Google Analytics。但对自建博客来说它有三个麻烦:国内访问 analytics 脚本经常加载失败、要挂 cookie 同意横幅、数据存在别人的服务器上。

Umami 换了个思路:一个自建的网站统计,不用 cookie、不收集个人数据、跟踪脚本只有 2KB,装在你自己服务器上,数据在你自己库里。MIT 协议,开源免费。

Umami 统计面板

它到底能干什么

  • 基础指标:访问量、访客数、页面浏览量、跳出率、平均停留时长
  • 来源分析:来源域名、来源链接、UTM 参数
  • 访问明细:页面、入口页、出口页、国家/地区、浏览器、操作系统、设备类型
  • 实时数据:最近 30 分钟的在线访客
  • 自定义事件:追踪按钮点击、表单提交、下载等任意行为
  • 多站点 + 团队:一个面板管多个网站,可以开子账号并限定只能看某个站
  • 保留数据:支持设置数据保留期,自动清理老数据

它不做的是:不做热图、不做会话录屏、不做漏斗转化分析。Umami 的定位是干净、轻、够用的访问统计,不是行为分析平台。

部署前准备

  • 内存:Umami 本身约 230MB + PostgreSQL 约 155MB,准备 1GB 空闲内存比较稳妥
  • 数据库:只支持 PostgreSQL(v3 起已经不支持 MySQL,网上老教程里的 MySQL 版配置直接跳过)
  • 域名:可选。只在本机看的话不用域名,不过建议配一个,方便随时查数据
  • 端口:默认 3000

docker-compose 配置

mkdir -p /opt/umami && cd /opt/umami

先生成两个密钥,别用示例里的占位符:

echo "数据库密码:$(openssl rand -hex 16)"
echo "APP_SECRET:$(openssl rand -hex 32)"

把输出记下来,填进下面的文件。新建 docker-compose.yml:

services:
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    container_name: umami
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      DATABASE_URL: postgresql://umami:这里换成数据库密码@umami-db:5432/umami
      APP_SECRET: 这里换成APP_SECRET
      DISABLE_TELEMETRY: "1"
    depends_on:
      umami-db:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/api/heartbeat || exit 1"]
      interval: 5s
      timeout: 5s
      retries: 5

  umami-db:
    image: postgres:16-alpine
    container_name: umami-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: 这里换成数据库密码
    volumes:
      - ./pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami"]
      interval: 5s
      timeout: 5s
      retries: 5

启动:

docker compose up -d
docker compose logs -f umami

第一次启动会自动跑数据库迁移,日志里出现 Ready 就成了。数据库没就绪时 Umami 会退出重启,这是正常的,等十几秒再看。

每个配置项为什么这么写

配置 说明
postgresql-latest 官方维护的 PostgreSQL 版滚动 tag。想要版本可复现就换成具体版本(当前 ghcr 上可见的最新为 postgresql-v2.16)。不要用不带后缀的裸 tag,那是 MySQL 版
DATABASE_URL 里的密码必须和下面 POSTGRES_PASSWORD 完全一致,两处不一致是最常见的启动失败原因
APP_SECRET 用来给登录态签名,至少 32 位随机字符。泄露等于别人能伪造你的登录会话;随便改一次会强制所有人重新登录
127.0.0.1:3000:3000 统计面板能看到你所有网站的流量,不该直接暴露公网。绑本机后用反代对外
DISABLE_TELEMETRY 关掉匿名使用数据上报。自建的意义就是数据不出门,这个顺手关掉
./pgdata:/var/lib/postgresql/data 所有统计数据在这里,这是唯一要备份的东西
depends_on: service_healthy 确保数据库真正就绪再启动 Umami,避免启动即崩溃的死循环
healthcheck /api/heartbeat 是官方健康检查接口,返回 {"ok":true} 即正常

首次登录

浏览器打开 http://服务器IP:3000(本机则用 http://localhost:3000)。

默认账号:

  • 用户名:admin
  • 密码:umami

登录后立刻去 Settings → Profile 改密码。这套默认凭据是公开写进文档的,而统计面板能看出你所有站点的流量结构。

添加网站,拿到跟踪代码

Settings → Websites → Add website:

字段 填什么
Name 起个名字,比如「个人博客」
Domain www.你的域名(多个域名用逗号分隔)

保存后会生成一段跟踪代码,形如:

<script defer src="https://stat.你的域名/script.js" data-website-id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></script>

data-website-id 是每个网站唯一的标识,不要复制错——贴错 ID 的表现是:脚本正常加载、但后台一条数据都没有。

接进 Halo 博客

如果你也用 Halo,最省事的做法是后台 → 设置 → 代码注入 → 页脚 HTML,把上面那段 script 粘进去保存。

刷新博客前台,然后打开 Umami 的实时面板,应该能看到自己这一条访问记录。看到就通了。

用了 Cloudflare 的话,注意别把 /script.js 和 /api/send 两个路径拦掉,那会导致「脚本 200 但数据为零」。

避开广告拦截器

主流广告拦截规则默认就会屏蔽 script.js 和 umami 这个路径。你的访客装了 uBlock,他这次访问就统计不到。改两个名字就能大部分绕开:

environment:
  TRACKER_SCRIPT_NAME: "stats"
  COLLECT_API_ENDPOINT: "/api/collect"

重启后跟踪代码变成:

<script defer src="https://stat.你的域名/stats" data-website-id="你的ID"></script>

追踪自定义事件

想知道「有多少人点了下载按钮」「有多少人提交了表单」,可以在按钮上加一个属性,什么都不用写:

<button data-umami-event="download-resume">下载简历</button>

也可以带上下文数据:

<button data-umami-event="buy" data-umami-event-plan="pro">升级专业版</button>

需要更复杂的判断就用 JS 调用:

umami.track('signup', { plan: 'pro', source: 'homepage' });

事件会出现在后台的 Events 页面,可以按事件名和属性值看。

套上域名与 HTTPS

Umami 本身不处理证书,用第 2 期讲过的 Nginx Proxy Manager 反代即可:新建 Proxy Host,域名填 stat.你的域名,Forward 指向 127.0.0.1:3000,勾上 SSL 一键签证书。

用 1Panel 的话更直接:网站 → 反向代理 → 新建,代理到 127.0.0.1:3000。

唯一要注意的是 WebSocket——实时面板靠它刷新,反代时记得开启。

备份

统计数据全在 PostgreSQL 里,用 pg_dump 导出最稳:

cd /opt/umami
docker compose exec -T umami-db pg_dump -U umami umami | gzip > umami-backup-$(date +%F).sql.gz

恢复:

gunzip -c umami-backup-2026-09-19.sql.gz | docker compose exec -T umami-db psql -U umami -d umami

挂到 1Panel 的计划任务或 crontab,每天凌晨跑一次,只保留最近 14 份。

踩坑清单

  • 容器一直重启:先看 docker compose logs umami,九成是 DATABASE_URL 里的密码和 POSTGRES_PASSWORD 不一致
  • 改了 APP_SECRET 后全部登出:属于正常行为,重新登录即可
  • 后台一条数据都没有:依次查——跟踪代码的 data-website-id 对不对、浏览器控制台有没有报错、Cloudflare 有没有拦 /script.js
  • 访客数据明显偏低:大概率是广告拦截器,按上面改名即可
  • 从 v2 升 v3 前先备份:v3 有破坏性变更且不再支持 MySQL,务必 pg_dump 之后再动
  • 别把 3000 端口直接开到公网:统计面板等于你网站的流量地图,绑本机 + 反代

小结

Umami 是那种「装上就不用管」的基础设施:2KB 脚本不影响博客加载速度,没有 cookie 就不用挂同意横幅,数据在自己库里想导就导。

适合:有自建博客或小站、想要干净访问数据、不想要 cookie 弹窗的个人站长。 不适合:需要热图、录屏、漏斗归因的运营分析场景——那是另一类工具(Matomo、Plausible 或商业 SaaS)的领域。

下期继续聊另一个 Docker 项目。


项目地址:https://github.com/umami-software/umami 官方文档:https://umami.is/docs