OpenAnalytics VPS 自架:4 GB RAM 與 DNS 設定
自架 OpenAnalytics 前先確認實際需求:ClickHouse、Postgres、Valkey、4 GB RAM、25 GB 可用空間與 4 筆 DNS 記錄,並了解安裝後磁碟空間如何被占用。
第一步之前,先確認資源需求
要自行代管 OpenAnalytics,您需要一台約有 4 GB RAM、25 GB 可用磁碟空間,並已安裝 Docker Compose plugin 的 Linux VPS,另外還要有 4 筆已指向該主機的 DNS 記錄。這是應在執行第一個命令前說明的實際需求,而不是事後才補充。
這個堆疊包含 6 個應用程式服務和 3 個資料存放區。Postgres 儲存控制平面的資料,包括帳戶、網站、API keys 和分享連結。ClickHouse 儲存原始事件,以及儀表板讀取的彙總資料。Valkey 執行兩個執行個體,分別作為持久化事件佇列和可丟失的快取,因為這兩項工作需要相反的淘汰政策。只有 query gateway 可讀取 ClickHouse,而且每次執行查詢前,都會驗證查詢封裝中的 Ed25519 簽章。
如果您要的是單一 binary 和單一設定檔,那麼這不是適合的方案。GoatCounter 是此類工具中的單一 binary 選項:一個 Go 執行檔,預設使用 SQLite,完全不需要外部資料庫。較複雜的堆疊可提供漏斗分析、網站效能指標、從您自己的 Stripe 帳戶取得的營收歸因,以及 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 使用的解析器快取為 NXDOMAIN,因此首次憑證要求失敗時,請等待一段時間,並查看 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 -dcheckout 指令中的 sed '/-/d' 會排除預發行標籤,因此會切換至最新的穩定版本,而不是 release candidate。--with-geoip 會在產生期間擷取 DB-IP city database。若略過此步驟,每個事件的 country 都會是 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。該資料庫每月更新,因此每月都要重新擷取,否則 city data 會逐漸過時。
先備份產生的 secret,再繼續操作
產生器會寫入 3 類內容。.env 存放網域名稱與映像檔參照。env/*.env 為每個服務各存放 1 個 secret 檔案。docker-compose.override.yml 以 YAML block scalar 格式存放 3 組 Ed25519 金鑰組,因為多行 PEM 無法存放在 env 檔案中。這些內容都已加入 gitignore,且無法重新產生為相同值。
現在就將這些檔案複製到其他機器。遺失各項內容會造成不同的影響:
- 遺失資料庫密碼後,您將無法存取 Postgres 與 ClickHouse。只能從容器內重設。
- 遺失
OA_CREDENTIAL_KEYRING後,所有已儲存的第三方憑證都無法復原。任何已連結 Stripe 帳戶的使用者都必須重新連結。 - 遺失
ANONYMOUS_IDENTITY_SECRET後,訪客識別會重新建立基準。昨天的訪客都會被視為新訪客,圖表中也會顯示這段中斷。 - 遺失
AUTH_SECRET後,所有 session 都會失效,因此所有人都必須重新登入。 - 遺失簽署用私密金鑰後,請輪替整組金鑰。這不會遺失任何資料。
有 2 個 secret 必須分別在 2 個檔案中保持完全相同的位元組內容。ANONYMOUS_IDENTITY_SECRET 會出現在 collector.env 與 worker.env 中,因為 collector 會計算訪客雜湊值,而 worker 會寫入該值。OA_CREDENTIAL_KEYRING 會出現在 api.env 與 worker.env 中。其他內容都刻意限定給單一服務使用;若服務取得不應持有的 secret,會直接結束,而不是啟動。
啟動服務堆疊並檢查
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 提供服務的 volume,然後也會結束。其他所有服務都應在 docker compose ps 中讀取 healthy。服務持續循環重新啟動時,幾乎總是環境驗證失敗。日誌會在同一份清單中列出所有問題,而不是每次重新啟動只列出一個問題。最常見的兩個原因是變數留白;留白會被拒絕,不會視為未設定,以及將 secret 放在錯誤的服務檔案中。
在 arm64 上,或從 branch 建置時,沒有已發布的 image,必須使用 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建置約需十分鐘。下載只需幾分鐘,這也是建立 release image 的原因。
立即認領第一個帳戶
開啟 https://app.example.com。尚未有任何人登入的部署不會顯示登入表單,而是提供建立第一個帳戶的選項。該帳戶會永久成為具備最高權限的帳戶,也是唯一能查看部署設定畫面的帳戶。帳戶建立後,該路由會回應 409,因此不會有人能在你之後直接進入。堆疊確認正常運作後,應立即完成此操作,不要拖到下一週。
安裝 tracker
在 dashboard 中新增網站後,系統會提供追蹤標籤。其格式固定如下:
<script
async
src="https://c.example.com/oa.js"
data-key="YOUR_TRACKING_KEY"
data-collector="https://c.example.com"
></script>將它放在頁面的 head 中。追蹤金鑰本來就是公開資訊,因此應放在任何人都能讀取的 HTML 中。此 script 會安裝 window.oa。例如 oa("track", ...) 這類呼叫會先由 stub 排入佇列,待檔案載入後再送出,因此不會遺失過早觸發的自訂事件。如果頁面上的其他程式已使用 window.oa,tracker 會改以 window.openanalytics 安裝。如果同一個網站也透過 onion service 提供服務,請勿在該版本中加入這個標籤,因為從 c.example.com 擷取的 script 會將 Tor Browser 的訪客帶回 clearnet,並在同一次頁面載入中連結這兩個位址。
接著端對端檢查完整流程:
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 log 是否出現批次記錄。collector 接受事件後會立即回覆 202,而 202 表示已排入佇列,不代表已儲存。worker 會將事件寫入 ClickHouse。如果事件已被接受,但 dashboard 沒有顯示任何內容,表示 worker 受到阻塞;持續上升的 Valkey queue depth 也能確認這一點。常見原因是 worker.env 中的 ClickHouse 憑證錯誤,或最近新增的 migration 所涉及資料表缺少必要的 grant。
讓 collector 公開,並在 dashboard 前加上驗證
Caddy 已包含在 compose file 中,並會自行為 4 個名稱申請憑證,因此預設路徑不需要你處理 proxy。如果主機已經執行 Nginx reverse proxy,請改用提供的 infra/selfhost/nginx.conf.example 將這個 stack 放在其後方,並保留原有的標頭處理方式:
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.,因此不要在這兩個 hostname 前方設定 basic auth 或 IP allowlist。app. 和 api. 只需要讓已登入的使用者連線。dashboard 由應用程式本身的 auth 保護:透過 env/api.env 中的 AUTH_PASSWORD_SIGNIN=enabled,密碼登入預設已啟用;只有在該 provider 同時存在 client ID 和 client secret 時,才會顯示 Google 或 GitHub 按鈕。Magic link 需要 mail transport;若未設定,API 只會將寄送作業寫入 outbox,因此郵件不會送出,也不會產生錯誤。如果其他自架應用程式已經放在 單一 Authentik 登入後方,請及早決定這個 dashboard 要加入其中,還是維持自己的帳戶,因為你在這裡建立的第一個帳戶會永久具備 privileged 權限。
有一項設定會直接決定 dashboard 是否能運作。env/api.env 中的 AUTH_TRUSTED_ORIGINS 必須與 dashboard origin 完全相符。若值錯誤或未設定,API 不會發出 CORS(跨來源資源共用)標頭,瀏覽器會拒絕所有呼叫;此時 dashboard 會顯示版面配置但沒有資料,而 docker compose ps 仍會回報一切正常。
設定 proxy 時,也要處理自動化流量。Crawler 會像其他訪客一樣請求 collector,其頁面瀏覽量也會寫入 ClickHouse 並計入統計。在伺服器封鎖 AI crawler,可在這些流量影響資料準確性與佔用磁碟空間前,先將其中一部分排除於資料庫之外。
此處的無 Cookie 意味,以及它的代價
這裡不會使用 Cookie。訪客識別碼是加鹽雜湊值,salt 每天輪替,且永遠不會儲存原始 IP 位址。Geolocation 會在本機使用自己磁碟上的 DB-IP 檔案解析,因此任何訪客查詢都不會離開主機。在本機執行查詢只能移除廠商,無法移除資料;這與 自行執行 SearXNG instance 時相同,因為搜尋引擎看到的是伺服器的 IP 位址。
這樣做的好處,是不會在訪客裝置上持久儲存識別碼。正是這類識別碼會使追蹤器受到 EU ePrivacy 的同意規範約束。因此,像這樣僅提供彙總資料的設定,通常不需要顯示同意橫幅。GDPR 仍適用於你實際儲存的資料及其保存時間;你的個案應由法律顧問判斷,而不是由 README 決定。
代價是無法跨日識別。由於 salt 會輪替,同一個人在星期一和星期三造訪時,會被設計成計算為兩名訪客,且沒有補救方式。每日不重複訪客數是可靠的。每週和每月不重複訪客數是由每日數值計算,因此會高估觸及人數;任何較長期間的「回訪訪客」數字,都不代表其標籤所宣稱的意義。單日內的工作階段與瀏覽路徑是可靠的。輪替 ANONYMOUS_IDENTITY_SECRET 的效果與跨日相同,因此應將這項輪替視為資料變更,而不是例行維護。
Collector 會遵守 Do Not Track 和 Global Privacy Control。這是瀏覽器傳送的訊號,用來告知網站不要出售或分享個人資料。Script tag 也提供相同用途的開關:data-respect-gpc、data-respect-dnt 和 data-require-consent。其中 data-require-consent 會在取得同意前暫停所有資料收集,並將回答儲存在 localStorage 的 oa.consent key 中。設定 data-storage="none" 會完全停用瀏覽器儲存功能。
磁碟為何在 6 個月後塞滿
這是自架分析伺服器最常見的故障原因,而事件資料通常不是主因。
先從映像檔開始。一次 release 會發布 10 個映像檔,合計約占用 13 GB 磁碟空間。升級時會先拉取新一代映像檔,再移除舊映像檔,因此一段時間內會同時保留兩代映像檔。單是這項就占去 25 GB 需求中的大部分,而且此時甚至還沒有任何頁面瀏覽事件。
接著是快照。snapshot.sh 會停止整個 stack,將兩個資料 volume 連同所有 secret 一起封存,然後重新啟動。這裡只有冷備份是安全的做法,因為 ClickHouse 會在背景合併 parts,而在合併期間建立的複本並不一致。upgrade.sh 會在每次升級前自動建立快照,因此封存檔會持續累積在同一個磁碟上,直到你設定保留上限。
./snapshot.sh create --label before-something-risky
./snapshot.sh list
./snapshot.sh --keep 3在接近容量上限的主機上,請在升級前回收上一代映像檔。stack 運作期間執行這項操作是安全的,因為執行中容器所使用的映像檔仍有參照:
docker image prune -a -f接著是事件資料本身。ClickHouse 會大幅壓縮欄式資料,因此原始事件量的成長速度通常比預期慢,而儀表板讀取的彙總資料表,相較於原始資料表也很小。請實際測量,不要猜測:
docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhouse若要取得各資料表的數值,請使用 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 週再次取得。兩個數值即可算出成長率,而成長率能告訴你何時需要擴充 volume。 截至 2026 年 8 月,自架指南沒有記載原始事件的保留期限或 time-to-live 設定,因此請根據實際測得的成長率規劃磁碟容量,不要假設舊資料列會自行過期。
有一個刪除陷阱必須先了解。刪除網站或帳戶時,系統會將工作排入 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 執行前會列出三項成本。停機會造成實際損失:collector 停止期間嘗試送出的事件會遺失,因為 tracker 不會重試。回滾會遺失資料,因為 rollback.sh --to backups/<snapshot> 會完整取代兩個儲存區,並捨棄該 snapshot 建立後寫入的所有資料列。磁碟空間是第三項成本,也就是前文所述累積的 snapshot。
有兩項重新啟動規則很容易弄錯。先啟動 query gateway,再啟動 API,因為較新版的 API 會傳送舊版 gateway 不接受的查詢欄位。ClickHouse 則需要重新建立容器,而不是重新啟動,因為 docker compose restart 會重用容器原本的環境設定,並靜默忽略你所做的修改:
docker compose up -d --force-recreate clickhousedashboard 也有相同的陷阱。env/web.env 中的三個 NEXT_PUBLIC_* origin 會編譯到瀏覽器 bundle 中,並在容器啟動時代入。因此,dashboard 若呼叫錯誤的主機名稱,必須使用 docker compose up -d --force-recreate web 修正,不能使用 restart。web container 的 log 會列出啟動時採用的 origin,這是確認修正是否生效最快的方法。
如果 ClickHouse 在修改設定後拒絕啟動,請先查看其 log 的第一行。若該行以 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 主機上,容器會先啟動,接著 kernel 的 out-of-memory killer 會終止其中一個,通常是 ClickHouse。如果 1 GB 方案是硬性限制,請改用 GoatCounter 這類單一執行檔工具。它使用 SQLite,不需要外部資料庫。
使用 OpenAnalytics 需要 Cookie 橫幅嗎?
這是應諮詢律師的問題,但技術事實對你有利。系統不使用 cookie,訪客識別資訊是每日輪替的加鹽雜湊值,也不會儲存原始 IP 位址,因此不會寫入可持久識別訪客的資料。GDPR 仍規範你儲存哪些資料,以及保留多久。如果希望明確要求同意後才收集資料,請在 script tag 上設定 data-require-consent:追蹤器在取得同意前不會收集任何資料,並會將選擇儲存在 localStorage 的 oa.consent 下。
為什麼事件會回傳 202,卻始終不會出現在儀表板中?
202 表示 collector 已接受事件並將其加入佇列,不代表事件已儲存。worker 會將該佇列中的事件寫入 ClickHouse,因此請求成功但儀表板沒有資料時,問題通常出在 worker。讀取 docker compose logs --tail=50 worker,並監控 Valkey 佇列深度。佇列持續增長表示 worker 受阻,常見原因是 worker.env 中的 ClickHouse 憑證錯誤,或最近的 migration 建立了資料表,但缺少對該資料表的權限授予。
為什麼每個容器都正常時,儀表板仍是空的?
先檢查 env/api.env 中的 AUTH_TRUSTED_ORIGINS。它必須與儀表板的 origin 完全一致。如果不一致,API 不會傳送 CORS 標頭,因此瀏覽器會拒絕所有呼叫,結果只會看到正常運作的版面,卻沒有資料。第二項要檢查的是 env/web.env 中的 3 個 NEXT_PUBLIC_* 值。這些值會在 web container 啟動時替換。修正後必須執行 docker compose up -d --force-recreate web,因為單純重新啟動會保留舊值。
AGPL-3.0 會阻止我向客戶提供這項服務嗎?
不會,但它附帶一項條件。直接執行未修改的程式碼時,不需要向任何人提供額外內容。若修改程式碼,並以服務形式執行該修改版本供他人使用,就必須向這些使用者提供修改後的原始碼;公開 fork 即可滿足此要求。此外,OpenAnalytics 這個名稱並未與程式碼一併授權,因此你銷售的任何產品都需要使用自己的名稱。