Plost Self-hosted

Plost 自托管版文档

Plost 自托管版是一个 Go 程序,网页界面、字体和埋点脚本都打包在这一个文件里,数据存在本机的 SQLite 中。运行它不需要 Node、Docker 或单独的数据库。一个实例只有一个管理员,可以管理多个网站。

本文档中的 stats.example.com 代表你的统计服务域名,example.com 代表被统计的网站,请换成你自己的。

目录

1. 安装与启动

1.1 获取程序

获取方式见项目仓库的 README。程序只有一个文件,服务器上不用装 Go、Node 或数据库。拿到后放到 /usr/local/bin/plost,并加上执行权限:

sudo install -m 0755 plost /usr/local/bin/plost
plost -version

1.2 启动参数

最常用的三个参数:

参数默认值作用
-listen127.0.0.1:3101监听地址。放在反向代理后面时保持只监听本机。
-data./data数据目录,保存数据库 plost.db、密钥文件 secrets.key 和 backups/。
-origin空统计服务的公网地址,例如 https://stats.example.com。设置后登录 Cookie 只走 HTTPS,Passkey 也绑定到这个域名。生产环境必须设置。

本机试用:

./plost -listen 127.0.0.1:3101 -data ./data

然后打开 http://localhost:3101(建议用 localhost 而不是 127.0.0.1,否则无法试用 Passkey)。

生产环境:

plost -listen 127.0.0.1:3101 -data /var/lib/plost -origin https://stats.example.com

其他参数(-trusted-proxies、-client-ip-header、-geoip-db、-allow-outbound、-memory)在用到的章节里说明。运行 plost -h 可以看到完整列表。

1.3 用 systemd 常驻运行

先建一个专用系统用户:

sudo useradd --system --home /var/lib/plost --shell /usr/sbin/nologin plost

把下面的内容保存为 /etc/systemd/system/plost.service,把 -origin 改成你的域名:

[Unit]
Description=Plost personal website analytics
After=network.target

[Service]
Type=simple
User=plost
Group=plost
StateDirectory=plost
StateDirectoryMode=0700
ExecStart=/usr/local/bin/plost -listen 127.0.0.1:3101 -data /var/lib/plost -origin https://stats.example.com
Restart=on-failure
RestartSec=3
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/plost
TimeoutStopSec=20

[Install]
WantedBy=multi-user.target

启动并设为开机自启:

sudo systemctl daemon-reload
sudo systemctl enable --now plost
systemctl status plost --no-pager

确认服务在运行:

curl -s http://127.0.0.1:3101/api/health

返回 {"ok":true,...} 即正常。

这个示例开启了 ProtectSystem=strict,程序只能写 /var/lib/plost。如果你把 GeoIP 库放在别处,它只需要可读;如果改了数据目录,记得同时改 ReadWritePaths。

1.4 首次创建管理员

服务第一次启动时数据库是空的,打开 https://stats.example.com 会进入初始化页面,创建唯一的实例管理员:

  • 密码至少 8 个字符,不限制字符类型,纯数字也可以(最长 72 字节)。
  • 创建完成后公开注册永久关闭,之后打开首页只会看到登录页。
  • 初始化之前,任何能打开这个地址的人都能抢先创建管理员。请在配好 HTTPS 后立刻完成初始化,不要让空实例长时间暴露在公网上。

登录后点「添加网站」,填入要统计的网站域名,就可以拿到埋点代码(见第 3 节)。

2. 放在反向代理或 CDN 后面

2.1 访客 IP 和国家:默认自动识别

大多数情况下什么都不用配置。 不论域名直接解析到服务器,还是经过本机的 Caddy、Nginx、Docker,或者 Cloudflare、阿里云 CDN 等,Plost 都会像 Umami 一样自己找出访客的地址和国家。

它按下面的顺序读取,用到第一个可用的:

  1. CDN 给出的单一地址头:CF-Connecting-IP、True-Client-IP、Ali-CDN-Real-IP、EO-Connecting-IP、Fastly-Client-IP。
  2. X-Forwarded-For 中的第一个公网地址(后面的是 CDN 节点和代理)。
  3. RFC 7239 Forwarded,以及 X-Real-IP。
  4. 都没有时,使用连接方的地址。

国家优先使用 CDN 的 CF-IPCountry 或 CloudFront-Viewer-Country,没有时查本机的 GeoIP 库(见 2.4)。

关于安全:

  • 访客可以伪造这些请求头,但只会影响他自己那几条记录的去重和国家。
  • 登录、恢复等频率限制不读这些头,始终按真实的连接方计算,伪造地址绕不过限制。
  • 原始 IP 不会写入数据库。去重用的是「站点 + IP + 浏览器标识 + 月份」生成的不可逆摘要,换月即变。

如果本机反代既没转发地址、也没有任何上面的头,Plost 会改按浏览器标识计数,不会把所有访客合并成一个人。

2.2 可选:严格模式

如果你不想相信任何人都能写的请求头,可以声明前面有哪些代理和 CDN。声明之后,只有来自这些地址的请求,其中的地址头才会被采用,X-Forwarded-For 也改为从右往左读、跳过受信任的地址。

参数说明
-trusted-proxies逗号分隔。可写预设名 loopback(本机)、private(内网和 Docker)、cloudflare(内置 Cloudflare 公布的网段),也可写网段、单个地址,或 @/路径/文件(每行一个,# 后为注释)。不接受 0.0.0.0/0。
-client-ip-header指定受信任 CDN 使用的单一地址头,例如 CF-Connecting-IP、Ali-CDN-Real-IP。只在请求确实经过受信任的 CDN 时采用;不写时读 X-Forwarded-For。
-trust-proxy旧参数,等于 -trusted-proxies loopback,继续可用。

例子:Caddy 在本机,前面再套 Cloudflare:

plost -listen 127.0.0.1:3101 -data /var/lib/plost -origin https://stats.example.com \
  -trusted-proxies loopback,cloudflare -client-ip-header CF-Connecting-IP

开了严格模式,就要把链路上每一层都列进去。漏掉一层(例如 Plost 跑在 Docker 里却只写了 loopback),请求会被当成来自不受信任的一方,访客地址和 CDN 国家头都不会被采用。拿不准时不要开,默认模式已经够用。

2.3 反向代理最小配置

Caddy(自动申请 HTTPS 证书,并会自己设置 X-Forwarded-For):

stats.example.com {
    encode zstd gzip
    reverse_proxy 127.0.0.1:3101
}

Nginx(证书部分按你自己的方式配置):

server {
    listen 443 ssl;
    http2 on;
    server_name stats.example.com;

    ssl_certificate     /etc/ssl/stats.example.com/fullchain.pem;
    ssl_certificate_key /etc/ssl/stats.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3101;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

注意 X-Forwarded-For 要用 $proxy_add_x_forwarded_for(在原有值后追加),不要写成 $remote_addr。前面有 CDN 时,写成 $remote_addr 会把 CDN 传来的访客地址覆盖成 CDN 节点地址。

整站转发 / 即可。如果你只想转发部分路径,至少要包含:/script.js、/api/send、/e、/api/event,以及后台用到的全部页面和接口,最简单的做法仍然是整站转发。

2.4 本机 GeoIP 库

用了 Cloudflare 或 CloudFront 时,国家直接来自 CDN,不需要这一步。域名直接解析到服务器、或用的 CDN 不提供国家头时,可以指定一个 MaxMind 格式的数据库:

plost ... -geoip-db /var/lib/plost/GeoLite2-Country.mmdb
  • 支持 GeoLite2 Country / City、DB-IP Country / City,只读取国家代码。
  • 库文件需要自己下载并定期更新,程序不会下载,也不调用任何外部 IP 查询服务。
  • 程序每分钟检查一次文件,替换后自动换用,不用重启。更新时请「写好新文件再改名覆盖」,不要在原文件上直接改写。
  • 启动时指定的库打不开,程序会拒绝启动。
  • 只能识别配置之后的新访问。已经存下的记录不会补查国家,因为原始 IP 从不保存。

目前只统计到国家,不采集省份和城市。

3. 安装埋点脚本

3.1 代码

在被统计网站每个页面的 <head> 里加入一行(单页应用只需在公共模板加一次):

<script defer data-domain="example.com" src="https://stats.example.com/script.js"></script>

后台的网站设置里会生成带你实际地址的代码,直接复制即可。

3.2 data-domain 必须等于站点域名

data-domain 要和你在 Plost 后台添加的站点域名一致,同时页面实际所在的域名也要和它一致,否则服务端会静默丢弃这条访问(请求照样返回 202,浏览器里看不出报错)。

  • www. 前缀和大小写会被忽略:www.example.com 和 example.com 视为同一个站点。
  • 其他子域名不算同一个站点:blog.example.com 需要单独添加。
  • 在本机预览(localhost)打开的页面,不会计入线上站点。

3.3 需要一起转发的地址

脚本的工作方式和 Umami 的追踪脚本相同:

地址用途
/script.js埋点脚本本身(/js/script.js、/tracker.js 为兼容别名)
/api/send正常上报,JSON POST
/e正常请求被拦截插件或网络拒绝时的重发通道(先图片请求,再 Beacon),服务端按事件编号去重,不会重复计数
/api/event旧版脚本的上报地址,仍然支持

用了反向代理、CDN 或防火墙规则时,这四个地址都要能到达 Plost。只放行 /api/send 会让被拦截的那部分访问救不回来。

3.4 哪些访问不会被计数

和 Umami 一致:

  • 页面完全加载完(document.readyState 变成 complete)才计第一次浏览,没等页面打开就关掉的不算。
  • 自动化浏览器(navigator.webdriver 为真,例如 Selenium、Puppeteer、Playwright)不发送任何事件。
  • 自报为爬虫、预览、监控或脚本工具的请求,以及没有浏览器标识的请求,会被服务端丢弃。

所以用 curl 或无界面浏览器去「测一下」是看不到数据的,请用真实浏览器打开网站验证。

3.5 自定义事件

window.plost?.track("Download guide", { location: "article" })

外链点击和文件下载会自动记录。事件属性只能是简单值,不要放邮箱、手机号等个人信息。

4. 访客地图

在「设置 → 系统设置 → 访客地图」里选择地图服务。

选项需要说明
内置地图无默认。Mapbox 和 Google 地图都关闭时使用,Token / Key 加载失败时也会自动回到它。
Mapbox公开 Token以 pk. 开头。
Google 地图浏览器 API Key以 AIza 开头。
  • 两种外部地图最多开一个,打开一个会自动关掉另一个;可以都关。
  • 开关点一下就保存,重新打开统计页后生效。

Mapbox Token 的设置:

  1. 在 Mapbox 账号里创建一个公开 Token(pk. 开头),不要填 sk. 开头的私密密钥。
  2. 在 URL 限制里只填你的统计服务域名,例如 https://stats.example.com。
  3. 权限勾选 styles:read 和 fonts:read。

Google 地图 Key 的设置:

  1. 在 Google Cloud 控制台创建 API Key(AIza 开头)。
  2. 应用限制选「HTTP 引荐来源(网站)」,填 https://stats.example.com/*。
  3. API 限制只勾选 Maps JavaScript API。

这两个凭证都会出现在浏览器里,任何能打开统计页的人都看得到,所以一定要做域名限制。

5. 备份与恢复

所有备份操作都在后台的「设置 → 系统设置 → 备份与恢复」中完成,不需要登录服务器执行命令。

5.1 手动完整备份

输入当前账号密码和一个备份密码,下载 .plostbk 文件。

  • 备份密码至少 12 个字符,与登录密码无关。
  • 备份密码不保存、不能找回。忘记它,这份备份就再也打不开。请和备份文件分开保管。
  • 文件经过加密(scrypt + AES-256-GCM),被篡改或截断都会在恢复时被拒绝。
  • 备份是数据库的一致性快照,包含账号、站点、统计数据、Passkey 和站点检查记录。

5.2 计划备份与 R2 / S3

在「自动备份」里选择每小时、每天或每周,并设置计划备份密码:

  • 备份文件格式与手动备份相同,保存在数据目录的 backups/scheduled/,只保留最新的若干份。保留份数设为 0 时只留云端副本。
  • 可以选择上传到 Cloudflare R2 或任何 Amazon S3 兼容存储。「测试连接」只验证能否列出文件,上传权限要等第一次备份时才能确认。
  • 云端旧备份不会被自动删除,请在存储桶里设置生命周期规则。
  • 为了无人值守,服务器需要保存计划备份密码和存储密钥,它们用数据目录里的 secrets.key 加密。迁移服务器时请把整个数据目录连同 secrets.key 一起带走;没有它,已保存的凭据需要重新填写,但不影响服务运行。
  • 服务器时区决定执行时间;停机期间错过的那次会在恢复后补跑一次,失败后至少隔 15 分钟重试。
  • 数据库超过 1 GiB 时,计划备份会明确报错,而不是生成一个恢复不了的文件。

5.3 恢复

可以上传 .plostbk 文件,或从本地、云端的备份列表中选择。恢复需要:当前账号密码、创建这份备份时的备份密码,以及输入 RESTORE 确认。

恢复过程:

  1. 校验文件完整性、数据库版本和表结构。
  2. 先把当前数据库保存为 backups/before-restore-*.db,再切换到新库。切换失败会尝试回滚到这份快照。
  3. 成功后所有登录会话失效,请用备份里的账号重新登录。

before-restore-*.db 是未加密的数据库文件,请像对待生产数据一样保护或在确认无误后自行清理。

旧版 PostgreSQL + ClickHouse 的 .lsb 备份不能恢复到这个版本。从 Umami 搬迁历史数据请使用随附的 plost-import-umami 工具。

6. 统计 API

需要在自己的程序、博客侧栏或看板里显示访问数字时,可以调用只读的统计 API。它只返回聚合数字,不返回账号、IP 或访客明细,也不能写入任何数据。

6.1 申请 API Key

  1. 登录后台,打开「设置 → API」,选择要读取的站点,名称写清用途,例如「博客侧栏」,点「申请 API Key」。
  2. 页面会显示一串以 plst_ 开头的 API Key,只显示这一次,请当场复制保存。之后列表里只显示末尾 4 位、创建时间和最近使用时间。
  3. 不再需要时在同一页删除这个 Key,调用随即失效,不用改账号密码。删除站点时,它的 Key 也一起删除。

API Key 只读,每个 Key 只对应一个站点,不能打开后台或分享报表。请只把它放在服务端的环境变量里,不要写进网页 JavaScript、公开仓库或网址参数。

更早的版本没有「设置 → API」,可以在「站点设置 → 可见性 → 分享链接」为这个用途单独建一条分享链接,取 /share/ 后面的最后一段当作凭证,用法和 API Key 相同。这种凭证同时能打开分享报表,升级后建议换成 API Key;旧凭证仍然有效。

6.2 请求

GET /api/v1/stats
Authorization: Bearer <API Key>
参数说明
period可选。summary(默认)一次返回在线人数、今天和昨天;live 只返回在线人数;today、yesterday 返回当天的访客、访问次数和浏览量。
domain可选。必须等于 API Key 所属站点的域名,省略时直接读取该站点。

只接受这两个参数,每个只能出现一次,API Key 不能放在网址里。

用 curl 调用:

export PLOST_TOKEN='<API Key>'
curl --fail-with-body 'https://stats.example.com/api/v1/stats?period=summary' \
  -H "Authorization: Bearer $PLOST_TOKEN"

用 JavaScript(Node.js 18 及以上)调用:

const response = await fetch('https://stats.example.com/api/v1/stats?period=summary', {
  headers: { Authorization: `Bearer ${process.env.PLOST_TOKEN}` },
})
if (!response.ok) throw new Error(`Plost API ${response.status}`)
const stats = await response.json()
console.log(stats.data)

用 Python 调用:

import os, requests

response = requests.get(
    'https://stats.example.com/api/v1/stats?period=summary',
    headers={'Authorization': 'Bearer ' + os.environ['PLOST_TOKEN']},
    timeout=15,
)
response.raise_for_status()
print(response.json()['data'])

6.3 响应

下面是 summary 的示例数据,不是真实站点的统计:

{
  "apiVersion": "1",
  "site": { "domain": "example.com", "timezone": "Asia/Shanghai" },
  "period": "summary",
  "generatedAt": "2026-11-03T08:00:00Z",
  "onlineWindowSeconds": 300,
  "data": {
    "online": 6,
    "today": { "date": "2026-11-03", "visitors": 120, "visits": 145, "pageviews": 320 },
    "yesterday": { "date": "2026-11-02", "visitors": 210, "visits": 260, "pageviews": 580 }
  }
}
  • online:近 5 分钟(onlineWindowSeconds)内有活动的访客数。
  • visitors:访客数,和 Umami 算法一致,按 IP 地址、浏览器标识和月份去重,同一个人换了 IP 或代理会重新计数。
  • visits:访问次数。同一访客闲置 30 分钟后算新的一次访问,刷新页面只增加浏览量。
  • pageviews:浏览量,只计页面浏览事件。
  • today、yesterday 按站点设置的时区划分自然日,不是最近 24 小时;generatedAt 是生成响应的 UTC 时间。
  • 今天的数字还在增长。不要把多天的访客数相加当作这几天的去重人数。

6.4 错误与调用约定

出错时返回 {"error":"…"}:

状态码含义
400参数不对,或出现了重复参数。
401没带 API Key、格式不对,或 Key 已被删除。
403domain 与 API Key 所属站点不一致。
429请求太频繁。每个 Key 每分钟最多 60 次,按响应头 Retry-After 等待后再试。
503服务暂时不可用。请稍后重试,不要当作访问量为 0。
  • 建议每 30 到 60 秒查询一次,并在自己的后端缓存结果。
  • 接口不支持浏览器带凭证跨域调用。网页上要显示数字,请经由自己的后端转发。
  • v1 的现有字段会保持兼容,客户端应忽略以后新增的字段。
  • 接口只返回聚合数字,不返回账号、IP 或访客明细,也不能写入数据。
  • 实例的 /api/v1/openapi.json 是机器可读的 OpenAPI 规范。让 AI 编程助手写调用代码时,把这个规范给它就够了,不需要也不应该把真实 API Key 发给 AI。

6.5 导出报表

需要整份数据时,登录后台,在站点统计页工具栏的「⋮」菜单里选「导出统计」,会下载当前时间范围的报表压缩包,里面是访客趋势、来源、页面、事件、设备、国家等多个 CSV 文件。导出需要登录,API Key 和分享凭证都不能导出。

7. 常见问题与排查

每个问题都给出:现象、常见原因、可以直接运行的检查命令,以及一段可以复制给 AI 助手(ChatGPT、Claude 等)的提示词。

把输出贴给任何人或 AI 之前,请先检查并替换: 你的真实域名、服务器 IP、访客 IP、Token / API Key、密码、备份密码、Cookie。可以统一替换成 stats.example.com、example.com、203.0.113.10、<已隐藏>。

下面的命令假设你按第 1 节用 systemd 运行、服务名为 plost、数据目录为 /var/lib/plost,请按实际情况调整。

一键收集基本信息

很多问题先跑这一段就能看出端倪:

plost -version; systemctl status plost --no-pager | head -n 15; systemctl cat plost | grep ExecStart; curl -s http://127.0.0.1:3101/api/health; echo; curl -sI https://stats.example.com/script.js | head -n 12; journalctl -u plost -n 50 --no-pager

7.1 统计不到数据

现象:网站已经加了脚本,后台一直是 0,或近期活动里没有记录。

常见原因

  • data-domain 和后台站点域名不一致,或页面实际所在的子域名不一致(服务端会静默丢弃,见 3.2)。
  • script.js 加载失败:地址写错、HTTPS 证书问题、被网站的内容安全策略(CSP)拦截。
  • 反向代理或防火墙没有放行 /api/send、/e、/api/event。
  • 用 curl、无界面浏览器或自动化工具测试(会被当作机器人丢弃)。
  • 浏览器装了拦截插件。换一个无插件的浏览器窗口试。
  • Plost 服务没有运行。

检查命令

curl -sI https://stats.example.com/script.js | head -n 5

应返回 200,content-type 为 JavaScript。

curl -s -o /dev/null -w '%{http_code}\n' -X OPTIONS https://stats.example.com/api/send -H 'Origin: https://example.com' -H 'Access-Control-Request-Method: POST'

应返回 204。返回 404、403 或 5xx,说明这个地址没有转发到 Plost。把 /api/send 换成 /e、/api/event 各试一次。

curl -s https://example.com/ | grep -o '<script[^>]*data-domain[^>]*>'

核对页面里实际输出的 data-domain 和 src。

浏览器检查:打开网站,按 F12 → 「网络」,筛选 send,刷新页面。正常应看到一条 api/send 请求,状态 202。看到红色的「已拦截」或 CSP 报错,就是浏览器侧的问题。

给 AI 的提示词

我在自己服务器上自托管了网站统计工具 Plost(Go 单程序 + SQLite,追踪方式和 Umami 一样:页面加载 /script.js,再向 /api/send 发 JSON POST,失败时改发到 /e)。网站已经加了埋点代码,但后台一直没有数据。

已知规则:埋点代码的 data-domain 必须等于后台添加的站点域名,页面所在域名也要相同(www. 前缀会被忽略,其他子域名不算同一站点),不一致时服务端返回 202 但不保存。curl 和无界面浏览器会被当成机器人丢弃。

我的部署方式:<例如:Ubuntu + systemd + Nginx,前面有/没有 Cloudflare>
后台添加的站点域名:<例如 example.com>
网页里的埋点代码:<粘贴>
下面是检查命令的输出(已隐藏真实域名和 IP):
<粘贴>
浏览器开发者工具「网络」里 api/send 请求的状态:<粘贴,或写“没有看到这条请求”>

请判断最可能的原因,按可能性排序,并给出具体的修改步骤(需要改配置文件时给出修改后的片段)。

7.2 访客全是同一个人,或全是同一个 IP

现象:访客数远低于浏览量,或近期活动里的访客看起来都一样;国家也全是服务器或 CDN 所在的国家。

常见原因

  • Nginx 写了 proxy_set_header X-Forwarded-For $remote_addr;,把 CDN 传来的真实地址覆盖成了 CDN 节点地址。应改为 $proxy_add_x_forwarded_for。
  • 中间某一层代理把访客地址替换成了自己的地址,且没有提供任何地址头。
  • 开了严格模式(-trusted-proxies 或 -trust-proxy),但没有把链路上的每一层都列进去,例如 Plost 在 Docker 里却只写了 loopback,或前面有 Cloudflare 却没写 cloudflare。不确定时,去掉这些参数恢复默认的自动识别。
  • 你只是在用自己的几台设备测试。访客按「IP + 浏览器 + 月份」去重,这是正常的。

检查命令

systemctl cat plost | grep -o -e '-trust-proxy[^ ]*' -e '-trusted-proxies [^ ]*' -e '-client-ip-header [^ ]*'

没有输出,说明是默认的自动识别。

sudo nginx -T 2>/dev/null | grep -n -i -e 'x-forwarded-for' -e 'x-real-ip' -e 'real_ip' -e 'proxy_pass'

(使用 Caddy 的话,Caddy 默认会正确设置 X-Forwarded-For,通常不用改。)

查看实际到达 Plost 的转发头(需要 tcpdump,按 Ctrl+C 结束;输出里有访客 IP,贴出去之前请替换):

sudo tcpdump -i lo -A -s 0 -c 20 'tcp dst port 3101' 2>/dev/null | grep -i -E '^(x-forwarded-for|x-real-ip|forwarded|cf-connecting-ip|true-client-ip|ali-cdn-real-ip|eo-connecting-ip|fastly-client-ip):'

在执行期间用手机流量(不连 Wi-Fi)打开一次网站。如果看到的 X-Forwarded-For 第一个地址就是 CDN 节点或服务器自己的地址,问题在代理配置。

给 AI 的提示词

我自托管的网站统计工具 Plost 把所有访客算成同一个人(或同一个 IP)。

Plost 识别访客地址的规则:默认不需要配置,依次读 CF-Connecting-IP、True-Client-IP、Ali-CDN-Real-IP、EO-Connecting-IP、Fastly-Client-IP,再读 X-Forwarded-For 中第一个公网地址、Forwarded、X-Real-IP,最后用连接方地址。如果启动参数里有 -trusted-proxies 或 -trust-proxy,则只相信来自所列代理的请求头,X-Forwarded-For 从右往左读。

我的链路:<例如:访客 → 阿里云 CDN → Nginx(本机)→ Plost(127.0.0.1:3101)>
Plost 的启动命令(ExecStart):<粘贴>
反向代理配置中相关的行:<粘贴>
tcpdump 看到的转发头(已把真实 IP 替换成 203.0.113.x 这类占位,但保留哪个是访客、哪个是 CDN/服务器):<粘贴>

请告诉我是哪一层把访客地址弄丢了,应该怎么改(给出修改后的配置片段),以及是否应该去掉或调整 -trusted-proxies。

7.3 国家全部显示「未知」

现象:国家和地区报表、访客地图全部是未知或空白。

常见原因

  • 没有经过 Cloudflare / CloudFront(没有国家头),也没有用 -geoip-db 指定 GeoIP 库。
  • 用了 Cloudflare,但在 Cloudflare 里关闭了「IP 地理位置」(不再发送 CF-IPCountry)。
  • 开了严格模式,但请求不被视为来自受信任的 CDN,国家头不被采用。
  • 拿不到访客 IP(见 7.2),这时也不会查 GeoIP 库。
  • 配置 GeoIP 之前的旧记录不会补查,只有新访问会有国家。

检查命令

curl -sI https://stats.example.com/api/health | grep -i -E '^(server|cf-ray|via|x-cache):'

有 cf-ray 说明经过 Cloudflare。

systemctl cat plost | grep -o -e '-geoip-db [^ ]*'
sudo -u plost ls -l /var/lib/plost/GeoLite2-Country.mmdb

(把路径换成你实际的库文件。确认 plost 用户能读取,且文件不是 0 字节。)

journalctl -u plost -n 200 --no-pager | grep -i geoip

给 AI 的提示词

我自托管的网站统计工具 Plost 里,所有访客的国家都是「未知」。

Plost 的国家来源:优先用 CDN 的 CF-IPCountry 或 CloudFront-Viewer-Country 请求头;没有时,用启动参数 -geoip-db 指定的 MaxMind 格式库(GeoLite2 / DB-IP 的 Country 或 City)在本机查询;拿不到访客 IP 时不查。开了 -trusted-proxies 时,只有经过受信任 CDN 的请求才采用国家头。程序不会自己下载库,也不调用外部 IP 查询服务。

我的链路:<例如:访客 → 直接解析到服务器 → Caddy → Plost>
Plost 的启动命令(ExecStart):<粘贴>
下面是检查命令的输出(已隐藏真实域名和 IP):
<粘贴>

请判断原因,并告诉我:需要下载哪个库、放在哪里、怎么设置 systemd 让 plost 用户可以读取,以及怎样定期安全地更新它(写新文件再改名覆盖)。

7.4 访客地图不显示

现象:地图区域空白、只显示灰色底图,或一直显示内置地图而不是你选的 Mapbox / Google 地图。

常见原因

  • 刚改完设置没有重新打开统计页(设置在重新打开后生效)。
  • Mapbox 填了 sk. 私密密钥,或 Token 没有 styles:read、fonts:read 权限。
  • Token / Key 的域名限制里没有包含你的统计服务域名(注意要写 https://stats.example.com,Google 写 https://stats.example.com/*)。
  • Google Cloud 项目没有启用 Maps JavaScript API、没有绑定结算账号,或 API 限制里没有勾选它。
  • 浏览器拦截插件拦掉了地图服务的请求。
  • 加载失败时 Plost 会自动回到内置地图;如果内置地图上也没有颜色,多半是国家都为未知(见 7.3)。

检查方法

在统计页按 F12 → 「控制台」,刷新页面,查看是否有包含 mapbox 或 google 的红色报错。常见的有:

  • 401 / 403:Token 或 Key 无效、权限不够,或域名限制不匹配。
  • RefererNotAllowedMapError:Google Key 的 HTTP 引荐来源没有包含当前域名。
  • ApiNotActivatedMapError:没有启用 Maps JavaScript API。

给 AI 的提示词

我自托管的网站统计工具 Plost 的访客地图不显示(或者一直是内置地图)。

Plost 的地图规则:后台「访客地图」里可以选 Mapbox(填 pk. 开头的公开 Token,需要 styles:read、fonts:read 权限,并限制为统计服务域名)或 Google 地图(填 AIza 开头的浏览器 Key,限制 HTTP 引荐来源,只启用 Maps JavaScript API),最多开一个;都关闭或加载失败时显示内置地图。改动在重新打开统计页后生效。

我选的服务:<Mapbox / Google 地图>
我的统计服务域名:stats.example.com(占位)
Token / Key 的限制设置:<描述你在 Mapbox 或 Google Cloud 里填写的域名和权限,不要贴 Token / Key 本身>
浏览器控制台里的报错:<粘贴,已删除 Token / Key>

请判断原因,并一步步告诉我在 Mapbox 或 Google Cloud 控制台里应该怎么改。

7.5 Passkey 无法添加

现象:「设置 → 安全」里的 Passkey 添加按钮是灰的,或点了之后浏览器报错、没有弹出指纹 / 面容验证。

常见原因

  • 用 IP 地址打开后台(例如 http://203.0.113.10:3101)。浏览器只在 HTTPS 域名或 localhost 上允许创建 Passkey,IP 地址不行。
  • 没有使用 HTTPS,或启动参数 -origin 没设置、和你在浏览器里打开的地址不一致(例如多了 www.、端口不同、http 与 https 混用)。
  • 本机试用时用了 127.0.0.1,请改用 http://localhost:3101。
  • 更换过统计服务域名:旧域名下创建的 Passkey 不能在新域名使用,需要重新添加。
  • 当前设备或浏览器不支持 Passkey,或系统里没有设置锁屏密码 / 指纹。
  • 已经添加了 20 把(上限)。

添加和移除 Passkey 都要输入当前账号密码。

检查命令

systemctl cat plost | grep -o -e '-origin [^ ]*'

把结果和浏览器地址栏里的地址逐字对比,协议、域名、端口都要一致。

curl -sI https://stats.example.com/login | head -n 1

应返回 200,并且没有证书错误。

给 AI 的提示词

我自托管的网站统计工具 Plost 无法添加 Passkey。

Plost 的 Passkey 规则:使用 WebAuthn 可发现凭据;浏览器只在 HTTPS 域名或 localhost 上允许,IP 地址不行;Passkey 绑定到启动参数 -origin 指定的域名,浏览器地址必须与它一致;本机试用要用 http://localhost:端口 而不是 127.0.0.1。

我在浏览器地址栏里打开的地址:https://stats.example.com(占位,请说明是否带端口或 www.)
Plost 的 -origin 参数:<粘贴,已替换真实域名但保留格式>
浏览器和系统:<例如:Chrome 140 / macOS>
点击添加后看到的提示或控制台报错:<粘贴>
反向代理类型:<Caddy / Nginx / 其他>

请判断原因,并告诉我应该怎么改。

7.6 备份失败

现象:手动备份下载失败、计划备份显示失败,或上传到 R2 / S3 失败。

常见原因

  • 备份密码少于 12 个字符。
  • 磁盘空间不足,或 plost 用户不能写数据目录下的 backups/(例如改了数据目录却没改 systemd 的 ReadWritePaths)。
  • 数据库超过 1 GiB(计划备份会明确报错)。
  • 云存储的 Endpoint、存储桶名、区域、Access Key 写错,或密钥没有上传权限(「测试连接」只验证列表权限)。
  • Endpoint 不是公网 HTTPS 地址。Plost 只连接公网地址。
  • 服务器上开了会把域名解析成 198.18.0.0/15 虚拟地址的代理工具,所有出站连接都会显示「没有可安全访问的公网地址」。确认是这种情况后,可以加启动参数 -allow-outbound 198.18.0.0/15。不要用它放行内网。
  • 迁移服务器时没有带上 secrets.key,已保存的计划备份密码和存储密钥无法解密,需要在页面里重新填写。

检查命令

df -h /var/lib/plost
sudo du -sh /var/lib/plost/plost.db
sudo ls -la /var/lib/plost /var/lib/plost/backups /var/lib/plost/backups/scheduled 2>&1 | tail -n 30
journalctl -u plost -n 200 --no-pager | grep -i -E 'backup|s3|r2|restore'
systemctl cat plost | grep -E 'ReadWritePaths|ExecStart'

给 AI 的提示词

我自托管的网站统计工具 Plost 备份失败。

Plost 的备份规则:在后台生成加密的 .plostbk 文件,备份密码至少 12 位;计划备份保存在数据目录的 backups/scheduled/,可选上传到 Cloudflare R2 或 S3 兼容存储;云存储地址必须是公网 HTTPS,程序拒绝内网地址,本机代理的 198.18.0.0/15 虚拟地址需要用 -allow-outbound 放行;数据库超过 1 GiB 时计划备份会报错;计划备份密码和存储密钥用数据目录里的 secrets.key 加密保存。systemd 示例开启了 ProtectSystem=strict,只允许写 ReadWritePaths 里的目录。

失败的是:<手动备份 / 计划备份 / 上传到 R2 或 S3>
页面上的错误提示:<粘贴>
云存储类型和 Endpoint 格式:<例如 R2,https://<账号ID>.r2.cloudflarestorage.com,请隐藏账号 ID>
下面是检查命令的输出(已隐藏真实域名、IP、密钥):
<粘贴>

请判断原因,并给出具体的修改步骤。

还是没解决?

  • 运行 journalctl -u plost -n 100 --no-pager 查看完整日志。
  • 到 GitHub 仓库 提交 Issue,附上版本号(plost -version)、部署方式,以及已隐藏敏感信息的命令输出。