SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

OpenAnalyticsをVPSにセルフホストする方法

OpenAnalyticsに必要な実容量を先に確認できます。ClickHouse、Postgres、Valkey、RAM 4 GB、空き容量25 GB、4件のDNSレコードを準備し、インストール後にディスクを埋める要因も解説します。

手順 1 の前に確認する構成

OpenAnalytics をセルフホストするには、約 4 GB の RAM、25 GB の空きディスク容量、Compose plugin を備えた Docker が動作する Linux VPS と、あらかじめそのサーバーを指す 4 件の DNS レコードが必要です。これが正確な前提条件です。最初のコマンドを実行する前に示すべき内容であり、実行後に説明するものではありません。

このスタックは、6 つのアプリケーションサービスと 3 つのデータストアで構成されます。Postgres は、アカウント、サイト、API keys、共有リンクなどのコントロールプレーンを保持します。ClickHouse は、生イベントとダッシュボードが参照する集計データを保持します。Valkey は 2 つの用途で実行します。1 つは永続的なイベントキュー、もう 1 つは失われても問題のないキャッシュです。この 2 つの用途では、必要な eviction policy が正反対になるためです。ClickHouse の読み取りを許可されるプロセスは query gateway だけです。query gateway は、各 query envelope の実行前に Ed25519 署名を検証します。

1 つのバイナリと 1 つの設定ファイルだけを求めている場合、この構成は適していません。このカテゴリで単一バイナリを採用する選択肢は GoatCounter です。Go の実行ファイル 1 つで動作し、デフォルトでは SQLite を使用するため、外部データベースは不要です。この大規模なスタックを採用すると、ファネル、Web Vitals、自分の Stripe アカウントによる収益アトリビューション、MCP (model context protocol) server を利用できます。セルフホスト型分析ツールの選び方では、このトレードオフを比較しています。このガイドでは、すでに選択を終えていることを前提とします。

まず 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 -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 を先にバックアップする

先に進む前に、生成された Secret をバックアップしてください。

ジェネレーターは3つのものを書き出します。.env にはドメイン名とイメージ参照が入ります。env/*.env には、サービスごとに1つの Secret ファイルが入ります。docker-compose.override.yml には、YAML のブロックスカラーとして2組の Ed25519 キーペアが入ります。1組はクエリの署名用、もう1組はリアルタイムトークン用です。複数行の PEM は env ファイルに格納できないためです。各キーは必要なサービスだけに渡されます。API には両方の秘密鍵を渡します。クエリゲートウェイにはクエリ公開鍵を渡します。リアルタイムサービスにはトークン検証鍵を渡します。これらはすべて Git の追跡対象外であり、同じ値を再生成することはできません。

今すぐ、これらのファイルをマシンの外部へコピーしてください。失うと、次の問題が発生します。

  • ストアのパスワードを失うと、Postgres と ClickHouse にアクセスできなくなります。リセットできるのはコンテナ内部からだけです。
  • OA_CREDENTIAL_KEYRING を失うと、保存されたサードパーティー認証情報はすべて復元できません。Stripe アカウントを接続していた利用者は、再度接続する必要があります。
  • ANONYMOUS_IDENTITY_SECRET を失うと、訪問者 ID の基準がリセットされます。前日までの訪問者もすべて新規訪問者として扱われ、グラフにもその切れ目が現れます。
  • AUTH_SECRET を失うと、すべてのセッションが無効になります。そのため、全員が再度サインインする必要があります。
  • 署名用の秘密鍵を1つ失っても、キーペアをローテーションすれば済みます。データは失われません。

2つの Secret は、それぞれ2つのファイルでバイト単位に同一でなければなりません。ANONYMOUS_IDENTITY_SECRET は collector.env と worker.env にあります。collector が訪問者ハッシュを計算し、worker がそのハッシュを書き込むためです。OA_CREDENTIAL_KEYRING は api.env と worker.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 のスキーマを適用して終了するため、migrate コンテナが停止した状態が正しい最終状態です。tracker-build は oa.js を Caddy が配信するボリュームにコンパイルして、同様に終了します。それ以外のサービスは、docker compose ps 内で healthy を読み取れる状態になるはずです。サービスがループして再起動する場合、ほとんどは環境変数の検証に失敗しています。ログには、再起動のたびに1件ずつではなく、すべての問題が1つの一覧として出力されます。よくある原因は2つあります。1つは変数が空欄のままになっていることです。この場合、未設定として扱われず拒否されます。もう1つは、Secret を誤ったサービスファイルに配置していることです。

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

ビルドにはおよそ10分かかります。Pull は数分で完了します。そのため、リリースイメージが用意されています。

最初のアカウントを直ちに登録する

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 としてインストールされます。同じサイトが onion サービスとしても応答する場合は、そのビルドにタグを含めないでください。c.example.com から取得するスクリプトによって、Tor Browser の訪問者が clearnet に戻され、同じページの読み込み中に 2 つのアドレスが関連付けられるためです。

次に、経路全体を最初から最後まで確認します。

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 で追加されたテーブルに対する grant が不足していることです。

コレクターは公開し、ダッシュボードは認証で保護する

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. にアクセスできる必要があります。そのため、この 2 つの前段に basic 認証や IP allowlist を設定しないでください。app. と api. は、サインインしたユーザーだけがアクセスできれば十分です。ダッシュボードはアプリケーション独自の認証で保護します。パスワードによるサインインは、env/api.env の AUTH_PASSWORD_SIGNIN=enabled によりデフォルトで有効です。Google または GitHub のボタンは、そのプロバイダーの client ID と client secret が両方存在する場合にのみ表示されます。マジックリンクにはメールトランスポートが必要です。設定されていない場合、API は送信要求を outbox に書き込むだけなので、配信されず、エラーも発生しません。他のセルフホストアプリをすでに単一の Authentik ログインの背後で運用している場合は、このダッシュボードもそこに参加させるか、独自のアカウントを維持するかを早めに決めてください。ここで最初に作成したアカウントが、恒久的に特権アカウントになるためです。

ダッシュボードが動作するかどうかを決める設定が 1 つあります。env/api.env の AUTH_TRUSTED_ORIGINS は、ダッシュボードの origin と完全に一致していなければなりません。値が間違っているか未設定の場合、API は CORS(cross-origin resource sharing)ヘッダーを出力しません。ブラウザーはすべての呼び出しを拒否し、ダッシュボードはレイアウトだけを表示してデータを表示しなくなります。一方、docker compose ps はすべて正常と報告します。

プロキシ設定を変更する際は、自動化されたトラフィックにも対処してください。クローラーも通常の訪問者と同じようにコレクターへアクセスし、そのページビューは ClickHouse と集計値に記録されます。サーバーで AI クローラーをブロックすることで、精度とディスク容量に影響する前に、その一部をデータベースから除外できます。

Cookie は使用しません。訪問者の識別情報にはソルト付きハッシュを使い、ソルトは毎日ローテーションします。生の IP アドレスは保存しません。ジオロケーションは、ホスト自身のディスク上にある DB-IP ファイルを使ってローカルで解決するため、訪問者に関する照会がホストの外部に送られることはありません。照会をローカルに限定しても、データ自体がなくなるわけではなく、外部のサービス提供者がなくなるだけです。これは、独自の SearXNG インスタンスを運用する場合に、検索エンジンから見えるものがサーバーの IP になるのと同じ制約です。

この方式で得られるのは、訪問者の端末に識別子を保存しないことです。これが、トラッカーを EU の ePrivacy における同意ルールの対象にする要素です。そのため、このような集計専用の構成は、同意バナーなしで運用されることが一般的です。ただし、保存するデータと保存期間には GDPR が適用されます。自分のケースを判断するのは README ではなく、依頼している法律顧問です。

代償は、日をまたいだ識別ができないことです。ソルトがローテーションするため、月曜日に訪問した人が水曜日に再訪すると、設計上、別の訪問者として数えられます。これを回避する方法はありません。日次のユニーク訪問者数は正確です。週次および月次のユニーク訪問者数は日次の値から作成されるため、リーチを過大評価します。したがって、長期間を対象にした「リピーター」の値は、ラベルどおりのものを測定していません。セッションと訪問経路は、同じ日の範囲内では信頼できます。ANONYMOUS_IDENTITY_SECRET のローテーションにも日付の境界と同じ効果があるため、このローテーションは通常の衛生管理ではなく、データの変更として扱ってください。

コレクターは Do Not Track と Global Privacy Control に対応しています。これは、個人データを販売または共有しないようサイトに指示するブラウザー信号です。スクリプトタグには、同じ目的の切り替えも用意されています。data-respect-gpc、data-respect-dnt、data-require-consent は、同意が得られるまで収集を停止し、回答を localStorage に oa.consent というキーで保存します。data-storage="none" を設定すると、ブラウザーのストレージを完全に無効にできます。

ディスクが 6 か月でいっぱいになる理由

これが self-hosted の分析サーバーを停止させる原因です。通常、イベントデータ自体が主な理由ではありません。

まずイメージを確認します。1 回のリリースで 10 個のイメージが公開され、ディスク上で合計約 13 GB を使用します。アップグレードでは古い世代を削除する前に新しい世代を取得するため、一時的に 2 世代を保持します。ページビューが 1 件もない段階で、必要な 25 GB の大部分を消費します。

次にスナップショットです。snapshot.sh はスタックを停止し、2 つのデータボリュームをすべての Secret とともにアーカイブしてから再起動します。ここではコールドコピーだけが安全です。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

テーブルごとの容量を確認するには、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 つの測定値があれば増加率を算出でき、増加率からボリュームの拡張が必要になる時期を判断できます。2026 年 10 月時点で、self-hosting guide には生イベント用の retention または time-to-live の設定項目が記載されていません。そのため、古い行が自然に期限切れになると想定せず、測定した増加率に基づいてディスク容量を決めてください。

削除処理には、問題になる前に知っておくべき注意点があります。サイトまたはアカウントを削除すると、処理が worker にキューイングされます。この worker には CLICKHOUSE_MAINTENANCE_USER と CLICKHOUSE_MAINTENANCE_PASSWORD が設定されており、ClickHouse に対応する oa_maintenance ユーザーが存在している必要があります。これらがないと、削除処理は永久にキューに残ります。サイトはダッシュボードから消えますが、すべての行はディスク上に残ります。そのため、削除が完了したように見えても、空き容量はまったく増えません。

アップグレードと3つのコスト

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

upgrade.shは実行前に3つのコストを表示します。ダウンタイムは実際に発生します。collector の停止中に試行されたイベントは、tracker が再試行しないため失われます。ロールバックではデータが失われます。rollback.sh --to backups/<snapshot>は両方のストアをスナップショットで完全に置き換え、そのスナップショット取得後に書き込まれたすべての行を破棄するためです。3つ目のコストはディスクです。これは前述したスナップショットの蓄積です。

2つの再起動ルールは間違えやすいものです。API より先に query gateway を起動してください。新しい API は、古い gateway が拒否する query フィールドを送信するためです。また、ClickHouse には再起動ではなく再作成が必要です。docker compose restartはコンテナ作成時の環境を再利用し、編集内容を黙って無視するためです。

docker compose up -d --force-recreate clickhouse

dashboard にも同じ落とし穴があります。env/web.env内の3つの NEXT_PUBLIC_* origin はブラウザバンドルにコンパイルされ、コンテナ起動時に置換されます。そのため、dashboard が誤った hostname を呼び出す場合は docker compose up -d --force-recreate webで修正し、restartは使用しません。web コンテナのログには起動時の origin が出力されるため、修正が反映されたことを確認する最も早い方法です。

設定を編集した後に ClickHouse が起動しない場合は、ログの最初の行を確認してください。oa-entrypoint:で始まる行は、設定した値を entrypoint が拒否したことを示します。それ以外の場合は、通常、設定ファイルが不正な XML であることを意味します。最も多い原因は XML コメント内の二重ハイフンです。XML コメント内では使用できません。

AGPL-3.0 と名称

コードは AGPL-3.0 でライセンスされています。ただし、1 つ例外があります。v0.7.0 以降、ブラウザトラッカー、apps/tracker/ 配下のすべて、およびそれがビルドする oa.js は MIT です。そのため、自分のページに貼り付けるタグに AGPL の条件は適用されません。変更せずに自分のサイトで実行するだけなら、公開義務は一切発生しません。義務が発生するのは、コードを変更し、その変更版をネットワークサービスとして実行する場合です。この場合、ライセンスにより、そのサービスの利用者へ変更後のソースコードを提供する必要があります。これは、自分のインスタンスでクライアントにダッシュボードを提供する場合にも適用されます。また、販売する製品に組み込む場合にも適用されます。変更内容を公開 fork に保持すれば、それ以上の手続きは不要です。

ブランドはコードとは別です。「OpenAnalytics」という名称とプロジェクトのホストドメインは、作者が運用するインスタンスを識別するものであり、ライセンス許諾の対象ではありません。自分のデプロイでは、ブランドを付けずにソフトウェアを実行します。そのため、料金を支払う顧客に提供する前に、サービス独自の名称を付けてください。

FAQ

OpenAnalytics を 1 GB の VPS で実行できますか?

いいえ。プロジェクトでは、約 4 GB の RAM と 25 GB の空きディスク容量が必要です。1 回のデプロイで、Postgres、ClickHouse、2 つの Valkey インスタンスとともに、6 つのアプリケーションサービスを実行するためです。ClickHouse だけでも軽量なプロセスではありません。1 GB の環境ではコンテナが起動しても、その後カーネルの out-of-memory killer によって、通常は ClickHouse のいずれかが停止されます。1 GB プランが絶対条件であれば、外部データベースを使わず SQLite で動作する GoatCounter のような単一バイナリのツールを使用してください。

これは弁護士に相談すべき問題ですが、技術的な事実は有利です。cookie は使用されず、訪問者の識別情報は毎日ローテーションする salt 付きハッシュです。raw IP アドレスも保存されないため、訪問者を識別する永続的な情報は書き込まれません。GDPR は、何を保存するか、どの期間保持するかを引き続き規制します。収集を明示的な同意後に限定する場合は、script tag に data-require-consent を設定してください。tracker は同意が得られるまで何も収集せず、回答を oa.consent の下にある localStorage に保持します。

イベントが 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 コンテナの起動時に置換されます。値を修正するには docker compose up -d --force-recreate web が必要です。通常の再起動だけでは古い値が保持されます。

AGPL-3.0 により、これをクライアントに提供できなくなりますか?

いいえ。条件が 1 つ付くだけです。コードを変更せずに実行する場合、誰かに何かを提供する義務はありません。コードを変更し、その変更版を他の人が利用するサービスとして実行する場合は、その利用者に変更後のソースコードを提供する必要があります。公開 fork でこの要件を満たせます。なお、「OpenAnalytics」という名称はコードとともにライセンスされないため、販売するものには別の名称が必要です。