🦘

Docker 環境で Playwright テスト実行時に「Invalid Host Header」エラーが発生する問題の解決方法

2025/03/08に公開

環境情報

  • 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 つのサービスを含む環境を構築しました:

  1. Frontend: Web アプリを提供するコンテナ(webpack-dev-server 使用)
  2. Backend: API を提供するバックエンドサービス
  3. 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 つの主要な問題が見つかりました:

  1. webpack-dev-server のホストヘッダー検証: webpack-dev-server がデフォルトでfrontendというホスト名からのリクエストを拒否していました(Invalid Host headerエラー)
  2. テストコードでの直接 URL 指定: テストコードで相対パスではなく絶対 URL を使っていたため、Playwright のbaseURL設定が適用されていませんでした

解決策

  1. webpack-dev-server の設定修正: allowedHostsオプションを追加して、必要なホスト名からのアクセスを許可する
  2. テストコードの修正: 絶対 URL ではなく相対パスを使うように変更する
  3. テストの期待値の修正: アプリの実際の動作と一致するように期待値を調整する

これらの修正により、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. テストコードの問題

テストコードを確認したところ、以下のような問題がありました。

  1. テストファイルで直接 URL を指定していた

    await page.goto("http://frontend:3000/");
    
  2. Playwright の設定ファイルでは、環境変数を使って baseURL を設定していた

    baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
    
  3. Docker Compose 環境では、環境変数 PLAYWRIGHT_BASE_URLhttp://frontend:3000 に設定されていたのに、テストファイル内で直接 URL を指定していたため、この設定が適用されていませんでした

詳細な解決方法

1. webpack-dev-server の設定修正

webpack.config.js ファイルを修正して、ホストヘッダーの検証設定を変更しました。

javascript
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. テストの期待値の修正

テストの期待値がアプリの実際の動作と一致していなかったため、以下の修正を行いました。

  1. タイトルの期待値を修正

    await expect(page).toHaveTitle(/Web App Title/);
    
  2. フォーム要素の確認方法を修正

    const formGroup = page.locator(".form-group").first();
    await expect(formGroup).toBeVisible();
    
  3. コピーボタンのテストを簡略化

    // 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で以下のように設定します:

javascript
// 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環境ではclientPorthostの設定が必要になることがあります。

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 のテストを実行する際には、以下の点に注意する:

  1. フレームワークの種類に応じた設定:

    • Webpack ベースのフレームワーク: host: '0.0.0.0'allowedHosts の両方を設定
    • Vite ベースのフレームワーク: host: '0.0.0.0' の設定のみで十分
  2. テストコードの書き方:

    • 直接 URL を指定せず、相対パスを使う
    • テストの期待値がアプリの実際の動作と一致していることを確認する
    • 要素を指定する際は、より具体的なセレクタを使う
  3. コンテナ間通信の確認:

    • コンテナ間の通信が正常に行われていることを確認する
    • 必要に応じて curl などでリクエストをテストする

これらの対策を講じることで、Docker 環境でも Playwright のテストが正常に実行できるようになります。また、フレームワークに関わらず、Docker 環境でのフロントエンド開発とテストでは、ネットワーク関連の設定に注意を払うことが大切です。

実装例

上記の解決策を実装した実際のサンプルコードは以下です。:
https://github.com/keishimizu26629/docker-playwright-sample

参考 URL

Discussion