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 が返らないときの切り分け
よくある原因を、可能性の高い順に並べます。
- モジュールが読み込まれていない、または書き方とバージョンが合っていない:古い Apache で
upgrade=websocketを書くと、構文エラーや無視される原因になります。手順1・2を見直します。 - バックエンドが待ち受けていない、またはパスが違う:Apache のサーバー上から
curl -i http://127.0.0.1:3000/ws/を実行し、応答があるか確認します。502 や 503 が返る場合はこちらを疑います。 - 前段の CDN やロードバランサーが WebSocket を通していない:Apache の前に別のプロキシがある場合、そこでもアップグレードの中継が必要です。サービスによっては設定で有効にする必要があるため、各サービスのドキュメントを確認します。
- 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 で安定させる