Docker 環境で Playwright テスト実行時に「Invalid Host Header」エラーが発生する問題の解決方法
環境情報
- Node.js: v20
- Playwright: v1.49.1
- Docker Compose
- webpack-dev-server: v5.1.0
- OS: macOS 24.3.0
はじめに
私のプロジェクトでは、フロントエンド環境と Playwright(E2E テスト)環境を別々の Docker コンテナとして分離しています。
分離するアプローチをするとコンテナ間通信の設定が必要になるので、解決方法を紹介します。
システム構成
Docker Compose を使って、以下の 3 つのサービスを含む環境を構築しました:
- Frontend: Web アプリを提供するコンテナ(webpack-dev-server 使用)
- Backend: API を提供するバックエンドサービス
- Playwright: E2E テストを実行するためのコンテナ
これらのサービスは同じ Docker Compose ネットワーク上で通信することを想定していました。
発生した問題
Docker Compose 環境で Playwright の E2E テストを実行しようとしたところ、以下のようなエラーが発生しました。
Running 9 tests using 4 workers
[webkit] › localization.spec.ts:3:5 › Application title should be in English
Page loaded:
1) [webkit] › localization.spec.ts:15:5 › Form labels should be in English ───────────────
Error: Timed out 10000ms waiting for expect(locator).toContainText(expected)
Locator: locator('label[for="price"]')
Expected string: "Item price (USD)"
Received: <element(s) not found>
Call log:
- expect.toContainText with timeout 10000ms
- waiting for locator('label[for="price"]')
テストコードは正しく書かれているはずなのに、要素が見つからないというエラーが出ました。また、ページのタイトルが空文字列として返されるなど、ページが正しく読み込まれていない様子でした。
Error: Timed out 10000ms waiting for expect(locator).toHaveTitle(expected)
Locator: locator(':root')
Expected pattern: /Web App Title/
Received string: ""
結論:問題の原因と解決策
問題の原因
調査した結果、以下の 2 つの主要な問題が見つかりました:
-
webpack-dev-server のホストヘッダー検証: webpack-dev-server がデフォルトで
frontend
というホスト名からのリクエストを拒否していました(Invalid Host header
エラー) -
テストコードでの直接 URL 指定: テストコードで相対パスではなく絶対 URL を使っていたため、Playwright の
baseURL
設定が適用されていませんでした
解決策
-
webpack-dev-server の設定修正:
allowedHosts
オプションを追加して、必要なホスト名からのアクセスを許可する - テストコードの修正: 絶対 URL ではなく相対パスを使うように変更する
- テストの期待値の修正: アプリの実際の動作と一致するように期待値を調整する
これらの修正により、Docker 環境でも Playwright のテストが正常に実行できるようになりました!
詳細な原因調査
1. コンテナの状態確認
まず、各コンテナが正常に起動しているか確認するために、以下のコマンドを実行しました。
docker compose ps
結果として、フロントエンドコンテナは正常に起動しており、ヘルスチェックも通過していることが確認できました。
NAME IMAGE COMMAND SERVICE CREATED STATUS
PORTS
my-app-frontend-1 my-app-frontend "docker-entrypoint.s…" frontend 16 minutes ago Up 16 minutes (healthy) 0.0.0.0:3000->3000/tcp
フロントエンドコンテナ自体からローカルホストにアクセスして、正常に HTML が返されることも確認しました。
docker compose exec frontend curl -s http://localhost:3000 | head -n 10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
/>
<title>Web App Title</title>
</head>
</html>
2. コンテナ間の接続確認
次に、playwright コンテナからフロントエンドコンテナにアクセスできるか確認するために、以下のコマンドを実行しました。
docker compose run --rm playwright curl -s http://frontend:3000 | head -n 10
結果として、以下のエラーが返されました。
Invalid Host header
これは、webpack-dev-server がホストヘッダーを検証していて、frontend
というホスト名からのリクエストを拒否していることを示しています。つまり、フロントエンドコンテナ自体は正常に動いているのに、他のコンテナからのアクセスを拒否していたんですね。
3. テストコードの問題
テストコードを確認したところ、以下のような問題がありました。
-
テストファイルで直接 URL を指定していた
await page.goto("http://frontend:3000/");
-
Playwright の設定ファイルでは、環境変数を使って baseURL を設定していた
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
-
Docker Compose 環境では、環境変数
PLAYWRIGHT_BASE_URL
がhttp://frontend:3000
に設定されていたのに、テストファイル内で直接 URL を指定していたため、この設定が適用されていませんでした
詳細な解決方法
1. webpack-dev-server の設定修正
webpack.config.js ファイルを修正して、ホストヘッダーの検証設定を変更しました。
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
port: 3000,
host: '0.0.0.0', // Dockerからアクセス可能にする設定
hot: true, // ホットリロードを有効にする
compress: true,
+ allowedHosts: ['frontend', 'localhost', '.local'], // 必要なホスト名のみを許可
},
ここで、allowedHosts
の'frontend'
はDocker Compose のサービス名を指しています。Docker Compose では、サービス名がコンテナ間通信のためのホスト名として使われます。つまり、docker-compose.yml
で定義したfrontend
サービスに対して、http://frontend:3000
のようにアクセスできるようになります。
allowedHosts: ['frontend', 'localhost', '.local']
を設定することで、必要なホスト名からのリクエストのみを許可し、セキュリティも向上させました。
2. テストコードの修正
テストファイルを修正して、直接 URL を指定する代わりに相対パスを使うようにしました。
修正前:
await page.goto("http://frontend:3000/");
修正後:
await page.goto("/");
これにより、Playwright の設定ファイルで指定された baseURL
が適用されるようになりました。
3. テストの期待値の修正
テストの期待値がアプリの実際の動作と一致していなかったため、以下の修正を行いました。
-
タイトルの期待値を修正
await expect(page).toHaveTitle(/Web App Title/);
-
フォーム要素の確認方法を修正
const formGroup = page.locator(".form-group").first(); await expect(formGroup).toBeVisible();
-
コピーボタンのテストを簡略化
// Check if copy buttons exist in the DOM await expect(page.locator("#copy-button")).toHaveText("Copy");
他のフレームワークでの対応方法
このプロジェクトではWordPressとの統合を目的としていたため、フレームワークを使わずTypeScriptで書いてJavaScriptにトランスパイルする方法を採用し、そのためにWebpackを使いました。しかし、他のフレームワークを使った場合でも同様の問題が発生する可能性があります。
フロントエンドのビルドツールや開発サーバーには様々な選択肢が存在します。今回の問題であるホストヘッダー検証の観点から、主に以下のパターンについて説明します。
Webpackベースの開発サーバー(allowedHosts設定が必要)
webpack-dev-serverを使用するフレームワークでは、host: '0.0.0.0'
の設定に加えて、allowedHosts
の設定も必要です。これは、Webpackがホストヘッダーの検証を行うためです。
Vue CLIを使用した場合
Vue CLIを使ったプロジェクトでは、vue.config.js
で以下のように設定します:
// vue.config.js
module.exports = {
devServer: {
host: "0.0.0.0",
port: 3000,
+ allowedHosts: ["frontend", "localhost", ".local"],
},
};
Next.js(Webpack使用時)を使用した場合
Next.js(Webpack設定を使用する場合)では、next.config.js
で以下のように設定します:
// next.config.js
module.exports = {
webpackDevMiddleware: (config) => {
config.watchOptions = {
poll: 1000,
aggregateTimeout: 300,
};
// Docker環境でのホストヘッダー検証のための設定
config.devServer = {
...(config.devServer || {}),
allowedHosts: ["frontend", "localhost", ".local"],
};
return config;
},
// Next.js 12以降
webpack: (config) => {
config.infrastructureLogging = { level: "error" };
return config;
},
};
Viteベースの開発サーバー(host設定のみで十分)
Viteを使用するフレームワークでは、host: '0.0.0.0'
の設定だけで十分です。Viteはデフォルトでホストヘッダーの検証を行わず、バインドアドレスによるアクセス制限を行います。
Viteを使用した場合
Viteを使ったプロジェクトでは、vite.config.js
(またはvite.config.ts
)で以下のように設定します:
// vite.config.js
export default {
server: {
host: "0.0.0.0",
port: 3000,
strictPort: true,
hmr: {
clientPort: 3000,
host: "localhost",
},
},
};
Viteではserver.hmr
の設定が重要で、特にDocker環境ではclientPort
とhost
の設定が必要になることがあります。
Nuxt.jsを使用した場合
Nuxt.js(Viteベース)では、nuxt.config.js
(またはnuxt.config.ts
)で以下のように設定します:
// nuxt.config.js または nuxt.config.ts
export default defineNuxtConfig({
devServer: {
host: "0.0.0.0",
},
});
Nuxt.jsはデフォルトではlocalhost
のみにバインドされており、外部からのアクセスを許可していません。起動時のログにもNetwork: use --host to expose
と表示されます。Docker環境でコンテナ間通信を行うためには、明示的にhost: '0.0.0.0'
の設定が必要です。
その他のビルドツールと開発サーバー
フロントエンド開発には、WebpackやVite以外にも多様なビルドツールが存在します:
- Turbopack: Next.js 15.2.1以降のデフォルトバンドラーで、ホストヘッダー検証の設定が自動化されています。
- Parcel: ゼロコンフィグのビルドツールで、開発サーバーは通常外部からのアクセスを許可します。
- esbuild: 高速なJavaScript/TypeScriptコンパイラで、多くの現代的なビルドツールの基盤として使用されています。
- Rollup: 主にライブラリ向けのバンドラーですが、SvelteKitなどのフレームワークでも使用されています。
- Snowpack: モジュールベースの開発環境を提供するビルドツールです。
これらのツールはそれぞれ異なるホストヘッダー検証の実装を持っている場合があるため、使用するツールの公式ドキュメントを参照することをお勧めします。
フレームワーク別の設定方法まとめ
フレームワーク | 主なビルドツール | 必要な設定 | 設定ファイル |
---|---|---|---|
Vue CLI | Webpack | host + allowedHosts | vue.config.js |
Next.js (Webpack) | Webpack | host + allowedHosts | next.config.js |
Next.js (15.2.1+) | Turbopack | 通常は不要(自動設定) | next.config.js |
Nuxt.js | Vite | host のみ | nuxt.config.js |
Vite | Vite | host のみ | vite.config.js |
SvelteKit | Vite/Rollup | host のみ | svelte.config.js |
Parcel | Parcel | 通常は不要 | .parcelrc |
まとめ
Docker 環境で Playwright のテストを実行する際には、以下の点に注意する:
-
フレームワークの種類に応じた設定:
- Webpack ベースのフレームワーク:
host: '0.0.0.0'
とallowedHosts
の両方を設定 - Vite ベースのフレームワーク:
host: '0.0.0.0'
の設定のみで十分
- Webpack ベースのフレームワーク:
-
テストコードの書き方:
- 直接 URL を指定せず、相対パスを使う
- テストの期待値がアプリの実際の動作と一致していることを確認する
- 要素を指定する際は、より具体的なセレクタを使う
-
コンテナ間通信の確認:
- コンテナ間の通信が正常に行われていることを確認する
- 必要に応じて
curl
などでリクエストをテストする
これらの対策を講じることで、Docker 環境でも Playwright のテストが正常に実行できるようになります。また、フレームワークに関わらず、Docker 環境でのフロントエンド開発とテストでは、ネットワーク関連の設定に注意を払うことが大切です。
実装例
上記の解決策を実装した実際のサンプルコードは以下です。:
Discussion