SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor ・更新日 2026-10-06

VPSでヘッドレスブラウザを安定稼働させる設定ガイド

VPS上でChromiumを動かす際に発生する/dev/shm不足やサンドボックスエラー、フォント欠損などのトラブルを回避する方法を解説します。Playwrightのバージョン固定や共有ライブラリの適切な設定を行い、AIエージェントの実行環境を安定させるための具体的な手順をまとめました。

実行しているもの

VPS 上のヘッドレスブラウザとは、ウィンドウを持たない Chromium のことであり、人間ではなくコードによって操作されます。サーバー上では、エージェントがローカルソケット経由で通信する常駐プロセスツリーとして動作します。インストールは 1 つのコマンドで完了しますが、その後の作業が重要です。ブラウザがマシンから消費するリソースを制限し、制御用エンドポイントをパブリックインターネットから遮断しておく必要があります。

本ガイドは、ツール選定が完了し、運用フェーズにあることを前提としています。クローラーや抽出ツールの比較検討中であれば、まず セルフホスト可能な Firecrawl の代替案 を参照してから戻ってきてください。以下では Playwright の Chromium を使用します。Playwright は独自のブラウザビルドと依存関係インストーラーを備えているため、クリーンな Ubuntu VPS 上でもコンテナ内でも同じコマンドが機能します。バージョンは 2026 年 8 月時点のものです。

依存関係を推測せずに Chromium をインストールする

npm i -D playwright@1.62.0
npx playwright install --with-deps chromium

--with-deps は、Chromium に必要な共有ライブラリとフォントのために apt を実行し、必要に応じて root 権限を要求します。ブラウザのビルド自体は、コマンドを実行したユーザーの ~/.cache/ms-playwright にダウンロードされます。サーバー環境では、サービスを実行するユーザーとログインユーザーが異なることが一般的であるため、この点は重要です。管理者が sudo npx playwright install-deps chromium を使用してシステムパッケージを一度インストールし、インストールコマンドとサービスユニットの両方で PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers を設定して、1 つのコピーを共有するようにしてください。ブラウザを参照できないサービスは、検索したパスを示すエラーメッセージを出力して起動に失敗します。

Playwright のバージョンを固定してください。各リリースは特定のブラウザビルドと結びついているため、バージョンを固定しない npm update を使用すると、稼働中のサービスでブラウザが意図せず入れ替わる可能性があります。2026 年 8 月現在、Playwright 1.62 が最新です。

Chromium には 2 種類のビルドが存在し、これらは同一のプログラムではありません。デフォルトのダウンロードはヘッドレスシェルであり、これはヘッドレスモードでのみ動作する軽量なバイナリで、npx playwright install --with-deps --only-shell はこれのみをインストールします。フルブラウザは chromium チャネルで提供されるものであり、Playwright のブラウザドキュメント では「本物の Chrome ブラウザであり、より信頼性が高く、多くの機能を提供する」と説明されています。大量のデータ取得にはシェルを使用し、サイトの挙動が異なり原因調査が必要な場合にはフルブラウザを使用してください。

コンテナ内でヘッドレスブラウザがクラッシュする理由

Docker は各コンテナに 64 MB の /dev/shm を割り当てます。Docker のドキュメントには「サイズを省略した場合、システムは 64m を使用する」と明記されています。Chromium はレンダリングされたコンテンツをプロセス間でこの共有メモリ領域を介して受け渡すため、1 ページでも重いコンテンツがあると容量が不足します。その結果、レンダラーが終了し、クライアントは「ターゲットがクラッシュした」と報告します。ローカル環境では正常に動作するページでも発生します。変更を加える前に、コンテナ内部からサイズを確認してください。

df -h /dev/shm

これには 2 つの根本的な解決策があり、これらは併用するものではなく、どちらか一方を選択します。--ipc=host を使用すると、コンテナはホストの IPC 名前空間に入るため、ホストの /dev/shm を使用するようになります。これは通常、RAM の半分に相当します。Playwright の Docker ガイドではこの方法が推奨されています。これを行わないと「Chromium がメモリ不足でクラッシュする可能性がある」ためです。代償として、コンテナとホスト間の IPC 分離が失われます。--shm-size=1g を使用すると、プライベートな名前空間を維持したまま、マウントサイズを単純に拡張できます。

docker run --rm -it --init --ipc=host --user pwuser mcr.microsoft.com/playwright:v1.62.0-noble /bin/bash

フラグ --disable-dev-shm-usage は多くの検索結果で見つかる回答ですが、これは異なる動作をします。これらのファイルを /dev/shm から一時ディレクトリへ移動させるものです。もし /tmp がディスク上にある場合、クラッシュを回避する代わりに、レンダリング速度の低下とディスク書き込みの増加を招きます。もし /tmp が tmpfs であれば、データは制限なしで RAM 上に戻ります。これはブラウザが小規模な VPS のリソースを食いつぶす原因の一つです。/dev/shm を適切に設定してください。

--no-sandbox が実際にもたらす代償

Chromium は、Linux のユーザー名前空間上に構築されたサンドボックス内で各レンダラーを分離します。そのサンドボックスは、悪意のあるページとサーバーとの間の境界線です。サンドボックスが起動できない場合、Chromium は実行を拒否し、ログには以下のような行が記録されます。

Failed to move to new namespace: PID namespaces supported, Network namespace supported, but failed: errno = Operation not permitted

一般的なアドバイスとして --no-sandbox が挙げられます。Chromium 自身のセキュリティドキュメントでは、その代償について「Chromium の重要なセキュリティ機能を無効にするため、オープンな Web を閲覧する際には決して使用すべきではない」と断言しています。リンクを辿るエージェントは、定義上オープンな Web を閲覧していることになります。真の原因を特定してください。

ほとんどのケースにおいて、原因は 2 つに集約されます。ブラウザを root ユーザーで実行すると、既に保持している権限を放棄できないためサンドボックスが無効になります。これが、Playwright のイメージに pwuser という一般ユーザーが含まれている理由です。Ubuntu 24.04 以降では、AppArmor が非特権ユーザー名前空間を制限しており、提供されたプロファイルでカバーされていないパスにある Chromium バイナリは拒否されます。~/.cache/ms-playwright 配下にダウンロードされた Playwright は、まさにその該当パスです。両方を確認してください。

id -u
sysctl kernel.apparmor_restrict_unprivileged_userns
sudo dmesg | grep -i userns_create

sysctl からの 1 と、apparmor="DENIED" operation="userns_create" を含むカーネル行があれば、2 つ目の原因であることが確定します。/etc/apparmor.d/pw-chromium でそのバイナリのみを許可してください。これにより、サーバー上の他のすべてに対して制限を維持できます。

abi <abi/4.0>,
include <tunables/global>

profile pw-chromium /home/*/.cache/ms-playwright/*/chrome-linux/{chrome,headless_shell} flags=(unconfined) {
  userns,
}

sudo apparmor_parser -r /etc/apparmor.d/pw-chromium で読み込みます。このパスにはブラウザのリビジョンが含まれているため、Playwright をアップグレードするたびにパスが変更されます。上記のグロブ(ワイルドカード)指定であれば、その変更にも対応可能です。特定のパスに対して記述されたプロファイルは、更新後に一致しなくなり、無関係に見える更新の後でブラウザが再び失敗する原因となります。

スクリーンショットが空白や四角い枠だらけになる理由

スクリーンショットが空白になったり、空の四角い枠で埋め尽くされたりする場合、それはレンダリングのバグではなく、フォントの問題であることがほとんどです。install-deps は動作に必要なベースフォントを取得します。具体的には、fonts-liberation、fonts-freefont-ttf、fonts-noto-color-emoji、fonts-unifont、fonts-ipafont-gothic(日本語用)、fonts-wqy-zenhei(中国語用)、fonts-tlwg-loma-otf(タイ語用)です。このセットには Noto CJK が含まれていないため、韓国語やその他のスクリプトは fontconfig が見つけられるフォントにフォールバックします。推測するのではなく、fontconfig に直接問い合わせてください。

fc-match "sans-serif:lang=ko"
fc-match "sans-serif:lang=ar"
fc-list | wc -l

対象の言語が unifont に解決されるか、実際のグリフを持たないフォントにフォールバックされる場合は、fonts-noto-core と fonts-noto-cjk をインストールしてから、再度チェックを実行してください。Fontconfig は結果をキャッシュするため、フォントのインストール後はブラウザを再起動してください。フォントが全く存在しない状態で画像を出力すると、起動時に Fontconfig error: Cannot load default config file がログに記録され、すべてのページが空白でレンダリングされます。

ロケールとタイムゾーンはフォントとは別物であり、見た目だけでなくページの内容そのものを変化させます。コンテナでは通常、LANG が未設定で TZ が UTC になっているため、サイトは英語で表示され、タイムスタンプも UTC で出力されます。その結果、エージェントが報告する時刻は、現地の人が見ている時刻と一致しません。これらはマシン単位ではなくブラウザのコンテキスト単位で設定してください。そうすることで、1 つのブラウザで異なる地域のタスクを処理できるようになります。

const context = await browser.newContext({
  locale: 'en-GB',
  timezoneId: 'Europe/Paris',
});

ブラウザプロセスのリークがサーバーをスワップさせる理由

「ゾンビ」という名称は、性質の異なる2つの問題に対して使われます。真のゾンビプロセスとは、終了したものの親プロセスが wait() を呼び出していないプロセスのことです。これはPIDエントリを保持するだけでメモリを消費しません。コンテナ内でブラウザをPID 1として実行すると、PID 1にはデフォルトのreaper(回収機能)がないため、こうしたゾンビが発生します。Dockerの --init フラグは、シグナル転送とプロセス回収を行う小さなinitプロセスを実行することで、この問題を解決します。Composeでは init: true が同じ役割を果たします。

サーバーをスワップさせる原因となるリークは、これとは異なり、終了されずに残り続けたChromiumのライブプロセスです。これは、newContext() と close() の間でタスクが例外を投げた場合や、制御スクリプトが強制終了されてブラウザのプロセスツリーが孤立(オーファン化)した場合に発生します。最も深刻なのは、リクエストごとに新しいブラウザを起動するコードです。以下のコマンドで数を確認してください。

pgrep -c -f 'headless_shell|chrome'
ps -eo pid,ppid,rss,etime,comm --sort=-rss | head -20

このカウント値は、タスクの合間にアイドル時の値へ戻る必要があります。1日を通して数値が増加し続ける場合、修正すべきは起動フラグではなくコードです。finally ブロック内でコンテキストを閉じ、SIGTERM でブラウザを終了させ、1ヶ月間起動し続けるのではなく一定のタスク数ごとにブラウザを再利用してください。systemd環境下では、停止や再起動によってユニットのcgroup内の全プロセスが終了されるため、sudo systemctl restart browser.service は信頼できるリセット手段となります。ターミナルマルチプレクサ内で手動起動されたブラウザにはそのような保証はなく、セッション終了後も孤立したプロセスが生き残ります。

1 つのブラウザコンテキストに必要な RAM 容量

「1 つのブラウザ」は 1 つのプロセスではないため、質問は正確に行う必要があります。Chromium はブラウザプロセス、GPU プロセス、ユーティリティプロセス、そしてサイトごとに 1 つのレンダラープロセスを実行します。また、サイト分離機能により、サイトをまたぐ iframe にも個別のレンダラーが割り当てられます。BrowserContext は同じツリー内にある独立したクッキー保存領域およびストレージ領域であるため、2 つ目のコンテキストのコストは低いです。一方、2 つ目のページはレンダラープロセスを起動するためコストが高く、広告の多いページであれば複数のプロセスが起動します。

したがって、測定すべき数値は、自身のワークロードにおけるツリー全体のピークメモリ使用量です。他者のブログにある数値は、エージェントが開くページによって結果が左右されるため、ここでは役に立ちません。実際に使用するマシン上で、アクセス対象のサイトを用いて測定してください。

sudo systemd-run --unit=browser-probe -p MemoryMax=2G -p MemorySwapMax=0 -p WorkingDirectory=/srv/agent /usr/bin/node worker.js
systemctl status browser-probe

Ubuntu 24.04 では、出力内の Memory: 行がそのユニットの現在およびピーク時の使用量を報告します。ワーカーを一度に 1 ページずつ実行してピーク値を記録し、次に 2 ページ開いて 2 ページ目の真のコストを確認してください。同時実行数は算術的に算出します。合計 RAM からシステムが必要とする分を差し引き、数百 MB の余裕を持たせた上で、測定したワーカーあたりのピーク値で割ります。基盤となるマシンのサイジングについては、エージェント用 VPS に必要な RAM と CPU 容量を参照してください。

この制限値は 2 箇所で適用します。コード内では固定のワーカープールまたはセマフォを使用し、エージェントのリクエストが急増した際にブラウザを次々と起動するのではなく、キューイングするようにします。OS レベルでは cgroup 制限を使用し、キュー内のバグがマシン全体を停止させないようにします。

[Service]
MemoryMax=2G
MemorySwapMax=0
TasksMax=512
Restart=always

MemorySwapMax=0 は見た目以上に重要です。これがないと、cgroup は制限に達した際にページをスワップへ追い出します。その結果、マシンは稼働し続けますがすべてのリクエストが低速化し、明確な障害よりも診断が困難になります。これがあれば、カーネルは cgroup 内のブラウザツリーを強制終了し、systemd がユニットを再起動するため、sshd は生存し続けます。Compose における同様の制御は mem_limit、shm_size、init であり、Docker Compose でのメモリ制限設定で解説しています。

ブラウザのエンドポイントをパブリックインターネットから隔離する

Playwrightはブラウザをサーバーとして実行し、エージェントにWebSocket URLを渡すことができます。

const { chromium } = require('playwright');
const server = await chromium.launchServer({ port: 3000 });
console.log(server.wsEndpoint());

このエンドポイントには認証機能がありません。PlaywrightのAPIドキュメントには「wsPathを知っているプロセスやWebページ(Playwrightで実行中のものを含む)は、OSユーザーの権限を乗っ取ることができる」と明記されています。デフォルトのホストはlocalhostであり、「ループバックインターフェースからの接続のみを受け入れる」仕様ですが、ドキュメントでは0.0.0.0のように明示的なアドレスを指定すると「リッスンしているポートに到達可能なあらゆるものに対してブラウザのRPCを公開してしまう」と警告しています。Chrome独自の--remote-debugging-portはさらに危険です。DevToolsプロトコルには一切の認証がなく、完全にループバックへのバインドに依存しています。

実際に何を公開しているかを確認してください。VPSからだけでなく、別のマシンからもチェックを行います。

ss -ltnp

0.0.0.0にバインドされているブラウザポートがあれば、それは脆弱性です。ほとんどのプロバイダーはコントロールパネルで個別のネットワークファイアウォールを提供しており、ufwの設定はそれを認識できない点に注意してください。エンドポイントへは、SSHトンネルやプライベートVPNを経由して別のマシンからアクセスするようにします。

ssh -N -L 3000:127.0.0.1:3000 you@your-vps

ここでのリスクは、ブラウザの実行時間を盗まれること以上のものです。操作可能なブラウザは、ネットワーク内部に設置されたリクエスト偽造マシンとなります。そのソケットに到達した攻撃者は、http://127.0.0.1:8080やデータベースの管理ページ、あるいは169.254.169.254にあるクラウドメタデータアドレスへアクセスさせ、その応答を読み取ることが可能です。ファイアウォールからはVPS自身からのリクエストに見えるため、許可されてしまいます。制御用エンドポイントは、そのサーバーのシェルアクセスと同等の権限を持つものとして扱ってください。

MCPサーバーも同様の構造です。npx @playwright/mcp@latest --headless --port 8931はlocalhost上のHTTPで提供され、--host 0.0.0.0はローカルツールをパブリックなものに変えてしまうフラグです。プロジェクトのREADMEには、Playwright MCPは「セキュリティ境界ではない」と明記されています。ポートはループバックに留め、エージェントには同じトンネル経由でアクセスさせてください。

エージェントが読み込むページは信頼できない入力です

オープンなウェブを閲覧するエージェントは、見知らぬ人物が書いたテキストを、あなたの指示も保持しているモデルに供給します。ページにはモデル宛てのテキストが含まれている可能性があり、タスクの放棄、ツールの呼び出し、あるいは特定のURLへのデータ送信を指示される恐れがあります。モデルは両方をテキストとして受け取るため、ページの言葉とあなたの指示を確実に区別する方法はありません。悪意のあるページが利用できる情報を最小限に抑える構成を設計してください。

  • ブラウザは専用のOSユーザーで実行し、環境内にSSHキーやクラウドの認証情報を配置しないでください。
  • タスクごとに新しいコンテキストを使用し、--isolated と Playwright MCP を組み合わせることで、あるサイトのセッションが次のページに引き継がれないようにしてください。
  • ジョブで許可されている場合は、オリジンの許可リストを維持してください。Playwright MCP は --allowed-origins および --blocked-origins をセミコロン区切りのリストとして受け取ります。
  • メールの送信や金銭の支払いなど、状態を変更するアクションの前には、必ず人間による確認ステップを要求してください。

さらに良い方法として、ブラウザ全体を破棄して再構築可能なマシン上で実行してください。これは 使い捨てのVMでコーディングエージェントを実行する ことと同じ論理です。エージェントの本来の仕事が自由なブラウジングではなく検索である場合、フルブラウザよりも限定的なツールの方が安全です。独自の SearXNG をバックエンドにした検索スキル を使用すれば、悪意のあるページを読み込むことなく結果を取得できます。

FAQ

なぜ Chromium は Docker 内でクラッシュし、同じ VPS 上で直接実行すると正常に動作するのですか?

コンテナのデフォルトの /dev/shm が 64 MB しかなく、ホスト側はそれよりはるかに大きいためです。Chromium はレンダリングしたコンテンツをこの共有メモリ領域経由で渡すため、重いページを開くと領域が不足し、レンダラーが終了します。コンテナ内で df -h /dev/shm を実行して確認し、ホストの共有メモリを使用する --ipc=host、またはコンテナ自身の共有メモリを拡張する --shm-size=1g のいずれかで起動してください。--disable-dev-shm-usage は問題を /tmp に移動させるだけです。

VPS で他のプログラムを動かしていない場合、--no-sandbox は安全ですか?

いいえ。サンドボックスは悪意のあるページがマシン全体に影響を及ぼすのを防ぐためのものであり、Chromium のドキュメントには「Chromium の重要なセキュリティ機能を無効にするため、オープンな Web を閲覧する際には決して使用すべきではない」と明記されています。リンクを辿るエージェントはオープンな Web を閲覧しているのと同じです。根本的な原因を解決してください。ブラウザを root で実行せず、Ubuntu 24.04 の場合はブラウザのバイナリパスに対して userns, を含む AppArmor プロファイルを適用し、そのプログラムに対してのみ非特権ユーザー名前空間を許可してください。

小規模な VPS で何個のブラウザを同時に実行できますか?

数値を鵜呑みにせず、測定してください。Chromium はサイトごとに 1 つのレンダラープロセスを起動するため、開くページによって結果は異なります。MemoryMax を設定した状態で systemd-run の下でワーカーを 1 つ実行し、systemctl status の Memory: 行からピーク時のメモリ使用量を読み取ります。その値を空き RAM 容量で割り、余裕を持たせてください。コード内にキューを実装し、ユニットファイルに MemoryMax を設定して、この制限を二重に適用してください。これにより、リクエストが急増してもマシンがスワップに陥るのではなく、待機するようになります。

エージェントは別のマシンからブラウザに接続できますか?

はい。ただし、ポートを 0.0.0.0 にバインドしてはいけません。Playwright サーバーのエンドポイントと Chrome DevTools ポートは、到達可能なすべてのクライアントをパスワードなしで受け入れてしまいます。リスナーは 127.0.0.1 に固定し、SSH トンネルまたはプライベート VPN 経由で接続してください。サーバー上で ss -ltnp を実行して確認し、外部からのポートチェックを行い、プロバイダー側のネットワークファイアウォール設定も確認してください。

ページは読み込まれているはずなのに、スクリーンショットが真っ白になるのはなぜですか?

フォントが不足しているためです。ページ内のスクリプトに対応するフォントがないと、テキストは空のボックスとしてレンダリングされるか、全く表示されず、画像が少ないページでは真っ白に見えます。スクレイピングする言語ごとに fc-match "sans-serif:lang=ko" を実行し、汎用的なフォールバックが必要な場合は fonts-noto-core や fonts-noto-cjk をインストールしてください。その後、fontconfig のキャッシュを再読み込みさせるためにブラウザを再起動します。フォントが全くないコンテナでは、起動時に Fontconfig error: Cannot load default config file がログに出力されます。