SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

OpenAnalytics VPS 自架安裝與資源需求

先確認實際需求:ClickHouse、Postgres、Valkey、4 GB RAM、25 GB 可用空間與 4 筆 DNS 記錄,再了解 Docker Compose 安裝流程及磁碟空間用途。

開始前的資源需求

若要自行託管 OpenAnalytics,需要一台約有 4 GB RAM、25 GB 可用磁碟空間的 Linux VPS,安裝 Docker 與 Compose plugin,並預先建立 4 筆已指向該主機的 DNS 記錄。這就是實際的前提,應放在第一個命令之前,而不是之後。

此堆疊包含 6 個應用程式服務與 3 個資料儲存服務。Postgres 儲存控制平面資料,包括帳戶、網站、API keys 與分享連結。ClickHouse 儲存原始事件,以及儀表板讀取的彙總資料。Valkey 執行 2 次:一次作為持久化事件佇列,另一次作為可捨棄的快取,因為這兩項工作需要相反的驅逐政策。只有 query gateway 能讀取 ClickHouse,而且每次執行查詢前,都會先驗證查詢封裝中的 Ed25519 簽章。

如果你要的是單一 binary 與單一設定檔,這套系統就不符合需求。GoatCounter 是此類工具中的單一 binary 選項:一個 Go 執行檔,預設使用 SQLite,完全不需要外部資料庫。較重的堆疊可提供 funnels、web vitals、從自有 Stripe 帳戶取得的 revenue attribution,以及 MCP(model context protocol)伺服器。選擇自行託管的分析工具 這篇文章會比較其中的取捨。本指南假設你已經做出決定。

先將 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 確認設定。剛在 1 分鐘前新增的名稱,仍可能被 Let's Encrypt 使用的 DNS 解析器快取為 NXDOMAIN,因此首次憑證要求失敗時,應等待 DNS 快取更新,並查看 Caddy 日誌。重新執行安裝不會加快 DNS 傳播速度。

如何使用 Docker Compose 自架 OpenAnalytics

檢出已標記的版本。預設分支是開發內容所在的位置,而發布標籤才是已發布映像檔實際對應的版本。以下命令假設 Docker 與 Compose plugin 已安裝完成,在 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' 會排除預發布標籤,因此取得的是最新穩定版本,而不是 release candidate。--with-geoip 會在產生期間擷取 DB-IP city database。若略過此步驟,每個事件的國家欄位都會是 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,以補上此資料庫。此資料庫每月更新,因此也應每月重新擷取,否則城市資料會逐漸偏離最新狀態。

繼續之前,先備份產生的 secret

產生器會寫入 3 類內容。.env 存放網域名稱與 image 參照。env/*.env 為每個服務各存放 1 個 secret 檔案。docker-compose.override.yml 以 YAML block scalar 存放 3 組 Ed25519 金鑰組,因為多行 PEM 無法放在 env 檔案中。這些內容都已加入 git-ignore,而且無法重新產生出相同值。

現在就將這些檔案複製到其他機器。遺失不同內容會造成不同影響:

  • 遺失 store 密碼後,您將無法登入 Postgres 和 ClickHouse;只能從容器內重設。
  • 遺失 OA_CREDENTIAL_KEYRING 後,所有已儲存的第三方憑證都無法復原,因此任何已連結 Stripe 帳戶的人都必須重新連結。
  • 遺失 ANONYMOUS_IDENTITY_SECRET 後,訪客識別會重新建立基準:昨天的訪客都會被視為新訪客,而且圖表中會看出這段中斷。
  • 遺失 AUTH_SECRET 後,所有 session 都會失效,因此所有人都必須重新登入。
  • 遺失 signing private key 後,請輪替金鑰組。此操作不會遺失任何資料。

有 2 個 secret 必須在各自的 2 個檔案中保持完全相同的位元組內容。ANONYMOUS_IDENTITY_SECRET 會出現在 collector.envworker.env 中,因為 collector 會計算訪客 hash,而 worker 會寫入該 hash。OA_CREDENTIAL_KEYRING 會出現在 api.envworker.env 中。其他內容都刻意只限定給 1 個服務使用;若服務取得不應持有的 secret,會直接結束而不會啟動。

啟動整個服務堆疊並檢查

grep OA_IMAGE .env
docker compose pull
docker compose up -d
docker compose logs -f migrate
docker compose ps

migrate 套用 Postgres 和 ClickHouse 的 schema,然後結束,因此停止中的 migrate 容器才是正確的最終狀態。tracker-buildoa.js 編譯到由 Caddy 提供服務的 volume 中,然後也會結束。其他服務都應在 docker compose ps 中讀取 healthy。服務若不斷重新啟動,幾乎總是環境驗證失敗所致;日誌會在同一份清單中列出所有問題,而不是每次重新啟動只列出一個問題。最常見的兩個原因是變數留白,系統會拒絕這種值,而不是將其視為未設定;以及 secret 放在錯誤的服務檔案中。

在 arm64 上,或從 branch 建置時,沒有已發布的 image,必須使用 docker compose up -d --build 在本機建置。4 GB 的主機會在建置過程中途耗盡記憶體。請先加入 swap;這只在建置期間需要:

fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

建置大約需要十10分鐘。下載只需幾分鐘,這就是存在 release image 的原因。

立即建立第一個帳號

開啟 https://app.example.com。尚未有人登入的部署不會顯示登入表單,而是提供建立第一個帳號的選項。這個帳號會永久保有特權,也是唯一能查看部署設定畫面的帳號。帳號建立後,該路徑會回應 409,因此其他人無法在你之後直接進入。堆疊確認正常運作後,應立即完成此操作,不要拖到隔週。

安裝 tracker

在儀表板中新增網站後,系統會提供追蹤標籤。其格式固定如下:

<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", ...) 這類呼叫會先由 stub 排入佇列,檔案載入後再送出,因此提早觸發的自訂事件不會遺失。如果頁面上的其他程式已經使用 window.oa,tracker 會改用 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 日誌中是否出現批次處理行。collector 接受事件後會立即回覆 202,而 202 表示事件已排入佇列,不代表已儲存。負責將事件寫入 ClickHouse 的是 worker。事件已被接受,但儀表板沒有顯示任何資料,表示 worker 受阻;若 Valkey 佇列深度持續增加,即可確認這點。最常見的原因是 worker.env 中的 ClickHouse 憑證錯誤,或新 migration 新增的資料表缺少必要的授權。

讓 collector 對外公開,讓 dashboard 受驗證保護

Caddy 已包含在 compose 檔案中,並會自行為全部 4 個名稱取得憑證,因此預設路徑不需要你處理 proxy。如果主機已經執行 nginx reverse proxy,請改用提供的 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 "";

collector 會根據 client IP 計算每日訪客雜湊,因此必須從連線取得該位址,不能從標頭取得。若從不受信任的 hop 傳入 CF-Connecting-IP,任何呼叫端都能宣稱使用任意位址,這會同時破壞地理定位結果並抬高訪客數。

不同 hostname 的存取權限區分明確。所有受測網站的每位訪客都必須能連線到 c.rt.,因此不要在這兩者前方設定 basic auth 或 IP allowlist。只有已登入的使用者需要連線到 app.api.。dashboard 由應用程式本身的 auth 保護:透過 AUTH_PASSWORD_SIGNIN=enabledenv/api.env 中預設啟用 password sign-in;只有在該 provider 同時存在 client ID 和 client secret 時,才會顯示 Google 或 GitHub 按鈕。Magic links 需要 mail transport;若未設定,API 只會將寄送要求寫入 outbox,因此不會送出郵件,也不會產生錯誤。

有一項設定會決定 dashboard 是否能正常運作。AUTH_TRUSTED_ORIGINSenv/api.env 中必須與 dashboard origin 完全相符。若設定錯誤或缺少,API 不會產生 CORS(cross-origin resource sharing)標頭,瀏覽器會拒絕所有呼叫,結果是 dashboard 只顯示版面而沒有資料,但 docker compose ps 仍會回報所有項目正常。

設定 proxy 時,也要處理自動化流量。Crawler 會像一般使用者一樣連線到 collector,其頁面瀏覽量也會寫入 ClickHouse 並計入統計。在伺服器封鎖 AI crawler 可在這些流量造成準確度與磁碟空間成本前,先阻擋其中一部分,避免寫入資料庫。

這裡不會設定 Cookie。訪客識別資訊是加鹽雜湊值,salt 每天輪換,且永遠不會儲存原始 IP 位址。系統會在本機使用自己磁碟上的 DB-IP 檔案解析地理位置,因此任何訪客查詢都不會離開主機。

這項設計的好處,是不會在訪客裝置上持久保存識別資訊。正是持久識別資訊會讓追蹤器受到 EU ePrivacy 同意規則的規範。因此,這類僅保留彙總資料的設定通常不會顯示同意橫幅。GDPR 仍適用於你實際儲存的資料及其保存時間;是否適用於你的情況,應由你的法律顧問判斷,而不是 README。

代價是無法跨日識別訪客。salt 輪換後,星期一造訪、星期三再次造訪的同一個人會被計為 2 名訪客。這是設計上的結果,沒有替代做法。每日不重複訪客數是可靠的。每週與每月不重複訪客數是由每日數據計算,因此會高估觸及人數。任何長時間範圍的「回訪訪客」數字,都無法反映其標示的實際意義。單日內的工作階段與瀏覽歷程是可靠的。輪換 ANONYMOUS_IDENTITY_SECRET 的效果與跨越一天相同,因此應將這項輪換視為資料變更,而不是例行維護。

收集器會遵守 Do Not Track 與 Global Privacy Control。這些是告知網站不得出售或分享個人資料的瀏覽器訊號。script tag 也提供相同用途的開關:data-respect-gpcdata-respect-dntdata-require-consent。其中 data-require-consent 會在取得同意前暫停所有資料收集,並將選擇記錄在 localStorage 中,使用的 key 為 oa.consent。設定 data-storage="none" 會完全停用瀏覽器儲存功能。

磁碟為何在 6 個月後用滿

這是自架分析伺服器最常見的故障原因,而事件資料通常不是主因。

先檢查映像檔。一個 release 會發布 10 個映像檔,合計約占用 13 GB 磁碟空間。升級時,系統會先拉取新版本,再移除舊版本,因此會暫時保留 2 個版本。這已經占去 25 GB 需求中的大部分,還沒計入任何頁面瀏覽資料。

接著是快照。snapshot.sh 會停止 stack,將 2 個資料 volume 及所有 secret 封存後再重新啟動。這裡只有 cold copy 安全,因為 ClickHouse 會在背景合併 parts,而在合併期間建立的 copy 並不一致。upgrade.sh 會在每次升級前自動建立快照,因此封存檔會持續累積在同一顆磁碟上,直到你設定上限。

./snapshot.sh create --label before-something-risky
./snapshot.sh list
./snapshot.sh --keep 3

在磁碟容量接近上限的主機上,請在升級前回收上一個版本。只要 stack 正在執行,這樣做就是安全的,因為執行中容器所使用的映像檔仍有參照:

docker image prune -a -f

接著是事件資料本身。ClickHouse 會對 columnar data 進行高強度壓縮,因此 raw event 的成長速度通常比預期慢,而 dashboard 讀取的 rollup table 相較於 raw table 也很小。請實際測量,不要只靠猜測:

docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhouse

若要取得各 table 的數值,請使用 generator 寫入 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 週各記錄一次數值。2 個測量點即可算出成長率,而成長率能告訴你何時需要擴充 volume。截至 2026 年 8 月,self-hosting guide 沒有記載 raw event 的 retention 或 time-to-live 設定,因此請依據實測成長率規劃磁碟容量,不要假設舊資料會自行過期。

有一個刪除陷阱需要事先了解。刪除 site 或 account 時,系統會將工作排入 worker 的佇列,而該 worker 必須設定 CLICKHOUSE_MAINTENANCE_USERCLICKHOUSE_MAINTENANCE_PASSWORD,且 ClickHouse 中必須存在相符的 oa_maintenance user。缺少這些設定時,刪除工作會永遠留在佇列中。site 會從 dashboard 消失,但所有資料列仍留在磁碟上,因此看起來像是完成清理,實際上卻沒有釋放任何空間。

升級與三項成本

git fetch --tags
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./upgrade.sh

upgrade.sh 執行前會列出三項成本。停機會造成實際損失:collector 停止期間嘗試送出的事件會遺失,因為 tracker 不會重試。回復會遺失資料,因為 rollback.sh --to backups/<snapshot> 會完整替換兩個儲存區,並捨棄該 snapshot 建立後寫入的所有資料列。磁碟空間是第三項成本,也就是前文所述累積的 snapshot。

有兩項重新啟動規則很容易弄錯。先啟動 query gateway,再啟動 API,因為較新的 API 會傳送較舊 gateway 拒絕的 query 欄位。ClickHouse 則需要重新建立容器,而不是重新啟動,因為 docker compose restart 會沿用容器最初的環境設定,並靜默忽略你的修改:

docker compose up -d --force-recreate clickhouse

dashboard 也有相同類型的陷阱。env/web.env 中的三個 NEXT_PUBLIC_* origin 會編譯到瀏覽器 bundle,並在容器啟動時代入。因此,dashboard 若呼叫錯誤的 hostname,應使用 docker compose up -d --force-recreate web 修正,不能使用 restart。web container 的 log 會列出啟動時使用的 origin,這是確認修正已套用的最快方式。

如果 ClickHouse 在修改設定後拒絕啟動,請查看其 log 的第一行。若該行以 oa-entrypoint: 開頭,表示 entrypoint 拒絕你設定的值。其他情況通常表示設定檔不是有效的 XML;最常見的原因是在 XML comment 中包含 double hyphen,而該內容在 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 主機上,容器會先啟動,接著 kernel 的 out-of-memory killer 會終止其中一個,通常是 ClickHouse。如果 1 GB 方案是硬性限制,請改用 GoatCounter 這類單一執行檔工具;它使用 SQLite,不需要外部資料庫。

這需要詢問你的律師,但技術事實對你有利。系統不使用 cookie,訪客識別資訊是每日輪替的加鹽雜湊值,也不會儲存原始 IP 位址,因此不會寫入可持久識別訪客的資料。GDPR 仍適用於你儲存的資料及其保留時間。若要明確要求取得同意後才開始收集,請在 script tag 上設定 data-require-consent:追蹤器在取得同意前不會收集任何資料,並將選擇儲存在 localStorageoa.consent 下。

為什麼事件回傳 202,卻永遠不會出現在 dashboard?

202 表示 collector 已接受並將事件加入佇列,不代表事件已儲存。worker 會將該佇列中的事件寫入 ClickHouse,因此請求成功但 dashboard 為空,通常表示問題出在 worker。讀取 docker compose logs --tail=50 worker,並監控 Valkey 佇列深度。佇列持續增長表示 worker 受阻,常見原因是 worker.env 中的 ClickHouse 憑證錯誤,或最近的 migration 建立了資料表,但缺少對該資料表的授權。

為什麼每個容器都正常時,dashboard 仍然是空的?

先檢查 AUTH_TRUSTED_ORIGINS 中的 env/api.env。它必須與 dashboard 的 origin 完全一致。若不一致,API 不會輸出 CORS 標頭,因此瀏覽器會拒絕所有呼叫,畫面看似正常但沒有資料。接著檢查 env/web.env 中的 3 個 NEXT_PUBLIC_* 值;這些值會在 web container 啟動時代入。修正後需要執行 docker compose up -d --force-recreate web,因為單純重新啟動會保留舊值。

AGPL-3.0 會阻止我將這項服務提供給客戶嗎?

不會,但有一項條件。直接執行未修改的程式碼,不需要向任何人提供額外內容。若修改程式碼,並將修改後的版本作為其他人使用的服務執行,就必須向這些使用者提供修改後的原始碼;公開 fork 即可符合這項要求。另外,「OpenAnalytics」這個名稱並未與程式碼一併授權,因此你銷售的任何服務都需要使用自己的名稱。