Claude Media
Claude CodeでPlaywrightをDockerの定期ジョブにする構成

Claude CodeでPlaywrightをDockerの定期ジョブにする構成

PlaywrightのスクリプトをMicrosoft公式イメージに閉じ込めてcronから回し、開発側はClaude Codeのdevcontainerで育てる構成を、設定例つきで分けて示します。

Playwrightのスクリプトをcronやジョブから回すなら、ブラウザと依存ライブラリをDockerイメージに閉じ込めるのが近道です。Claude Codeはその「イメージを育てる側」に置きます。定期ジョブの実行側には載せません。

この記事は、実行用のイメージ、cronからの呼び出し、Claude Code入りのdevcontainerの3つを順に組みます。実行側と開発側でコンテナを分ける理由も、ドキュメントの注意書きから読み解きます。

Playwrightのイメージが持つものと持たないもの

mcr.microsoft.com/playwrightは、Playwrightのブラウザとブラウザの動作に必要なシステム依存を含むイメージです。Microsoft Artifact Registryで公開されています。

持たないものもあります。Playwrightのパッケージ(npmのライブラリ)はイメージに入っておらず、別途インストールが必要です。ここを見落とすと、イメージを引いたのにimportで落ちます。

もう一つの要点がバージョンです。Playwrightのドキュメントの例はv1.64.1-nobleのようにバージョンを固定したタグで、イメージのPlaywrightとプロジェクト側のPlaywrightが食い違うと、ブラウザの実行ファイルを見つけられなくなります。ドキュメントにはバージョン固定を勧める記述もあります。

項目内容
ブラウザ・システム依存内容イメージに含まれる
Playwrightパッケージ内容含まれない(別にインストール)
タグ内容v<バージョン>、v<バージョン>-noble、-jammy、-resolute(Ubuntu 24.04 / 22.04 / 26.04)
Alpine内容非対応(FirefoxとWebKitはglibc向けのビルド)

自前でイメージを組む場合の最小形もドキュメントに載っています。node:20-bookwormにnpx -y playwright@1.64.1 install --with-depsを重ねる形です。既製のイメージで足りるなら、定期ジョブではMicrosoftのタグを使うほうが管理する行が減ります。

ジョブ用のDockerfileとスクリプトを置く

例として、あるページの見出しを取得して標準出力に書くだけのスクリプトを使います。巡回先は自分が管理するサイトとします。

{
  "type": "module",
  "dependencies": { "playwright": "1.64.1" }
}

package.jsonのバージョンは範囲指定(^)にせず、イメージのタグと同じ値で固定します。タグを上げるときは、この行も同じコミットで上げます。

// scripts/check-title.mjs
import { chromium } from 'playwright';
 
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(process.env.TARGET_URL, { timeout: 30_000 });
console.log(JSON.stringify({ url: page.url(), title: await page.title() }));
await browser.close();

Dockerfileは、イメージの上に依存だけを重ねます。

FROM mcr.microsoft.com/playwright:v1.64.1-noble
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY scripts ./scripts
USER pwuser
CMD ["node", "scripts/check-title.mjs"]

pwuserは、Playwrightのドキュメントのdocker run例に出てくるユーザーです。npm ciはpackage-lock.jsonを前提にするので、先にローカルでnpm installを実行して生成し、package.jsonと一緒にコミットしておきます。

実行はPlaywrightが推奨する設定に合わせます。

docker build -t pw-job:1.64.1 .
docker run --rm --init --ipc=host \
  -e TARGET_URL=https://example.com/ \
  pw-job:1.64.1

--initはPID 1の特別扱いを避けるため、--ipc=hostはChromiumのメモリ不足クラッシュを避けるための指定です。Chromiumの起動で原因不明のエラーが出るときは、ローカル開発に限って--cap-add=SYS_ADMINを試す、とも書かれています。

巡回先がよそのサイトなら、rootをやめる

このイメージは既定でrootのままブラウザを動かし、Chromiumのサンドボックスは無効になります。自分のサイトのE2Eテストのように信頼できるコードだけを動かすなら、rootでもよいとされています。

スクレイピングやクローリングは別です。他人のサイトを開くなら、別ユーザーで起動し、seccompプロファイルを付けるよう案内されています。上のDockerfileでUSER pwuserにしたのはそのためです。サンドボックスを使うには、ユーザー名前空間の作成を許すseccomp_profile.jsonを渡します。このファイルはDockerの既定のseccompプロファイルに、ユーザー名前空間の作成を許す設定を足したもので、中身はPlaywrightのDockerのページに全文が載っています。それをseccomp_profile.jsonとして保存して使います。

docker run --rm --init --ipc=host --user pwuser \
  --security-opt seccomp=seccomp_profile.json \
  -e TARGET_URL=https://example.com/ \
  pw-job:1.64.1

Dockerのページの冒頭には、このイメージはテストと開発の用途向けで、信頼できないサイトの閲覧には勧めないという注記もあります。外部サイトを巡回するジョブでは、対象を絞る構成がこの注記に沿います。

cronから呼んで、失敗を取りこぼさない

ホストのcronは、docker runを1行で呼ぶだけで足ります。

# /etc/cron.d/pw-job  毎日 6:00
0 6 * * * deploy /opt/pw-job/run.sh >> /var/log/pw-job.log 2>&1
#!/bin/sh
# /opt/pw-job/run.sh
set -eu
docker run --rm --init --ipc=host --user pwuser \
  --security-opt seccomp=/opt/pw-job/seccomp_profile.json \
  -e TARGET_URL=https://example.com/ \
  pw-job:1.64.1

set -eを入れておくと、コンテナが非0で終わった時点でシェルも非0で終わります。cronの失敗検知や、失敗時だけの通知に繋げやすくなります。失敗時だけ通知する書き方はClaude Codeのcron失敗を調査する手順と失敗時だけ通知する書き方に詳しくあります。

GitHub Actionsで毎朝回す選択肢もあります。その場合のcron設定はclaude-code-actionをcronで回し、毎朝のコミットとIssueを要約するが参考になります。

ジョブの途中でexit code 137が出たら、メモリ上限で落ちた可能性があります。切り分けはClaude Codeでexit code 137のOOMを切り分ける手順にまとめています。

開発側は、同じイメージのdevcontainerで育てる

スクリプトを書き直したり、セレクタを直したりする作業をClaude Codeに任せるなら、実行と同じブラウザ環境の中で動かすのが確実です。手元のmacOSで通ったのにイメージ内で落ちる、という差が消えます。

Claude CodeのdevcontainerはDev Container Featureで入ります。Claude Code側のサンプルはベースイメージにmcr.microsoft.com/devcontainers/base:ubuntuを使っていますが、imageの行は自分のベースイメージに置き換えてよいとされています。ここにPlaywrightのイメージを入れた形が次です。Claude Codeのdevcontainerの例を、この記事の構成に合わせて組み替えたものです。

{
  "image": "mcr.microsoft.com/playwright:v1.64.1-noble",
  "remoteUser": "pwuser",
  "features": {
    "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}
  },
  "mounts": [
    "source=claude-code-config-${devcontainerId},target=/home/pwuser/.claude,type=volume"
  ],
  "containerEnv": {
    "CLAUDE_CONFIG_DIR": "/home/pwuser/.claude"
  }
}

要点は4つです。

  • claude-code:1.0の末尾のタグは、機能のインストールスクリプトを固定するもので、Claude Codeのバージョンは固定しません
  • Node.jsが無いベースイメージでは、Featureが自分でNode.jsを入れます。それでFailed to install Node.js and npmが出るときは、ghcr.io/devcontainers/features/node:1をfeaturesの上側に足します
  • ~/.claudeにボリュームを載せ、CLAUDE_CONFIG_DIRを同じパスにしておくと、リビルドしてもサインインし直さずに済みます。ホームはremoteUserのものに合わせます
  • --dangerously-skip-permissionsは、rootで起動したCLIでは拒否されます。非rootのremoteUserにしておく必要があります

Claude Codeの認証トークンは~/.claudeに置かれます。devcontainerのページにある警告のとおり、devcontainerは万能の防壁ではなく、権限確認を外した実行では、コンテナ内から見えるものは外へ持ち出せる前提で考えます。

テストを手元で回し、ブラウザだけコンテナに置く手もある

スクリプトではなく@playwright/testのテストを回す用途なら、Playwright Serverをコンテナで起動し、テストは手元から繋ぐ構成も選べます。ブラウザ側のLinux依存だけをコンテナに閉じ込められます。

docker run -p 3000:3000 --rm --init -it --workdir /home/pwuser --user pwuser \
  mcr.microsoft.com/playwright:v1.64.1-noble \
  /bin/sh -c "npx -y playwright@1.64.1 run-server --port 3000 --host 0.0.0.0"
 
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:3000/ npx playwright test

手元のサーバーにコンテナから繋ぎたいときは--add-host=hostmachine:host-gatewayを足し、テスト側でlocalhostの代わりにhostmachineを使います。条件は一つで、手元のPlaywrightとコンテナ側のPlaywrightのバージョンを揃えることです。Claude Codeにテストを直させるときは、このバージョンの一致を依頼文に書いておくと、ずれた状態で修正が進むのを避けられます。

実行コンテナにClaude Codeを入れない理由

ここまでで、コンテナが2種類になりました。

コンテナClaude Code用途巡回先
ジョブ用(pw-job)Claude Code入れない用途cronで毎日実行巡回先自分で選んだURL
開発用(devcontainer)Claude Code入れる用途スクリプトの修正と検証巡回先自分のサイトかローカルのサーバー

分ける根拠は、二つのドキュメントの記述の組み合わせです。Playwrightのイメージは信頼できないサイトの閲覧に向かないとされ、Claude Codeのdevcontainerは、悪意あるプロジェクトが認証情報を持ち出せる可能性を警告しています。外部サイトを開くコンテナに認証情報を置かない構成が、この二つに沿った形です。

開発側から外部へ出る通信を絞りたいときは、Claude Codeのリポジトリにある参照コンテナがinit-firewall.shで送信先を制限する方式を示しています。ただしNET_ADMINとNET_RAWの権限が要ります。この仕組みはClaude Code自体には必須ではない、とも書かれています。

Claude Codeに頼む作業と、頼み方

devcontainerの中では、次の指示がそのまま通ります。依頼文にはイメージのタグと固定ルールを入れておくと、バージョンずれの修正を防げます。

# CLAUDE.md(抜粋)
- Playwrightのバージョンは `package.json` とDockerfileの `FROM` で同じ値に固定する。片方だけ上げない
- スクリプトの検証は devcontainer 内で `node scripts/check-title.mjs` を実行して出力を読む
- 巡回先は `TARGET_URL` で渡す。コードに外部サイトのURLを直書きしない

「ジョブが昨夜落ちたので原因を見て」という依頼なら、ログを渡して非対話で読ませる手もあります。--bareで動かすにはサブスクリプションのログインではなく、環境変数ANTHROPIC_API_KEYが必要です。サブスクリプションのログインで使うなら--bareを外します。

tail -n 200 /var/log/pw-job.log | claude --bare -p \
  "このログの失敗原因を3行で。再実行コマンドも添えて" \
  --allowedTools "Read"

--bareはスクリプトからの呼び出しに推奨されるモードで、フックやスキルやMCPサーバーの自動読み込みを省いて起動を速くします。標準入力で渡せる量は10MBまでです。大きなログは末尾だけに絞ります。上限の詳細はclaude -pにログを渡すと10MB上限で止まるにあります。

Playwrightのテストが落ちた場合の読み方はClaude CodeでPlaywrightのtraceを読み、テスト失敗の原因を切り分けるに分けてあります。

つまずきやすい点

  • イメージとnpmのバージョンが違う: playwrightを^1.64.0のような範囲指定にすると、ロックファイルを更新した拍子にタグとずれることがあります。ブラウザが見つからない、という症状で表に出ます
  • Chromiumがメモリ不足で落ちる: --ipc=hostを付けているか確認します
  • ゾンビプロセスが溜まる: --initを付けます
  • Alpineで動かない: FirefoxとWebKitがglibc前提のため、Debian系かUbuntu系のイメージを選びます
  • マウントした出力先に書けない: --user pwuserで動かすと、ホスト側のディレクトリの権限が効きます。スクリーンショットを書き出すなら、書き込めるディレクトリを用意します
  • タグを上げ忘れる: イメージのタグ、package.json、npm ciの再実行、docker buildをひとまとめにして更新します

まとめ

定期ジョブの実行側は、Playwrightのイメージとnpmパッケージを同じ版に固定した小さなコンテナで足ります。cronはdocker runを呼ぶだけです。

Claude Codeは、そのスクリプトとDockerfileを直す開発側に置きます。実行側に認証情報を持ち込まないので、外部サイトを開くジョブと開発環境の事情が混ざりません。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/devcontainer
  2. playwright.dev/docs/docker
  3. code.claude.com/docs/en/headless
この記事を共有:XはてブLinkedIn