背景
個人開発中のタスク管理アプリで、フロントエンド(React + Vite)にvitest + Testing Libraryを導入したついでに、E2EテストとしてPlaywrightも入れようとしました。
既存のフロントエンドはDockerコンテナ(node:24-alpineベース)で動かしていたので、素直にそこへ@playwright/testを追加しようとしたところから、いくつかの罠を踏むことになりました。
環境
- Docker Compose(Windows / Docker Desktop)
- フロントエンド: React 19 + Vite 8(
node:24-alpineコンテナ) - バックエンド: Laravel(PHP、
appという別コンテナ)
罠1: PlaywrightはAlpine(musl)を公式サポートしていない
既存のフロントエンドコンテナに@playwright/testを追加してブラウザをインストールしようとすると、そもそも動作しません。
Playwrightのブラウザバイナリ(Chromium/Firefox/WebKit)はglibcに依存しており、Alpine Linuxが採用しているmuslライブラリでは動作しないためです。Microsoft公式のPlaywright Dockerイメージも、Alpineは非対応と明記されています。
対処法
既存のAlpineベースの開発用コンテナは変更せず、E2E専用の新しいサービスをdocker-compose.ymlに追加しました。ベースイメージにはMicrosoft公式のmcr.microsoft.com/playwright(Ubuntuベース)を使います。
services:
frontend:
build: ./frontend # node:24-alpineベース。これはそのまま
e2e:
image: mcr.microsoft.com/playwright:v1.63.0-jammy
working_dir: /e2e
volumes:
- ./e2e:/e2e
- e2e_node_modules:/e2e/node_modules
ipc: host
profiles: ["e2e"] # 通常のdocker-compose upでは起動しない
depends_on:
- frontend
volumes:
e2e_node_modules:
イメージのタグ(v1.63.0-jammy)は、e2e側のpackage.jsonに固定する@playwright/testのバージョンと完全に一致させる必要があります。ブラウザバイナリとテストランナーのバージョンがズレると起動に失敗します。
profiles: ["e2e"]を指定しておくと、普段のdocker-compose upではこのコンテナは起動しません。E2Eテストを実行したいときだけdocker-compose run --rm e2e npx playwright testのように明示的に叩く運用にしています。
ipc: hostも付けています。Chromiumは共有メモリを使ってプロセス間通信をしますが、Dockerのデフォルトの/dev/shmは64MBしかなく、これが足りないとChromiumが静かにクラッシュすることがあるためです。
罠2: E2Eコンテナ内のブラウザから、バックエンドのlocalhostに到達できない
フロントエンドのAPI通信は、環境変数が未設定のときにhttp://localhost:8000/apiにフォールバックする作りになっていました。この状態でE2Eコンテナ内のブラウザから開発サーバーにアクセスすると、画面は表示されるものの、API通信だけが失敗します。
原因はシンプルで、E2Eコンテナ内のブラウザから見たlocalhostは、E2Eコンテナ自身を指してしまうからです。バックエンド(Laravel)は別コンテナ(app)で動いているので、そちらのポートには到達できません。
最初に試して失敗した方法: page.route()でURLを書き換える
Playwrightにはリクエストをインターセプトして書き換えるpage.route()という機能があります。最初はこれで、localhost:8000宛のリクエストを内部サービス名app:8000に付け替えようとしました。
await page.route("http://localhost:8000/**", (route) => {
const url = new URL(route.request().url());
url.hostname = "app";
route.continue({ url: url.toString() });
});
これを実行すると、ブラウザ側で謎の307リダイレクトが発生し、最終的に接続エラーになりました。route.fetch()を使ってPlaywright側で代理リクエストする方式も試しましたが、今度はリクエストがハングしたまま完了しませんでした。
Playwrightの公式ドキュメントや関連issueを調べたところ、route.continue()やroute.fetch()でURLを書き換えても、Hostヘッダーは元のリクエストの値のまま保持され、上書きできない仕様であることが分かりました。これが今回の挙動に関係している可能性が高いです。
対処法: Viteの開発サーバー側でプロキシする
原因調査に時間をかけるより、確実に動く方法に切り替えました。フロントエンドの環境変数を相対パス(/api)にし、Viteの開発サーバー自体にプロキシ機能を持たせる方式です。これは本番環境(nginxがリバースプロキシする構成)と同じ考え方です。
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://app:8000',
changeOrigin: true,
},
},
},
})
# docker-compose.yml
services:
frontend:
environment:
- VITE_API_BASE_URL=/api
この方式だと、ブラウザは常に同一オリジン(フロントエンド自身)にリクエストするだけで済みます。実際にAPIサーバーへ中継する処理はNode.jsプロセス(Viteの開発サーバー)が担当するので、ブラウザ側のクロスオリジンにまつわる問題を丸ごと回避できました。
罠3: Vite 8のホスト名制限に引っかかる
Viteのプロキシ設定まで直したのに、今度は別のエラーが出ました。
Blocked request. This host ("frontend") is not allowed.
Vite 8には、DNS rebinding攻撃への対策として、開発サーバーへのアクセス元ホスト名を制限するデフォルト設定があります。E2Eコンテナはサービス名(frontend)で開発サーバーにアクセスするため、これが弾かれてしまいました。
対処法
vite.config.tsのserver.allowedHostsに、許可したいホスト名を明示的に追加します。
export default defineConfig({
server: {
allowedHosts: ['frontend'],
},
})
罠4: docker-compose run --rmのたびにnode_modulesが消える
E2Eコンテナでnpm installを実行して依存関係を入れたはずなのに、次にテストを実行するとCannot find packageエラーになる、という現象にも遭遇しました。
原因は、docker-compose.ymlのボリューム指定を匿名ボリューム(ホスト側パスを書かない形式)にしていたことです。
# うまくいかない例
volumes:
- ./e2e:/e2e
- /e2e/node_modules # 匿名ボリューム
docker-compose run --rmはコンテナを実行後に削除する便利なオプションですが、匿名ボリュームも一緒に削除してしまう仕様があります。使い捨てで実行するたびに、node_modulesもまっさらな状態に戻っていたわけです。
対処法
名前付きボリュームに変更します。名前付きボリュームはコンテナのライフサイクルと切り離されるため、--rmで消えません。
volumes:
- ./e2e:/e2e
- e2e_node_modules:/e2e/node_modules # 名前付きボリューム
まとめ
- PlaywrightはAlpine非対応 → E2E専用のUbuntuベースコンテナを別サービスとして追加する
- E2Eコンテナ内のブラウザは
localhostが自分自身を指してしまう →page.route()のURL書き換えは不安定。開発サーバー側のプロキシ機能に任せる方が確実 - Vite 8はデフォルトでホスト名を制限する →
server.allowedHostsに追加する -
docker-compose run --rmは匿名ボリュームも削除する → 使い回したいキャッシュは名前付きボリュームにする
どれも一つひとつは小さな問題ですが、組み合わさると原因の切り分けに時間がかかりました。同じ構成でPlaywrightを導入しようとしている方の参考になれば幸いです。