2026年7月2日

2026年7月2日

Apache mod_proxy で WebSocket に対応する手順|バージョン別の書き方と101が返らないときの確認

Apache で WebSocket を中継するには、Apache 2.4.47 以降なら ProxyPass に upgrade=websocket を付けるのが最も簡単な方法です。それより古いバージョンでは mod_proxy_wstunnel と ws:// の転送先、または Upgrade ヘッダーを見て振り分ける RewriteRule を使います。この記事では、自分の Apache がどちらの方式を使えるかの確認から、モジュールの有効化、設定例、101 Switching Protocols が返らないときの切り分けまでを順に説明します。

WordPress のコア自体は WebSocket を使いません。ここで想定しているのは、WordPress と同じドメインの一部のパス(たとえば /ws/)で、Node.js などで作ったチャットや通知の仕組みを動かすケースです。

WebSocket が通常のプロキシ設定で通らない理由

WebSocket の接続は、最初に通常の HTTP リクエストとして始まります。クライアントは Upgrade: websocket と Connection: Upgrade というヘッダーを付けて接続し、サーバーが 101 Switching Protocols を返すと、その TCP 接続は以後 HTTP ではなく双方向の WebSocket 通信に切り替わります。

Connection や Upgrade は「ホップごとのヘッダー」と呼ばれ、プロキシは通常これらを転送先に渡しません。そのため、ふつうの ProxyPass だけではアップグレードの要求がバックエンドに届かず、接続が確立しないのです。Apache に「このパスではプロトコルの切り替えを中継してよい」と明示する必要があるのはこのためです。

手順1:Apache のバージョンを確認する

まず、使える方式を決めるためにバージョンを確認します。

# Debian / Ubuntu
apache2 -v
# RHEL / AlmaLinux / Rocky Linux など
httpd -v
  • 2.4.47 以降:mod_proxy_http が WebSocket のアップグレードを扱えるため、upgrade=websocket を使った書き方ができます。
  • 2.4.46 以前:mod_proxy_wstunnel と ws://(TLS を使う場合は wss://)の転送先を使います。

ディストリビューションのパッケージでは、セキュリティ修正だけを取り込んでバージョン番号が古いままのこともあります。(2026年10月現在)主要なディストリビューションの現行版は 2.4.47 より新しいものが多いですが、念のため実際の表示で判断してください。

手順2:必要なモジュールを有効にする

Debian 系では a2enmod で有効にします。

sudo a2enmod proxy proxy_http proxy_wstunnel headers
sudo apache2ctl configtest
sudo systemctl reload apache2

RHEL 系では、多くの場合 /etc/httpd/conf.modules.d/ 内の設定ファイルで proxy 関連のモジュールがすでに読み込まれています。読み込み状況は次のように確認できます。

sudo httpd -M | grep -E 'proxy_module|proxy_http|proxy_wstunnel'

2.4.47 以降で upgrade=websocket を使う場合、mod_proxy_wstunnel は必須ではありませんが、古い書き方の設定が残っていてもエラーにならないよう有効にしておくと移行がスムーズです。

手順3:バーチャルホストに設定を書く

設定ファイルを編集する前に、バックアップを取っておきます。以下は、https://example.com/ で WordPress を表示しつつ、/ws/ だけを 127.0.0.1:3000 で動く WebSocket サーバーに渡す例です。

Apache 2.4.47 以降の書き方

<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/wordpress

    # SSL関連の設定(証明書など)は既存のものを利用

    ProxyPreserveHost On
    RequestHeader set X-Forwarded-Proto "https"

    # /ws/ だけを WebSocket サーバーへ。WordPress 本体には影響させない
    ProxyPass        "/ws/" "http://127.0.0.1:3000/ws/" upgrade=websocket
    ProxyPassReverse "/ws/" "http://127.0.0.1:3000/ws/"
</VirtualHost>

upgrade=websocket を付けると、クライアントが WebSocket へのアップグレードを求めたときにだけトンネルとして中継し、通常の HTTP リクエストはそのまま HTTP として転送します。Socket.IO のように、最初は通常の HTTP 通信(ロングポーリング)で始めてから WebSocket に切り替えるライブラリでも、この1行で両方を扱えるのが利点です。

Apache 2.4.46 以前の書き方

<VirtualHost *:443>
    ServerName example.com
    DocumentRoot /var/www/wordpress

    ProxyPreserveHost On
    RequestHeader set X-Forwarded-Proto "https"

    RewriteEngine On
    # Upgrade: websocket のリクエストだけ ws:// に転送する
    RewriteCond %{HTTP:Upgrade} =websocket [NC]
    RewriteRule ^/ws/(.*)$ "ws://127.0.0.1:3000/ws/$1" [P,L]

    # それ以外の /ws/ へのリクエストは通常のHTTPとして転送する
    ProxyPass        "/ws/" "http://127.0.0.1:3000/ws/"
    ProxyPassReverse "/ws/" "http://127.0.0.1:3000/ws/"
</VirtualHost>

WebSocket 専用のパスで HTTP のリクエストが来ないと分かっているなら、ProxyPass "/ws/" "ws://127.0.0.1:3000/ws/" の1行でも動きます。ただし、ポーリングで始まるライブラリでは HTTP のリクエストも来るため、上の例のように Upgrade ヘッダーで振り分ける書き方が無難です。

書くときに注意したい点

  • ProxyPass は上から順に評価されます。/ 全体を別のサーバーに渡すような設定と併用する場合は、/ws/ のような具体的なパスを先に書きます。
  • パス末尾のスラッシュは、左右(公開側と転送先)でそろえます。片方だけに付けると、転送先の URL が意図しない形になります。
  • WordPress のパーマリンク用の .htaccess は DocumentRoot 配下に効くため、/ws/ が WordPress の書き換えルールに吸い込まれていないかも確認します。ProxyPass はバーチャルホストの設定として先に処理されるのが一般的ですが、RewriteRule を使う書き方では評価順に注意が必要です。

設定後は構文チェックをしてから再読み込みします。

sudo apache2ctl configtest   # RHEL系は sudo apachectl configtest
sudo systemctl reload apache2

手順4:101 が返るか確認する

ブラウザの開発者ツールのネットワークタブで、WebSocket の接続のステータスが 101 になっていれば成功です。コマンドラインでは、curl でハンドシェイクだけを試せます。

curl -i -N --http1.1 \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
  https://example.com/ws/

HTTP/1.1 101 Switching Protocols が返れば、Apache からバックエンドまでアップグレードが通っています。curl はその後の通信を扱えないため、応答を確認したら Ctrl+C で止めてください。

101 が返らないときの切り分け

よくある原因を、可能性の高い順に並べます。

  1. モジュールが読み込まれていない、または書き方とバージョンが合っていない:古い Apache で upgrade=websocket を書くと、構文エラーや無視される原因になります。手順1・2を見直します。
  2. バックエンドが待ち受けていない、またはパスが違う:Apache のサーバー上から curl -i http://127.0.0.1:3000/ws/ を実行し、応答があるか確認します。502 や 503 が返る場合はこちらを疑います。
  3. 前段の CDN やロードバランサーが WebSocket を通していない:Apache の前に別のプロキシがある場合、そこでもアップグレードの中継が必要です。サービスによっては設定で有効にする必要があるため、各サービスのドキュメントを確認します。
  4. HTTP/2 で接続している:WebSocket の従来のハンドシェイクは HTTP/1.1 を前提にしています。ブラウザは通常自動で扱いますが、テスト用のクライアントでは HTTP/1.1 を明示して確認します。

原因が分からないときは、一時的にプロキシ関連のログを詳しくします。

LogLevel warn proxy:debug proxy_http:debug proxy_wstunnel:debug

エラーログに転送先への接続失敗やアップグレードの処理状況が出るので、原因を特定したら元のログレベルに戻します。詳細ログは量が多く、ディスクを圧迫しやすいためです。

接続がすぐ切れる場合の対策

101 は返るのに、しばらく通信がないと接続が切れる場合は、タイムアウトが原因のことが多いです。Apache の ProxyTimeout や ProxyPass の timeout= パラメーターがトンネル中の無通信時間にも影響します。どの設定が効くかは Apache のバージョンや使っているモジュールで異なるため、公式ドキュメントで確認してください。

タイムアウトを長くするより確実性の高い対策は、アプリケーション側で数十秒ごとに ping/pong(ハートビート)を送ることです。途中に CDN やロードバランサーが入っていても、無通信と判断されにくくなります。

また、WebSocket は1本の接続を長時間保持するため、Apache の MPM が prefork のままだと、接続数に応じてプロセスを使い切ってしまいます。WebSocket を多く扱うなら event MPM で運用するのが一般的です。MPM の変更は PHP の動かし方(mod_php か PHP-FPM か)にも関わるため、ステージング環境で検証してから切り替えてください。

セキュリティ面の注意

  • 公開する接続は wss://(TLS)に限定し、TLS は Apache で終端します。
  • WebSocket サーバー側で Origin ヘッダーを確認し、自サイト以外のページからの接続を拒否します。
  • ログインが必要な機能では、接続時にトークンなどで認証し、WordPress の Cookie をそのまま信用する作りにしないよう設計します。
  • バックエンドの 3000 番ポートは 127.0.0.1 で待ち受け、外部から直接つながらないようにします。

まとめ

  • WebSocket は Upgrade ヘッダーによるプロトコル切り替えが必要で、通常の ProxyPass だけでは通らない
  • Apache 2.4.47 以降は ProxyPass ... upgrade=websocket、それ以前は mod_proxy_wstunnel と ws:// を使う
  • ポーリングから始まるライブラリでは、HTTP と WebSocket の両方を中継できる書き方にする
  • 101 が返らないときは、モジュールとバージョン、バックエンド、前段のプロキシ、HTTP/2 の順に確認する
  • 接続が切れる場合はタイムアウトを見直し、アプリ側のハートビートと event MPM で安定させる

お気軽にご相談ください

お見積りへ お問い合わせへ
▲