SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-21

MoliをVPSで自己ホストする方法とCDP接続

小規模なVPSでヘッドレスChromeが重い場合に、MoliをインストールしてloopbackでCDPを公開し、エージェントやPlaywrightから接続する方法を説明します。Chromeが必要なページの見分け方も紹介します。

小規模な VPS に収まるヘッドレスブラウザー

Moli は AI エージェント向けのヘッドレスブラウザーです。ヘッドレス Chrome を実行できない VPS でも自己ホストできるほど小型です。Chromium のラッパーではなく、Rust で記述されたブラウザーエンジンです。自動化ライブラリがすでに使用しているプロトコルである Chrome DevTools Protocol (CDP) に対応します。1 つのバイナリをインストールして moli serve を実行し、Playwright または独自のエージェントコードから http://127.0.0.1:9222 に接続します。

インストールする前に、トレードオフを確認してください。プロジェクトは対応範囲を明確に示しています。GUI ブラウザー、GPU コンポジター、Chrome とのピクセル単位の互換性、高忠実度の Canvas やメディア再生には対応しません。これらを必要とするページは失敗します。Playwright で実行する実際の Chrome が引き続きフォールバックとなります。最後のセクションでは、どのページで Chrome が必要かを判断する方法を説明します。

以下のコマンドはすべて、2026 年 8 月に確認したプロジェクトの README と公開済みの skill ファイルから引用しています。グラフ内の数値はすべて、このサイトで測定した値ではなく、プロジェクトが自らのエンジンについて公開した値です。各グラフのキャプションにもその旨を記載しています。まだエンジンを選定中であれば、VPS 上でエージェント向けに使えるヘッドレスブラウザーの詳しい比較で代替手段を確認できます。

ヘッドレス Chrome はなぜこれほど多くのメモリを使用するのか

Chrome はマルチプロセスブラウザーです。各タブと各クロスサイト iframe には専用の renderer process が割り当てられ、各 renderer が独自の V8 heap と graphics buffer を保持します。この設計は、1つのタブがクラッシュしてもウィンドウ全体が巻き込まれないデスクトップ環境には適しています。2 GB VPS では、1回のブラウジング処理だけで、実行中のアプリケーション本体を上回るメモリを消費することがあります。

このプロジェクトでは、4つのエンジンを使用して、さまざまな公開 URL 192件をクロールし、結果を公開しました。

ChartMixed public web crawl, 192 URLs, figures published by the Moli project
The data behind this chart
[
  {
    "engine": "Moli",
    "useful_pages": 103,
    "median_rss_mib": 73
  },
  {
    "engine": "Chrome Headless",
    "useful_pages": 101,
    "median_rss_mib": 773
  },
  {
    "engine": "Lightpanda",
    "useful_pages": 85,
    "median_rss_mib": 40
  },
  {
    "engine": "Obscura",
    "useful_pages": 57,
    "median_rss_mib": 39
  }
]

Chrome Headless は有用なページを 101 件返し、Moli は 103 件返しました。このサンプルでは、2つのエンジンが Web 全体から読み取った割合はおおむね同じです。差が出るのはメモリ使用量です。Chrome の RSS(resident set size、プロセスが実際に RAM 上で保持しているメモリ)の中央値は 773 MiB でした。一方、Moli は 73 MiB でした。この傾向には信頼性があります。プロセスアーキテクチャから説明できるためです。ただし、実際のページで同じ比率になるとは限りません。

問題になるのは中央値ではありません。ピーク値です。2 GB のマシンでメモリが不足すると、kernel がプロセスを1つ選んで強制終了します。その記録は dmesg -T または journalctl -k に残ります。

Out of memory: Killed process 4211 (chrome) total-vm:2318936kB, anon-rss:1418324kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:3540kB oom_score_adj:0

エージェントはその行を認識できません。通常は page.goto: Page crashed などの Playwright エラー、または閉じられた target として、応答を停止したブラウザーを認識します。このエラーにはメモリ不足を示す情報が含まれません。そのため、小規模なマシンでエージェントがランダムに失敗する場合は、最初に OOM(out of memory)killer を確認します。ピーク値を基準にサイジングする作業は、エージェント VPS に必要な RAM と CPU を選ぶ作業と同じです。

Moli バイナリを特定のバージョンに固定してインストールする

このプロジェクトは、shell installer とビルド済み tarball を GitHub のリリースで公開しています。2026 年 8 月時点の最新リリースは 1.0.1 で、2026 年 8 月 18 日に公開されました。このガイドに記載したベンチマーク値は、プロジェクトが 0.1.1 で測定したものです。そのため、インストールするビルドの性能を保証する値ではなく、エンジンのおおまかな傾向として扱ってください。

バージョンを固定してください。常に latest を解決する installer を使うと、次回の再ビルドでエージェントが別のブラウザーエンジンへ移行します。ブラウザーの動作変更は、発生してから気付くのではなく、予定に組み込んで管理すべき変更です。

shell installer は手早く導入する方法です。ただし、実行する前に内容を読むことを推奨します。

curl --proto '=https' --tlsv1.2 -fsSL \
  -o /tmp/moli-installer.sh \
  https://github.com/lexmount/moli/releases/download/v1.0.1/moli-installer.sh
less /tmp/moli-installer.sh
sh /tmp/moli-installer.sh

実行する前にスクリプトを読んでください。短いスクリプトです。uname -m からアーカイブを選び、単一のバイナリを ~/.local/bin に展開します。x86_64 では moli-x86_64-unknown-linux-gnu.tar.gz を使用し、Arm サーバーでは aarch64 アーカイブを使用するため、Arm と x86 の両方の VPS プランに対応します。別の場所にインストールする場合は、MOLI_INSTALL_DIR を設定してください。バージョンの解決方法にも注意してください。スクリプトを取得した tag ではなく、最新リリースを解決します。最初に試す用途には問題ありませんが、再現可能な再ビルドには適していません。

そのため、恒久的に使用する場合は、installer と同じ処理を手動で行い、正確なアーカイブを自分で指定してください。これにより、system service からアクセスできる場所にバイナリを配置できます。また、ダウンロードしたスクリプトを shell にパイプする必要もありません。

cd /tmp
curl --proto '=https' --tlsv1.2 -fsSLO \
  https://github.com/lexmount/moli/releases/download/v1.0.1/moli-x86_64-unknown-linux-gnu.tar.gz
mkdir -p moli-pkg
tar -xzf moli-x86_64-unknown-linux-gnu.tar.gz -C moli-pkg --strip-components=1
sudo install -m 0755 moli-pkg/moli /usr/local/bin/moli
moli --version

moli --version で固定したバージョンが表示されれば、確認は完了です。installer の直後に moli: command not found となる場合は、インストールディレクトリが PATH に含まれていません。installer は、追加が必要なディレクトリを示す行を出力します。

moli fetch による単発の抽出

エージェントがブラウザーに指示する処理の多くは、「この URL を読み込み、内容を伝える」というものです。この場合、サーバーは必要ありません。moli fetch はエンジンを起動し、1 ページを読み込み、1 つの成果物を標準出力に書き込んで終了します。そのため、呼び出しの間にメモリを保持しません。

moli fetch --dump markdown --wait-until networkidle https://example.com
moli fetch --dump semantic_tree_text --wait-selector "main" https://example.com
moli fetch --dump json --wait-until networkidle https://example.com > page.json

最初のコマンドは、# Example Domain で始まるページを Markdown として出力します。Markdown はマークアップを省き、テキストを保持するため、モデルに渡すデータとして最も軽量です。semantic_tree_text はロールと構造を保持します。リンクが本文と同じくらい重要な、ナビゲーションの多いページで適しています。--dump json は HTTP ステータスとリクエストのトレースを含めます。取得結果が空で理由を確認したい場合に使用します。

待機戦略によって、コンテンツを取得できるか、空のシェルだけを取得するかが決まります。--wait-until networkidle はネットワーク通信が落ち着くと返ります。--wait-until domstable は DOM の変更が止まると返ります。バックグラウンドでポーリングを続け、通信がまったく落ち着かないページでは、こちらの方が適しています。--wait-selector は指定した 1 つのセレクターを待機します。取得対象のページについて情報を持つ唯一の戦略です。対象が分かっている場合に、最も信頼できます。

スクリーンショットと PDF には実際のレイアウトが必要です。レイアウトはデフォルトで無効になっています。

moli fetch --layout --dump screenshot https://example.com > page.png
moli fetch --layout --dump screenshot_full https://example.com > full-page.png
moli fetch --layout --dump pdf https://example.com > page.pdf

README では、デフォルトのレイアウトポリシーを LayoutPolicy::Mock としています。レイアウトと描画はブラウザー処理の中でも負荷が高いため、デフォルトではジオメトリを模擬し、何も描画しません。これが、上記のメモリ使用量が示す値になる理由です。また、空の PNG が生成される場合は、ページの破損ではなく --layout フラグの指定漏れであることがほとんどです。

エージェントが見つけた URL を、指定した URL としてではなく取得する場合は、--block-private-networks を追加します。ページ上で見つけたリンクをたどるエージェントは、クラウドインスタンスの認証情報を取得するために http://169.254.169.254/ へ誘導されたり、Web に公開する想定のない localhost 上のデータベースポートへ誘導されたりする可能性があります。このフラグはプライベートアドレス空間への移動を拒否し、--block-cidrs はさらに対象を絞ります。1 ページの読み取りではなくクロールを行う場合のパイプライン構成については、セルフホスト型 Firecrawl 代替ツールで説明しています。その前段階である URL の発見方法については、SearXNG を基盤とするエージェント向け検索スキルで説明しています。

CDP 経由でエージェントを Moli に接続する

多くの手順でページを移動してクリックするエージェントでは、代わりにサーバーを実行します。

moli serve --host 127.0.0.1 --port 9222

127.0.0.1 とポート 9222 がデフォルトなので、引数なしの moli serve でも loopback のみにバインドされます。ただし、永続的な設定では必ず両方を明示してください。次に service file を読む人がデフォルト値を覚えておく必要がなくなります。

クライアントを接続する前に、サーバーを確認します。

curl -s http://127.0.0.1:9222/json/version

正常なサーバーは webSocketDebuggerUrl フィールドを含む JSON オブジェクトを返します。この URL に CDP クライアントを接続します。curl: (7) Failed to connect to 127.0.0.1 port 9222: Connection refused は何も待ち受けていないことを示します。その場合は、サーバーを起動した端末を確認するか、サービスとして実行している場合は journalctl -u moli -n 50 を確認します。/json/list ではオープンなターゲットを一覧表示し、/json/protocol ではこの build が実装するドメインを一覧表示します。これにより、依存する CDP メソッドが利用できるか確認できます。

Playwright は独自にブラウザーを起動せず、その endpoint に接続します。

import { chromium } from "playwright";

const browser = await chromium.connectOverCDP("http://127.0.0.1:9222");
const context = browser.contexts()[0];
const page = context.pages()[0] ?? await context.newPage();

await page.goto("https://example.com");
console.log(await page.locator("body").innerText());

await browser.close();

重要なのは connectOverCDP であり、chromium.launch() ではありません。ここでは子プロセスとして Chromium を起動しないため、executablePath--no-sandbox などの一般的なコンテナ用フラグには作用対象がありません。同じ理由で、proxy、cookie、user-agent の設定は Moli サーバーの独自フラグとして指定します。Chrome protocol 全体ではなく、選択された CDP 機能だけが利用できると考えてください。明示的な unsupported-method エラーは、コードのバグではなくエンジンの対応範囲を示します。

エージェントが実行できる処理は、2 つのサーバーフラグで決まります。--layout は実際の geometry を有効にします。座標によるクリックとスクリーンショットにはこの機能が必要です。--resource はオプションの画像、フォント、メディアを取得します。ページを読み込むたびに帯域幅とメモリを消費するため、必要であることが確認できるまで無効にしてください。--profile-dir は実行間で cookie と storage を保持します。これを指定しない場合、各実行の状態は破棄されます。

ChartOne agent episode, Moli against Chromium, figures published by the Moli project
The data behind this chart
[
  {
    "engine": "Moli",
    "cdp_ready_ms": 34.85,
    "peak_pss_mib": 102.46,
    "processes": 1
  },
  {
    "engine": "Chromium",
    "cdp_ready_ms": 169.37,
    "peak_pss_mib": 348.82,
    "processes": 11
  }
]

プロジェクトのサンプルエージェント workload では、Moli は Chromium の 169.37 ms に対し、34.85 ms で CDP 接続を受け付けました。ピーク PSS(proportional set size。共有ページを共有するプロセス間で分割して数えるメモリ使用量)は、Chromium の 348.82 MiB に対し、102.46 MiB でした。構造上の違いは最後の列です。プロセス数は 11 に対し、1 です。1 プロセスであれば、systemd が監視する対象も、上限を設定する cgroup も 1 つで済みます。そのため、次のセクションは短くなります。

loopback で moli serve を systemd サービスとして実行する

エージェントが待機中のブラウザーを必要とする場合は、サーバーをサービスとして実行します。必要ない場合は、URL ごとに moli fetch を使い続けてください。アイドル状態のサーバーでもメモリを保持するためです。

ポート 9222 をパブリックインターフェイスで待ち受けさせないでください。CDP には、どのような認証手順もありません。そのポートに到達できる人は誰でもブラウザーを操作でき、ブラウザーから到達可能な内容を読み取れます。これには、プロファイルディレクトリ内の Cookie も含まれます。127.0.0.1 でのみ待ち受けさせてください。別のマシンから接続する場合は、SSH トンネル(ssh -L 9222:127.0.0.1:9222 user@your-vps)またはプライベート VPN インターフェイスを使用します。そのトンネルのエージェント側で、エージェントを http://127.0.0.1:9222 に接続させます。

サービスユーザーを作成してから、unit ファイルを作成します。

sudo useradd --system --home-dir /var/lib/moli --shell /usr/sbin/nologin moli

/etc/systemd/system/moli.service を記述します。

[Unit]
Description=Moli headless browser CDP server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=moli
Group=moli
ExecStart=/usr/local/bin/moli serve --host 127.0.0.1 --port 9222 --profile-dir /var/lib/moli/profile --block-private-networks
Restart=on-failure
RestartSec=2
StateDirectory=moli
MemoryAccounting=yes
MemoryMax=768M
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now moli.service
systemctl status moli.service
curl -s http://127.0.0.1:9222/json/version

systemctl status には active (running) が表示され、curl は discovery JSON を返します。ProtectSystem=strict は、この unit に対してファイルシステム全体を読み取り専用でマウントします。そのため、ここでは StateDirectory=moli が必須です。これにより、サービスユーザーが所有する /var/lib/moli が作成され、そのパスだけが書き込み可能になります。起動後に journalctl -u moli の権限エラーで終了する unit は、ほとんどの場合、ProtectSystem によって読み取り専用にされた場所へ書き込もうとしています。そのパスを state directory 配下へ移動してください。

MemoryMax=768M により、アプリケーションと同じホスト上で安全に実行できます。この unit には専用の cgroup が割り当てられます。その cgroup が制限値を超えると、カーネルは cgroup 内のプロセスを終了し、ホストの他の部分には影響を与えません。journal には次のように記録されます。

moli.service: A process of this unit has been killed by the OOM killer.

この行は、リソースサイズを決めるための情報として読み取ります。ページが想定より重いか、制限値が低すぎます。次のセクションで説明するように、自分のページを測定して値を設定してください。同じ accounting 用のフラグにより、ホスト上の他のサービスも制限できます。systemd でメモリと CPU を制限する ことで、他のサービスにも同じ仕組みを適用できます。

ピークメモリを自分で測定する

公開されている数値は、別のハードウェアと別のページを使った測定結果です。ピークメモリ使用量によって、サーバーが処理を継続できるかどうかが決まります。また、ピーク値は読み込む内容に完全に依存します。サイジングの前に測定してください。

単発の取得には time バイナリを使用します。同名の shell builtin よりも、はるかに多くの情報を出力します。

sudo apt update && sudo apt install -y time
/usr/bin/time -v moli fetch --dump markdown --wait-until networkidle https://example.com > /dev/null

出力の末尾にはリソース統計のブロックがあり、Maximum resident set size (kbytes) が含まれています。MiB に換算するには 1024 で割ります。example.com ではなく、実際にエージェントがアクセスする 10 ページに対して実行してください。平均値ではなく最大値を採用します。OOM killer はピーク値に反応するためです。

サービスについては、kernel がその cgroup に対して保持しているカウンターを読み取ります。

cat /sys/fs/cgroup/system.slice/moli.service/memory.peak
systemd-cgtop -m

memory.peak はバイト数です。unit の起動後に記録された high-water mark であるため、再起動するとリセットされます。この値を MemoryMax が上回るようにし、まだアクセスしていないページのうち最も重いものに備えて余裕も確保します。systemd-cgtop -m では unit ごとの現在の使用量を確認できます。現在、サーバー上でどのサービスが最もメモリを消費しているかを把握する最も速い方法です。

Moli はどこで失敗し、それでも Chrome が必要になるのはいつですか?

このプロジェクトでは、比較可能なブラウザー自動化タスク 1,308 件のベンチマークも実施し、複数のエンジンについてスコアを公開しています。

ChartLexbench headless browser suite, 1,308 tasks, figures published by the Moli project
The data behind this chart
[
  {
    "engine": "Chrome",
    "success_rate_pct": 99.85
  },
  {
    "engine": "Moli 0.1.1",
    "success_rate_pct": 81.88
  },
  {
    "engine": "Kitesurf",
    "success_rate_pct": 62.08
  },
  {
    "engine": "Lightpanda",
    "success_rate_pct": 53.29
  },
  {
    "engine": "Obscura",
    "success_rate_pct": 44.88
  }
]

これら 5 個のエンジンでは、Moli 0.1.1 がタスクの 81.88 パーセントを完了し、リファレンスエンジンである Chrome は 99.85 パーセントを完了しました。これはプロジェクト自身のテストスイートによる自己評価です。そのため、独立した結果ではなく、プロジェクト側の主張として読む必要があります。

実際の意味は単純です。Chrome なら完了できたタスクのおよそ 5 件に 1 件が、Moli では失敗しました。エージェントが管理下の固定されたページ群だけを訪問する場合、この比率から分かることはほとんどありません。ページが動作するかどうかは自分で確認でき、その日のうちに把握できるためです。エージェントがオープン Web を閲覧する場合は、設計上考慮すべき現実の失敗率になります。

失敗する対象は、プロジェクトが示す対象範囲から予測できます。

  • DOM ではなく Canvas 要素にインターフェースを描画するアプリケーション。Canvas の再現性は明示的に対象外です
  • WebGL または GPU コンポジットを必要とするもの。GPU コンポジターがないためです
  • DRM で保護された動画と、負荷の高いメディア再生
  • Chrome とのピクセル単位で正確なスクリーンショットを検証するビジュアルテスト。Chrome との互換性は目標ではありません

プロジェクトが示すもう 1 つの数値、つまり 1 回の完全な実行で 1.612 million 件の Web Platform Tests に合格したという結果は、標準仕様への対応範囲を示すものです。エージェントが訪問するサイトで動作することを保証するものではありません。十分に対応された標準仕様しか使っていないページでも、ボットチェックで失敗することがあります。どのエンジンスコアでも、その問題までは評価できません。

そのため、設計にはフォールバックを残してください。すべての URL をまず Moli に送ります。ページが空の状態で返る場合や、セレクターが表示されない場合は、その URL だけを Playwright で実際の Chrome を操作して再試行します。より大きなマシンを使うか、773 MiB のプロセスを許容できるスケジュールで実行します。ほとんどのエージェントは通常のページに大半の時間を費やすため、小規模なエンジンで大量処理を担い、高コストなエンジンで例外的なケースを処理します。

FAQ

Moli はエージェント向けのヘッドレス Chrome の代わりになりますか?

ページの読み取り、テキストの抽出、通常のクリックであれば、通常は代用できます。プロジェクト独自の 1,308 タスクのベンチマークでは、Chrome の 99.85 パーセントに対し 81.88 パーセントを完了しました。そのため、約 5 件に 1 件は Moli が対応していない処理を必要とします。既知の不足は、Canvas で描画されるアプリケーション、WebGL、DRM video です。該当する URL は、すべてを元に戻すのではなく、実際の Chrome に振り分けてください。

VPS で Moli を動かすには、どの程度の RAM が必要ですか?

プロジェクトの報告では、192-URL のクロール全体における RSS の中央値は 73 MiB、サンプルのエージェントエピソードにおける PSS のピークは 102.46 MiB でした。ヘッドレス Chrome の RSS 中央値は 773 MiB です。これらはプロジェクトのページに記載された数値です。単発利用では moli fetch の呼び出しを /usr/bin/time -v で囲んで測定してください。サービスでは /sys/fs/cgroup/system.slice/moli.service/memory.peak を確認し、確認した最大値を上回るように MemoryMax を設定します。

port 9222 をインターネットに公開しても安全ですか?

いいえ。CDP には認証がないため、その port に到達できる第三者はブラウザーを操作でき、ブラウザーから到達可能な情報をすべて読み取れます。--host 127.0.0.1 を維持し、別のマシンから SSH トンネルまたはプライベート VPN インターフェース経由でエンドポイントに接続してください。別のアドレスに bind する必要がある場合は、プライベートインターフェースに設定し、ファイアウォールでアクセスを制御します。

スクリーンショットが空になる、またはクリックしても何もない場所に移動するのはなぜですか?

レイアウトはデフォルトで無効です。README ではデフォルトのポリシーを LayoutPolicy::Mock としています。そのため、要素の geometry は実際のものではなく、ページ上のボックスに依存する処理は利用できません。moli serve --layout でサーバーを起動するか、--layoutmoli fetch に追加してください。これでスクリーンショットと座標を使う処理が動作します。画像が表示されない場合は別の flag で、--resource です。

どのバージョンの Moli をインストールすべきですか?

1 つのバージョンに固定し、使用したものを記録してください。2026 年 8 月時点の current release は 1.0.1 ですが、プロジェクトが公開しているベンチマーク値は 0.1.1 で測定されています。そのため、他者と結果を比較する際に両者を同じものとして扱うことはできません。その tag の moli-x86_64-unknown-linux-gnu.tar.gz をダウンロードし、shell installer に依存せず、自分で binary をインストールしてください。shell installer は取得した tag ではなく、最新の release を解決します。最後に moli --version で確認します。