OpenAnalyticsをVPSにセルフホストする方法
OpenAnalyticsの必要リソースを先に確認できます。ClickHouse、Postgres、Valkey、RAM 4 GB、空き容量25 GB、DNSレコード4件を準備し、インストール手順とディスクを圧迫する要因まで解説します。
ステップ 1 の前提条件
OpenAnalytics をセルフホストするには、約 4 GB の RAM、25 GB の空きディスク容量、Compose プラグイン付きの Docker、そして対象サーバーを指す DNS レコード 4 件が設定済みの Linux VPS が必要です。これが前提条件の正確な内容です。最初のコマンドを実行する前に確認してください。
このスタックは、アプリケーションサービス 6 個とデータストア 3 個で構成されます。Postgres は、アカウント、サイト、API キー、共有リンクなどのコントロールプレーン情報を保持します。ClickHouse は、ダッシュボードが読み取る生イベントとロールアップを保持します。Valkey は 2 つの用途で実行します。1 つは永続的なイベントキュー、もう 1 つは失われても問題ないキャッシュです。この 2 つの用途では、必要な削除ポリシーが正反対だからです。ClickHouse の読み取りを許可されるプロセスはクエリゲートウェイだけです。クエリゲートウェイは、各クエリエンベロープの Ed25519 署名を検証してから実行します。
単一のバイナリと 1 つの設定ファイルを想定している場合、この構成は適していません。このカテゴリで単一バイナリを採用する選択肢は GoatCounter です。Go の実行可能ファイル 1 つで動作し、デフォルトでは SQLite を使用し、外部データベースも必要ありません。この重いスタックでは、ファネル、Web Vitals、自分の 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 で確認してください。数分前に追加した名前でも、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 の都市データベースを取得します。これを省略すると、すべてのイベントの国情報が 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 にはドメイン名とイメージ参照が入ります。env/*.env にはサービスごとに 1 ファイルの Secret が入ります。docker-compose.override.yml には 3 組の Ed25519 鍵ペアが YAML の block scalar として入ります。複数行の PEM は env ファイルに格納できないためです。これらはすべて git-ignored であり、同じ値で再生成することはできません。
今すぐ、これらのファイルをマシンの外部へコピーしてください。失うと、それぞれ次の問題が発生します。
- ストアのパスワードを失うと、Postgres と ClickHouse に接続できなくなります。リセットできるのはコンテナ内部からだけです。
OA_CREDENTIAL_KEYRINGを失うと、保存済みのサードパーティ認証情報はすべて復元できません。Stripe アカウントを接続していた利用者は、再度接続する必要があります。ANONYMOUS_IDENTITY_SECRETを失うと、訪問者の識別が初期化されます。昨日までの訪問者もすべて新規として集計され、その切断はチャートに現れます。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 psmigrate は 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 としてインストールされます。
次に、全経路をエンドツーエンドで確認します。
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 許可リストを設定してはいけません。app. と api. は、サインインしたユーザーだけがアクセスできれば十分です。ダッシュボードはアプリケーション独自の認証で保護します。env/api.env の AUTH_PASSWORD_SIGNIN=enabled により、パスワードサインインはデフォルトで有効です。Google または GitHub のボタンは、そのプロバイダーのクライアント ID とクライアントシークレットの両方が存在する場合にだけ表示されます。マジックリンクにはメールトランスポートが必要です。これがない場合、API は送信処理を outbox に書き込むだけなので、メールは配信されず、エラーも発生しません。
ダッシュボードが動作するかどうかを決める設定が 1 つあります。env/api.env の AUTH_TRUSTED_ORIGINS は、ダッシュボードの origin と完全に一致していなければなりません。値が間違っているか未設定の場合、API は CORS(クロスオリジンリソース共有)ヘッダーを出力しません。ブラウザーはすべての呼び出しを拒否します。その結果、ダッシュボードはレイアウトだけを表示し、データを表示しません。一方、docker compose ps はすべて正常と報告します。
プロキシ設定を確認する際は、自動化されたトラフィックにも対処してください。クローラーも通常の訪問者と同じようにコレクターへアクセスし、そのページビューは ClickHouse と集計値に記録されます。サーバーで AI クローラーをブロックすると、データベースに書き込まれる前にその一部を除外できます。これにより、精度とディスク容量の両方への影響を抑えられます。
ここでいう cookieless の意味と、その代償
Cookie は使用しません。訪問者の識別情報にはソルト付きハッシュを使い、ソルトは毎日ローテーションします。元の IP アドレスは一切保存しません。地理情報は、ホスト上のディスクにある DB-IP ファイルをローカルで参照して解決するため、訪問者に関する照会がホスト外へ送信されることはありません。
この方式の利点は、訪問者のデバイスに識別子を永続化しないことです。これが、トラッカーを EU の ePrivacy における同意ルールの対象にする具体的な要因です。そのため、このような集計専用の構成は、同意バナーなしで運用されることが一般的です。ただし、保存するデータと保存期間には引き続き GDPR が適用されます。個別のケースについて判断するのは README ではなく、あなたの法務担当者です。
代償は、日をまたいだ識別ができないことです。ソルトがローテーションするため、月曜日に訪問した人が水曜日に再訪すると、設計上、回避策なしに 2 人の訪問者としてカウントされます。日次のユニーク訪問者数は正確です。週次および月次のユニーク訪問者数は日次の値から作成されるため、リーチを過大評価します。そのため、長期間を対象とする「リピーター数」は、ラベルが示す内容を測定しません。セッションと訪問経路は、同じ 1 日の範囲では信頼できます。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 か月で満杯になる理由
これがセルフホストの分析サーバーを停止させる原因です。通常、イベントデータ自体が原因ではありません。
まずイメージを確認します。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 はカラム型データを強く圧縮するため、raw イベントの容量は多くの人が予想するより緩やかに増加します。また、ダッシュボードが読み取るロールアップテーブルは raw テーブルに比べて小規模です。推測せずに測定します。
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 週目にもう一度測定します。2 つの測定値から増加率を算出でき、その増加率からボリュームの拡張が必要になる時期を判断できます。2026 年 8 月時点で、セルフホスティングガイドには raw イベントの保持期間や TTL を設定する項目が記載されていません。そのため、古い行が自動的に期限切れになると考えず、実測した増加率に基づいてディスク容量を設計します。
削除時に問題になりやすい落とし穴が 1 つあります。サイトまたはアカウントを削除すると、処理が 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.shupgrade.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 clickhousedashboard にも同じ種類の落とし穴があります。env/web.envにある3つの NEXT_PUBLIC_* origin はブラウザー用 bundle にコンパイルされ、コンテナ起動時に置換されます。そのため、誤った hostname を呼び出す dashboard は docker compose up -d --force-recreate webで修正し、restartでは修正できません。web コンテナのログには起動時に使用した origin が出力されます。修正が反映されたことを確認する最も早い方法です。
設定を編集した後に ClickHouse が起動しない場合は、ログの最初の行を確認してください。oa-entrypoint:で始まる行は、entrypoint が設定した値を拒否したことを示します。それ以外の場合は、通常、設定ファイルが無効な XML です。最も多い原因は、XML コメント内に二重ハイフンが含まれていることです。XML コメントでは二重ハイフンは使用できません。
AGPL-3.0 と名称
このコードは AGPL-3.0 でライセンスされています。変更せずに自分のサイトで実行するだけなら、公開に関する義務は一切発生しません。義務が発生するのは、コードを変更し、その変更版をネットワークサービスとして実行する場合です。この場合、ライセンスにより、そのサービスの利用者へ変更後のソースコードを提供する必要があります。自分のインスタンスでクライアントにダッシュボードを提供する場合も、販売する製品に組み込む場合も該当します。変更内容を公開フォークで管理すれば、それ以上の手続きは必要ありません。
ブランドはコードとは別のものです。「OpenAnalytics」という名称とプロジェクトのホストドメインは、開発者が運用するインスタンスを示すものであり、ライセンス許諾の対象には含まれません。デプロイした環境では、ブランドを付けずにソフトウェアを実行します。そのため、料金を支払う顧客に提供する前に、サービス独自の名称を設定してください。
FAQ
1 GB VPS で OpenAnalytics を実行できますか?
いいえ。プロジェクトには約 4 GB の RAM と 25 GB の空きディスク容量が必要です。1 つのデプロイメントで、Postgres、ClickHouse、2 つの Valkey インスタンスに加えて、6 つのアプリケーションサービスを実行するためです。ClickHouse だけでも軽量なプロセスではありません。1 GB のサーバーではコンテナが起動しても、その後カーネルの out-of-memory killer がコンテナのいずれかを停止させます。通常は ClickHouse が対象になります。1 GB プランが絶対条件であれば、外部データベースを使わず SQLite で動作する GoatCounter のような単一バイナリのツールを使用してください。
OpenAnalytics には cookie banner が必要ですか?
法律上の判断は弁護士に相談してください。ただし、技術的な事実は導入する側に有利です。cookie は使用されず、訪問者の識別情報は毎日ローテーションする salted hash です。また、生の 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 で作成されたテーブルに対する grant が不足していることです。
すべてのコンテナが正常なのに、ダッシュボードが空なのはなぜですか?
まず 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」という名称はコードとともにライセンスされるものではありません。そのため、販売するものには別の名称が必要です。