1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VPSにPlaywrightをDockerで入れたら、第四の腕が生えた

1
Last updated at Posted at 2026-03-19

n8n と Claude Code で自動化を進めていたら、だいたいのことは手が離れた。

API があるサービスは n8n が叩く。ファイル操作は Claude Code がやる。定期処理は cron が回す。ところが一つだけ、どうしても手作業が残る場所があった。ログインが必要な Web サイトの情報取得だ。

会員ページの更新確認。管理画面の数値チェック。API が用意されていないサービスの巡回。これらは毎回ブラウザを開いて、ログインして、目で見て、コピペしていた。自動化していると豪語しておいて、実はここだけアナログ。正直、気持ち悪かった。

VPS に Playwright を Docker で突っ込んだら、この最後の手作業が消えた。

なぜ Playwright か

ヘッドレスブラウザといえば Puppeteer が定番だった。自分も最初はそっちを考えた。ただ調べてみると、Playwright のほうが後発ゆえに痒いところに手が届く。

決め手になったのは 3 つ。

  • Auto-wait: 要素が読み込まれるまで自動で待つ。JS が重いサイトでも sleep(3000) みたいな雑な待機が要らない
  • getByRole / getByLabel: CSS セレクタではなく「ボタン」「ラベル」で要素を指定できる。サイトのデザインが変わってもスクリプトが壊れにくい
  • storageState: 認証 Cookie をファイルに保存して再利用できる。一度手動でログインすれば、次からは自動巡回

Puppeteer でもやれなくはない。ただ Playwright はこれが全部標準で入っている。「できるか」ではなく「楽にできるか」で選んだ。

Docker で隔離する理由

VPS にはすでに十数個のコンテナが動いている。n8n、MT4、その他もろもろ。ここにブラウザエンジンを直接入れると、ホストの依存関係が汚れる。Chromium のライブラリ群は重いし、バージョン管理も面倒だ。

Docker なら docker compose up -d で立ち上がり、docker compose down で跡形もなく消える。ホストには何も残らない。

docker-compose.yml のポイント

services:
  playwright:
    build: .
    shm_size: '1gb'
    network_mode: host
    volumes:
      - ./scripts:/app/scripts
      - ./output:/app/output
      - ./auth:/app/auth
    env_file:
      - .env

2 つだけ注意点がある。

shm_size: '1gb' — Chromium は共有メモリ(/dev/shm)を大量に使う。デフォルトの 64MB だとタブがクラッシュする。1GB あれば安定する。これを忘れると「なぜか途中で落ちる」という地味なハマりが待っている。

network_mode: host — コンテナからホストのネットワークをそのまま使う。ポートマッピングの設定が不要になるし、n8n など他コンテナとの通信もシンプルになる。セキュリティ的にはブリッジのほうが硬いが、VPS 内部での用途なのでこれで十分と判断した。

Dockerfile

FROM mcr.microsoft.com/playwright:v1.58.2-noble

WORKDIR /app
COPY package.json .
RUN npm install
COPY . .

CMD ["node", "index.js"]

公式イメージにブラウザが全部入っている。自前でインストールしなくていい。これだけで Chromium、Firefox、WebKit が使える状態になる。

n8n から呼ぶためのラッパー

Docker コンテナ内のスクリプトを外から叩きたい。n8n の Execute Command ノードや cron から呼べるように、シェルのラッパーを作った。

#!/bin/bash
# run-scrape.sh — n8n/cron から呼ぶラッパー
URL="${1:?URL required}"
NAME="${2:?Name required}"
MODE="${3:-text}"

cd /opt/playwright-bot
docker compose exec -T playwright \
  node scripts/scrape.js "$URL" "$NAME" "$MODE"

引数は 3 つ。URL、出力名、モード。モードを省略するとテキスト取得になる。

# テキスト取得
./run-scrape.sh 'https://example.com/mypage' dashboard text

# スクリーンショット
./run-scrape.sh 'https://example.com/mypage' dashboard screenshot

# 全部取り(テキスト + HTML + スクリーンショット)
./run-scrape.sh 'https://example.com/mypage' dashboard all

n8n からは Execute Command ノードに /opt/playwright-bot/run-scrape.sh のパスを書くだけ。これで「定期巡回 → 変更検知 → 通知」のパイプラインが組める。

scrape.js の 4 モード

汎用スクレイパーは 4 つのモードを持つ。

const { chromium } = require('playwright');

const [,, url, name, mode = 'text'] = process.argv;

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle' });

  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const base = `/app/output/${name}_${timestamp}`;

  if (mode === 'text' || mode === 'all') {
    const text = await page.innerText('body');
    require('fs').writeFileSync(`${base}.txt`, text);
  }
  if (mode === 'html' || mode === 'all') {
    const html = await page.content();
    require('fs').writeFileSync(`${base}.html`, html);
  }
  if (mode === 'screenshot' || mode === 'all') {
    await page.screenshot({ path: `${base}.png`, fullPage: true });
  }

  await browser.close();
  console.log(`Done: ${base}`);
})();

networkidle で待つので、JS でレンダリングされるページでも中身が取れる。SPA だろうが WIX だろうが関係ない。

出力は output/ ディレクトリにタイムスタンプ付きで保存される。前回の結果と diff を取れば変更検知になる。

n8n 連携:定期巡回の実際

n8n 側のワークフローはこうなる。

  1. Schedule Trigger — 毎朝 9:00 に起動
  2. Execute Commandrun-scrape.sh を叩いてページ取得
  3. Read File — 今回の出力を読む
  4. Compare — 前回の出力と diff
  5. IF — 差分があれば通知、なければ終了
  6. 通知 — チャットツールに「○○のページが更新されました」と投げる

Execute Command ノードに書くのは 1 行だけだ。

/opt/playwright-bot/run-scrape.sh 'https://example.com/dashboard' daily-check text

これで「朝起きたら更新通知が来ている」状態が作れる。

Codegen:操作を録画してコード生成

Playwright の隠し球が Codegen だ。ブラウザを立ち上げて普通に操作すると、その操作がそのままテストコードとして記録される。

npx playwright codegen https://example.com

ブラウザが開く。ログインする。ページを遷移する。ボタンを押す。操作をやめると、今の操作を再現する JavaScript が出力される。

新しいサイトの自動化スクリプトを書くとき、セレクタを調べる時間がゼロになる。操作して、出力されたコードを貼って、微調整するだけ。

コンプラ:やっていいこと、ダメなこと

ヘッドレスブラウザは強力だからこそ、線引きが要る。自分の中で決めたルールはこうだ。

判定 用途
✅ OK 自分のアカウントで自分の情報を自動取得
✅ OK 手動ログイン → Cookie 保存 → 自動巡回(技術的保護手段の回避に該当しない)
❌ NG SNS への自動投稿(BAN リスク)
❌ NG reCAPTCHA の突破
❌ NG 利用規約で自動アクセスを禁じているサービス

「技術的にできる」と「やっていい」は別の話だ。Cookie 再利用は正規のセッション維持と同じ仕組みなので問題ない。一方、CAPTCHA 突破やログイン画面の自動突破は技術的保護手段の回避にあたる可能性がある。グレーなことはやらない。

「第四の腕」とは何か

自分の自動化環境には 3 つの腕があった。

  1. Claude Code — 対話的な開発・ファイル操作・コード生成
  2. MCP — 外部サービスとの API 接続(Google、チャットツール、会計ソフト等)
  3. n8n — 24 時間ワークフロー自動実行(VPS 上で常時稼働)

この 3 つで大半のことは回る。ただ「ブラウザでしかアクセスできない情報」だけは取れなかった。API がない。MCP もない。人間がブラウザを開くしかない。

Playwright がこの穴を埋めた。

n8n が「9 時に起きてスクリプトを叩く」。Playwright が「ブラウザを開いてログインして情報を取る」。取れた情報を n8n が整形して通知する。Claude Code はその通知を見て判断する。

4 本の腕が、それぞれ違う仕事をしている。どれか 1 本が欠けても、自動化に穴が開く。

正直、Playwright を入れる前は「スクレイピングツールでしょ」くらいの認識だった。入れてみたら、手作業の最後の砦が崩れた。もっと早く入れればよかった。

まとめ

  • VPS に Playwright を Docker で入れた。ホストを汚さず、docker compose で管理できる
  • shm_size: '1gb'network_mode: host がハマりポイント
  • n8n から呼べるラッパーを作り、定期巡回 → 変更検知 → 通知のパイプラインを構築
  • Codegen で操作録画 → コード生成。新サイトの自動化が爆速になる
  • コンプラの線引きは自分で決めておく。グレーなことはやらない

3 本の腕で「だいたい自動化できた」と思っていた。4 本目が生えて初めて、「だいたい」の中に残っていた手作業の大きさに気づいた。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?