在 VPS 上自行托管 OpenAnalytics:资源与安装指南
先确认实际资源:ClickHouse、Postgres、Valkey、4 GB 内存、25 GB 可用磁盘和 4 条 DNS 记录。本文介绍安装步骤,并说明哪些数据会持续占用磁盘。
第一步之前的资源需求
要自行托管 OpenAnalytics,您需要一台约有 4 GB RAM、25 GB 可用磁盘空间的 Linux VPS,安装 Docker Compose 插件,并准备好 4 条已指向该服务器的 DNS 记录。这是必须先说明的实际要求,应放在第一个命令之前,而不是之后。
该堆栈包含 6 个应用服务和 3 个数据存储。Postgres 保存控制平面数据,包括账户、站点、API 密钥和共享链接。ClickHouse 保存原始事件以及仪表板读取的汇总数据。Valkey 运行 2 次:一次用作持久化事件队列,另一次用作可以丢失的缓存,因为这两种用途需要相反的驱逐策略。只有查询网关进程可以读取 ClickHouse,并且每次执行查询前都会验证查询封装中的 Ed25519 签名。
如果您需要的是一个二进制文件和一个配置文件,那么这不是适合您的方案。GoatCounter 是这一类别中的单二进制选项:一个 Go 可执行文件,默认使用 SQLite,完全不需要外部数据库。这个更重的堆栈支持漏斗分析、Web Vitals、从您自己的 Stripe 账户进行收入归因,以及 MCP(模型上下文协议)服务器。选择自行托管的分析工具一文会权衡这些取舍。本指南假设您已经做出了选择。
首先将4条 DNS 记录指向该服务器
开始任何操作前,4个子域名都必须解析到服务器的公网 IP。Caddy 会在首次启动时申请 Let's Encrypt 证书。如果域名尚未解析,验证就会失败。
app.example.com提供仪表板。api.example.com提供 API 和 OAuth 回调。c.example.com提供收集器和跟踪脚本。rt.example.com提供实时流。
使用4条 A 记录,或使用1条 A 记录和3条指向该记录的 CNAME 记录。继续之前,使用 dig +short app.example.com 确认解析结果。刚添加一分钟的域名,Let's Encrypt 使用的解析器可能仍将其缓存为 NXDOMAIN。因此,首次申请证书失败后,应等待一段时间,并查看 Caddy 日志。重新运行安装不会加快 DNS 传播速度。
如何使用 Docker Compose 自托管 OpenAnalytics
检出带标签的发行版。默认分支用于开发,实际发布的镜像与发行版标签匹配。以下命令假定已安装 Docker 和 Compose 插件,在 VPS 上运行 Docker Compose 服务对此进行了介绍。
git clone https://github.com/OpenLabs-so/openanalytics
cd openanalytics
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./generate-secrets.sh --domain example.com --email you@example.com --with-geoip
docker compose pull && docker compose up -d检出命令中的 sed '/-/d' 会排除预发行版标签,因此最终使用的是最新稳定版本,而不是候选发行版。--with-geoip 会在生成过程中获取 DB-IP city 数据库。跳过该步骤后,每个事件的国家字段都会是 null,地理视图将完全没有数据。之后可以运行 infra/selfhost/geoip/fetch-dbip.sh,在 env/collector.env 中设置 GEOIP_DB_PATH=/geoip/dbip-city-lite.mmdb,再使用 docker compose up -d --force-recreate collector 重新创建 collector,以补充该数据库。该数据库每月更新,因此应每月重新获取,否则城市数据会逐渐过时。
继续操作前,先备份生成的密钥
生成器会写入三类内容。.env包含域名和镜像引用。env/*.env按服务分别包含一个密钥文件。docker-compose.override.yml以 YAML 块标量的形式包含 3 对 Ed25519 密钥,因为多行 PEM 无法存放在环境变量文件中。所有这些文件都已加入 Git 忽略列表,而且无法重新生成出相同的值。
现在将这些文件复制到其他机器。每种文件丢失都会产生具体影响:
- 丢失存储密码后,您将无法访问 Postgres 和 ClickHouse,只能从容器内部重置密码。
- 丢失
OA_CREDENTIAL_KEYRING后,所有已存储的第三方凭据都无法恢复,因此任何已连接 Stripe 账户的用户都必须重新连接。 - 丢失
ANONYMOUS_IDENTITY_SECRET后,访客身份基线会重新建立:昨天的访客都会被计为新访客,图表中也会显示出中断。 - 丢失
AUTH_SECRET后,所有会话都会失效,因此所有人都必须重新登录。 - 丢失签名私钥后,只需轮换密钥对,不会丢失任何数据。
其中有两个密钥必须在各自的两个文件中保持字节完全一致。ANONYMOUS_IDENTITY_SECRET同时出现在 collector.env 和 worker.env 中,因为 collector 会计算访客哈希,而 worker 会写入该哈希。OA_CREDENTIAL_KEYRING同时出现在 api.env 和 worker.env 中。其他内容都特意限定为仅供一个服务使用;如果向服务提供了它不应持有的密钥,该服务会退出,而不是启动。
启动整套服务并检查
grep OA_IMAGE .env
docker compose pull
docker compose up -d
docker compose logs -f migrate
docker compose psmigrate 应用 Postgres 和 ClickHouse schema,然后退出,因此 migrate 容器停止是正确的最终状态。tracker-build 将 oa.js 编译到由 Caddy 提供服务的卷中,然后也会退出。其他所有服务都应在 docker compose ps 中读取 healthy。服务反复重启通常表示环境变量校验失败。日志会在一个列表中打印所有问题,而不是每次重启只打印一个问题。最常见的两个原因是变量留空;空值会被拒绝,不会按未设置处理。另一个原因是密钥放在了错误的服务文件中。
在 arm64 上,或从某个分支构建时,没有已发布的镜像,因此需要使用 docker compose up -d --build 在本地构建。4 GB 主机会在构建过程中耗尽内存。请先添加 swap;swap 只在构建期间需要:
fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab构建大约需要十分钟。拉取只需几分钟,这也是提供发布镜像的原因。
立即创建首个账户
打开 https://app.example.com。尚未有任何用户登录的部署不会显示登录表单,而是提供创建首个账户的选项。该账户永久拥有特权,也是唯一可以查看部署设置页面的账户。账户创建后,该路由会返回 409,因此其他人无法再进入。堆栈运行正常后立即完成此操作,不要拖到下一周。
安装跟踪器
在控制面板中添加站点后,系统会提供标签代码。其格式固定:
<script
async
src="https://c.example.com/oa.js"
data-key="YOUR_TRACKING_KEY"
data-collector="https://c.example.com"
></script>将其放入页面的 head 中。跟踪密钥本来就是公开的,因此应放在任何人都能读取的 HTML 中。该脚本会安装 window.oa。像 oa("track", ...) 这样的调用会先由存根排队,文件加载后再刷新,因此提前触发的自定义事件不会丢失。如果页面上的其他代码已经占用 window.oa,跟踪器会改为安装为 window.openanalytics。
然后端到端检查整个链路:
curl -s https://c.example.com/oa.js -o /dev/null -w '%{http_code} %{size_download}\n'
curl -s https://api.example.com/health | head -c 200
docker compose logs --tail=50 worker | grep -i batch第一条命令应输出 200 和几 KB 内容。在网站上加载一个页面,然后在几秒内检查 worker 日志中是否出现批处理记录。采集器接受事件后会立即返回 202,而 202 表示已排队,并不表示已存储。worker 负责将事件写入 ClickHouse。事件已被接受,但控制面板中没有任何显示,说明 worker 被阻塞;如果 Valkey 队列深度持续上升,则可确认这一点。最常见的原因是 worker.env 中的 ClickHouse 凭据错误,或迁移刚添加的表缺少相应授权。
保持采集端公开,并为仪表板启用身份验证
Caddy 已包含在 compose 文件中,并会自行为这4个域名申请证书,因此默认路径无需配置代理。如果服务器已经运行 Nginx 反向代理,请改用提供的 infra/selfhost/nginx.conf.example 将此堆栈接入,并保留其请求头处理逻辑:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header CF-Connecting-IP "";
proxy_set_header True-Client-IP "";
proxy_set_header Fly-Client-IP "";采集器根据客户端 IP 生成每日访客哈希,因此必须从连接中获取该地址,不能从请求头获取。来自不可信跳转节点的请求如果透传 CF-Connecting-IP,任何调用方都可以伪造任意地址,从而同时破坏地理位置数据并夸大访客数。
按主机名划分访问权限即可。所有被统计的网站访客都必须能够访问 c. 和 rt.,因此不要在这两个地址前配置 basic auth 或 IP allowlist。只有登录用户需要访问 app. 和 api.。仪表板由应用自身的身份验证机制保护:默认情况下,AUTH_PASSWORD_SIGNIN=enabled 中的 env/api.env 会启用密码登录;只有提供商同时存在 client ID 和 client secret 时,Google 或 GitHub 登录按钮才会显示。Magic link 需要邮件传输配置;如果未配置,API 只会将发送操作写入 outbox,不会实际发送,也不会报错。
有一项设置决定仪表板是否能够正常工作。env/api.env 中的 AUTH_TRUSTED_ORIGINS 必须与仪表板源完全匹配。如果该值错误或缺失,API 不会发送 CORS(跨源资源共享)响应头,浏览器会拒绝所有调用。此时仪表板可以渲染布局,但不会显示数据,而 docker compose ps 仍会报告所有组件正常。
配置代理时,同时处理自动化流量。爬虫与其他访客一样会访问采集器,其页面浏览量会写入 ClickHouse 并计入统计数据。在服务器端阻止 AI 爬虫可以在这些请求进入数据库前拦截其中一部分,避免同时影响数据准确性并增加磁盘占用。
这里的无 Cookie 意味着什么,以及需要付出什么代价
这里不会写入 Cookie。访客身份是加盐哈希值,盐值每天轮换,且绝不存储原始 IP 地址。地理位置会在本地使用磁盘上的 DB-IP 文件解析,因此不会有任何关于访客的查询离开主机。
这样做的好处是,访客设备上不会持久化存储标识符。正是持久化标识符会使跟踪器受到欧盟 ePrivacy 同意规则的约束。因此,这类仅统计聚合数据的部署通常无需显示同意横幅。GDPR 仍然适用于您存储的数据及其保留时长。您的具体情况应由您自己的法律顾问判断,而不是由 README 决定。
代价是无法跨天识别访客。由于盐值会轮换,周一访问、周三再次访问的同一个人会被设计为统计成两个访客,且没有规避方法。每日独立访客数是准确的。每周和每月独立访客数由每日数据构成,因此会高估覆盖范围。任何较长时间窗口内的“回访访客”数据,都不能代表其标签所描述的指标。单日内的会话和访问路径是可靠的。轮换 ANONYMOUS_IDENTITY_SECRET 的效果与跨越一天相同,因此应将该轮换视为数据变更,而不是例行维护。
采集器遵循 Do Not Track 和 Global Privacy Control。这是浏览器发出的信号,用于告知网站不要出售或共享个人数据。脚本标签提供了用于相同目的的开关:data-respect-gpc、data-respect-dnt 和 data-require-consent。其中 data-require-consent 会在获得同意前暂停所有采集,并将选择记录在 localStorage 中,使用的键为 oa.consent。设置 data-storage="none" 后会完全关闭浏览器存储。
磁盘为什么在 6 个月后被占满
这通常是自托管分析服务器出问题的原因,而事件数据往往不是主因。
先检查镜像。一次发布会生成 10 个镜像,总计约占用 13 GB 磁盘空间。升级时,系统会先拉取新一代镜像,再删除旧镜像,因此一段时间内会同时保留两代镜像。这已经占用了 25 GB 需求中的大部分空间,此时还没有产生任何页面访问数据。
然后检查快照。snapshot.sh会停止整个服务栈,将两个数据卷及所有密钥一并归档,然后重新启动。这里唯一安全的方式是冷备份,因为 ClickHouse 会在后台合并数据分片,在合并期间创建的副本不一致。upgrade.sh会在每次升级前自动创建快照,因此归档文件会持续累积在同一块磁盘上,直到达到保留上限。
./snapshot.sh create --label before-something-risky
./snapshot.sh list
./snapshot.sh --keep 3如果主机的磁盘空间接近上限,请在升级前回收上一代镜像。服务栈运行期间执行此操作是安全的,因为运行中容器所使用的镜像仍有引用:
docker image prune -a -f然后检查事件数据本身。ClickHouse 会对列式数据进行高效压缩,因此原始事件数据的增长速度通常低于预期,仪表板读取的汇总表相较原始表也很小。请通过测量而不是猜测来判断:
docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhouse要获取每个表的占用空间,请使用生成器写入 infra/selfhost/env/ 下的 ClickHouse 凭据运行以下命令:
SELECT table, formatReadableSize(sum(bytes_on_disk)) AS size, sum(rows) AS row_count
FROM system.parts
WHERE active
GROUP BY table
ORDER BY sum(bytes_on_disk) DESC;在第 1 周和第 4 周分别记录一次结果。两个数据点即可计算增长率,增长率可以帮助您判断何时需要扩容存储卷。截至 2026 年 8 月,自托管指南没有记录用于原始事件的保留期限或 TTL 配置项。因此,请根据实际测得的增长率规划磁盘容量,不要假定旧数据会自动过期。
有一个删除陷阱需要提前了解。删除站点或账户后,系统会将任务加入 worker 队列;该 worker 要求设置 CLICKHOUSE_MAINTENANCE_USER 和 CLICKHOUSE_MAINTENANCE_PASSWORD,并且 ClickHouse 中必须存在匹配的 oa_maintenance 用户。缺少这些配置时,删除任务会永久停留在队列中。站点会从仪表板中消失,但所有数据行仍保留在磁盘上,因此看起来像是完成了清理,实际却没有释放任何空间。
升级及三项成本
git fetch --tags
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./upgrade.shupgrade.sh 执行前会打印三项成本。停机确实会造成损失:采集器停机期间尝试上报的事件都会丢失,因为跟踪器不会重试。回滚会丢失数据,因为 rollback.sh --to backups/<snapshot> 会整体替换两个存储,并丢弃创建该快照后写入的所有行。磁盘空间是第三项成本,也就是上文所述的快照堆积。
有两条重启规则很容易弄错。先启动查询网关,再启动 API,因为新版 API 发送的查询字段可能会被旧版网关拒绝。ClickHouse 则需要重新创建容器,而不是重启,因为 docker compose restart 会复用容器最初的环境变量,并静默忽略你的修改:
docker compose up -d --force-recreate clickhouse仪表板也存在同类问题。env/web.env 中的三个 NEXT_PUBLIC_* 来源会编译到浏览器 bundle 中,并在容器启动时替换。因此,仪表板调用了错误主机名时,应使用 docker compose up -d --force-recreate web 修复,不能使用 restart。Web 容器的日志会打印其启动时使用的来源,这是确认修复是否生效的最快方法。
如果修改配置后 ClickHouse 拒绝启动,请查看其日志的第一行。以 oa-entrypoint: 开头的行表示 entrypoint 拒绝了你设置的值。其他情况通常表示配置文件不是有效的 XML;最常见的原因是 XML 注释中包含双连字符,而 XML 注释不允许出现双连字符。
AGPL-3.0,以及名称
代码采用 AGPL-3.0 许可证。未经修改地运行代码来服务您自己的网站,完全不会产生发布义务。只有在您修改代码,并将修改后的版本作为网络服务运行时,才会产生该义务:许可证要求您向该服务的用户提供修改后的源代码。这包括在您的实例上为客户提供仪表板,也包括将其打包到您销售的产品中。将修改保留在公开 fork 中即可满足该要求,无需其他流程。
品牌与代码相互独立。“OpenAnalytics”名称和项目的托管域名用于标识其作者运营的实例,不属于许可证授予范围。您的部署运行的是该软件,但不应继续使用其品牌。因此,在面向付费客户提供服务前,请为该服务设置独立名称。
FAQ
我可以在 1 GB VPS 上运行 OpenAnalytics 吗?
不可以。该项目需要约 4 GB RAM 和 25 GB 可用磁盘空间,因为一次部署会运行 6 个应用服务,以及 Postgres、ClickHouse 和 2 个 Valkey 实例。仅 ClickHouse 就不是小型进程。在 1 GB 服务器上,容器启动后,内核的 out-of-memory killer 会终止其中一个容器,通常是 ClickHouse。如果 1 GB 套餐是硬性限制,请使用 GoatCounter 这类单二进制工具。它使用 SQLite,无需外部数据库。
使用 OpenAnalytics 时需要添加 Cookie 横幅吗?
这个问题应咨询您的律师,但从技术事实看,您处于有利位置。系统不会使用 Cookie,访客标识是每天轮换的加盐哈希,也不会存储原始 IP 地址,因此不会写入可持久识别访客的信息。GDPR 仍然规定您存储哪些数据以及保留多长时间。如果您希望明确要求用户同意后才开始收集数据,请在脚本标签上设置 data-require-consent:跟踪器在获得同意前不会收集任何数据,并将用户的选择保存在 localStorage 的 oa.consent 中。
为什么事件返回 202,却始终不显示在控制面板中?
202 表示收集器已接受事件并将其加入队列,并不表示事件已经存储。工作进程会将该队列中的事件写入 ClickHouse,因此请求成功但控制面板为空,通常说明工作进程存在问题。读取 docker compose logs --tail=50 worker,并监控 Valkey 队列深度。队列持续增长表示工作进程被阻塞,常见原因是 worker.env 中的 ClickHouse 凭据错误,或者最近的迁移创建了某个表,但相应表缺少授权。
为什么每个容器都正常运行,但控制面板仍然为空?
首先检查 env/api.env 中的 AUTH_TRUSTED_ORIGINS。它必须与控制面板的来源完全匹配。如果不匹配,API 不会发送 CORS 响应头,浏览器会拒绝所有请求,因此您只能看到正常显示的页面布局,却看不到数据。第二项检查内容是 env/web.env 中的 3 个 NEXT_PUBLIC_* 值。这些值会在 Web 容器启动时替换。修正这些值需要执行 docker compose up -d --force-recreate web,因为普通重启会保留旧值。
AGPL-3.0 会阻止我向客户提供这项服务吗?
不会,但它附带一个条件。直接运行未修改的代码时,您不需要向任何人提供额外内容。如果修改代码,并将修改后的版本作为其他人使用的服务运行,则必须向这些用户提供修改后的源代码;发布公开 fork 即可满足这一要求。此外,“OpenAnalytics”这一名称并未随代码一同获得许可,因此您销售的任何服务都需要使用自己的名称。