# chrome-devtools-mcp と headless Google Chrome を1つのイメージに入れ、2つの使い方を用意する。 # # 既定(profile なし): `mcp`。MCP の接続1本ごとにコンテナを1つ起こし、その中で MCP が # 自分の Chrome を起こす。ポートは開かず、相手からは見えない。抜けると全部消える。 # Hermes や nilkani06 の MCP 登録からは compose を介さず `docker run -i --rm ...` で起こす # (README)。compose から試すなら `docker compose run --rm -T mcp`。 # 任意(profile "shared"): `chrome`(常駐ブラウザ)と `mcp-shared`(それに相乗りする MCP)。 # ログイン状態を残したいときや、1本のブラウザを複数で共有したいとき用。相乗りの性質 # (相手のタブもクッキーも見える)は README の「常駐ブラウザに相乗りする」を読んでから。 name: chrome-docker services: mcp: build: context: . args: # どちらも空なら最新。固定したいときだけ .env に書く(Dockerfile 参照) MCP_VERSION: ${MCP_VERSION:-} CHROME_VERSION: ${CHROME_VERSION:-} image: chrome-docker:local stdin_open: true # PID 1 を docker-init にして、孤児の回収と SIGTERM の配達を任せる init: true # コンテナ1つ(MCP + Chrome 1本)の天井。1ページの V8 ヒープは CMD の --js-flags で # 512MB に抑えているが、それ以外(レンダラの数、ArrayBuffer、GPU プロセス)はここで止める mem_limit: 2g # 引数は Dockerfile の CMD(--headless --isolated --chromeArg=--no-sandbox ...)。 # ネットワークは既定の bridge: 外のサイトには出られ、ホストの 127.0.0.1 には届かない # (0.0.0.0 で待つホストのサービスには docker0 の IP 経由で届く。README「注意」)。 # この接続の Chrome の CDP をホストから使う: entrypoint.sh が CDP_PORT を見て Chrome に # --remote-debugging-port を足し、cdp-forward.js で外向きに出す。ホスト側の番号は Docker に # 選ばせる(127.0.0.1 だけ)。`docker compose run --rm -T --service-ports mcp` で公開され、 # 番号は `docker ps --filter label=chrome-docker.owner` か `docker port <名前> 9222` で見る。 environment: CDP_PORT: "9222" ports: - "127.0.0.1::9222" labels: chrome-docker.owner: compose # ---- 以下は任意(docker compose --profile shared ...)---- chrome: profiles: ["shared"] image: chrome-docker:local container_name: chrome-docker restart: unless-stopped # PID 1 を Chrome にしない。PID 1 は孤児になった子プロセスを回収する役を負うが # Chrome はそれをしないので、落ちたレンダラ等が zombie として残りうる。docker-init # (tini)を挟んで回収と SIGTERM の配達を任せる(`docker stop` が 10 秒待って SIGKILL # になる形も避けられる)。 init: true # 既定の ENTRYPOINT は MCP なので、ブラウザ役はここで差し替える。 # /usr/bin/google-chrome-stable(bash のラッパー)でなくバイナリを直接起こす。 # ラッパーは $HOME/.local/share/applications を作り stdout を cat に通すだけで、旗は足さない。 # puppeteer が channel "chrome" で起こすのもこのバイナリ。 entrypoint: ["/opt/google/chrome/chrome"] command: - --headless=new # Docker 既定の seccomp が名前空間付きの clone を止めるので Chrome の sandbox は # 使えない。Chrome 同梱の chrome-sandbox(setuid root)で非 root から起こしても # "Failed to move to new namespace ... Operation not permitted" で落ちる(2026-10-25 実測)。 # 代わりに非 root(Dockerfile の USER node)で動かし、CDP は 127.0.0.1 にだけ開ける。 - --no-sandbox # 共有メモリを /dev/shm でなく /tmp に置く。Docker の /dev/shm は既定 64MB で、 # Chrome が落ちる原因になる(代わりに shm_size を数 GB にする手もある)。 - --disable-dev-shm-usage - --disable-gpu - --no-first-run - --no-default-browser-check - --disable-background-networking - --disable-extensions - --disable-component-extensions-with-background-pages # レンダラ数の上限。1 だと全タブが1プロセスに乗り、片方のボットの重いページが # もう片方のタブまで巻き込んで落とす。2ボット+数タブなら 4(1つ増えるごとに # idle で数十MB)。ただしコンテキストが違うとレンダラを共有できないので固い上限ではない。 - --renderer-process-limit=4 # V8 ヒープの上限(レンダラごと)。256 では重い SPA が "Out of Memory" で落ちる # ことがあるので 512。全体の天井は下の mem_limit で押さえる。 - --js-flags=--max-old-space-size=512 # Chrome 本体(--headless=new)はこの旗を読まず、常に loopback にだけ束縛する # (chrome/browser/devtools/remote_debugging_server.cc)。意図の明示として残す。 - --remote-debugging-address=127.0.0.1 # host network なので内側と外側は同じ番号になる - --remote-debugging-port=${CDP_PORT:-9223} - --user-data-dir=/tmp/chrome-profile - --window-size=1440,900 - about:blank # ホストのネットワークをそのまま使う。Chrome は 127.0.0.1 にしか束縛しないので、 # ポート公開(docker-proxy はコンテナ側の IP に繋ぎに行く)では届かない。 # host network なら 127.0.0.1:${CDP_PORT} がホストとそのまま同じ場所になる。 # 代償: ブラウザで開いたページからホストの 127.0.0.1 のサービスにも届く(README「注意」)。 network_mode: host # ブラウザ1本の天井。超えると中で一番大きいプロセス(たいていレンダラ)が OOM で落ち、 # ブラウザ本体が落ちたときは restart で戻る。同じ箱に居るボット2体を巻き込まないため。 mem_limit: 3g # 常駐なので、ログを無限に溜めない(dbus の警告が起動ごとに出る) logging: driver: json-file options: max-size: "10m" max-file: "3" healthcheck: test: - CMD - node - -e - fetch('http://127.0.0.1:${CDP_PORT:-9223}/json/version').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1)) interval: 30s timeout: 5s retries: 3 start_period: 10s # 常駐の chrome に相乗りする MCP(`docker compose --profile shared run --rm -T mcp-shared`) mcp-shared: profiles: ["shared"] image: chrome-docker:local stdin_open: true # chrome は host network に居るので、compose のネットワークからは `chrome` の名前で # 引けない。さらに Chrome は Host ヘッダが IP か localhost でないと 500 を返す # ("Host header is specified and is not an IP address or localhost.")。 # こちらも host network に載せて 127.0.0.1 で繋ぐ。 network_mode: host entrypoint: ["chrome-devtools-mcp"] command: - --browserUrl - http://127.0.0.1:${CDP_PORT:-9223} # --headless / --isolated / --chromeArg は「MCP が Chrome を起こすとき」の引数で、 # 繋ぐ形(--browserUrl)では読まれない(BrowserManager.js の #connect)。付けても # 隔離にはならないので書かない。隔離は new_page の isolatedContext で取る(README)。 - --no-usage-statistics # 性能計測の URL を Google の CrUX API に送らない - --no-performance-crux depends_on: chrome: condition: service_healthy