2026年9月16日

2026年9月16日

ApacheでWebSocketをプロキシする方法(upgrade=websocketとタイムアウト対策)

ApacheでWebSocketをバックエンドのアプリ(Node.jsなど)へ中継するには、Apache 2.4.47以降なら mod_proxy_http の ProxyPass に upgrade=websocket を付けるのが最も簡単な方法です。それより古いバージョンでは mod_proxy_wstunnel と書き換えルールを組み合わせます。この記事では、両方の書き方、HTTPS(wss://)での構成、接続がすぐ切れるときのタイムアウト対策、WordPressと同じドメインで動かすときの注意、動作確認と切り分けの方法を説明します。

WebSocketのプロキシで何が起きているか

WebSocketは、最初に通常のHTTPリクエストとして始まり、Upgrade: websocket と Connection: Upgrade ヘッダーで「この接続をWebSocketに切り替えたい」と申し出ます。サーバーが 101 Switching Protocols を返すと、以降は同じTCP接続の上で双方向にデータをやり取りします。

リバースプロキシとして間に入るApacheは、この切り替えの申し出をバックエンドへそのまま伝え、切り替わった後は接続を開いたまま中継し続ける必要があります。通常の ProxyPass だけではこのヘッダーが引き継がれないため、WebSocketを扱う設定を明示します。

必要なモジュールを有効にする

Ubuntuでは次のように有効にします。

sudo a2enmod proxy proxy_http headers
# 2.4.46以前の構成で書き換えルールを使う場合
sudo a2enmod proxy_wstunnel rewrite
sudo systemctl restart apache2

RHEL系(AlmaLinux・Rocky Linuxなど)では、これらのモジュールは httpd パッケージに含まれ、標準で読み込まれていることが多いです。httpd -M | grep proxy で確認できます。バージョンは apachectl -v で確かめてください。主要なディストリビューションの現行版が提供しているApacheは、多くが2.4.47より新しいバージョンです(2026年10月現在)。

Apache 2.4.47以降の書き方

ここでは、127.0.0.1:3000 で動くアプリのうち、/ws/ 以下をWebSocketとして公開する例を示します。

<VirtualHost *:443>
    ServerName example.com
    # SSL関連の設定は省略

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

    ProxyPass        "/ws/" "http://127.0.0.1:3000/ws/" upgrade=websocket timeout=600
    ProxyPassReverse "/ws/" "http://127.0.0.1:3000/ws/"
</VirtualHost>

upgrade=websocket を付けると、Upgrade: websocket ヘッダーを持つリクエストだけがWebSocketとして中継され、それ以外は通常のHTTPとして処理されます。Socket.IOのように、最初はHTTPのポーリングで接続し、後からWebSocketに切り替えるライブラリでも、この1行で両方を扱えます。

timeout=600 は、この経路でバックエンドからの応答やデータを待つ秒数です。理由は後のタイムアウトの節で説明します。

ProxyPreserveHost On は、ブラウザが送った Host ヘッダーをバックエンドへそのまま渡す設定です。アプリがホスト名を見て動作を変える場合や、Originの確認に使う場合に必要になります。X-Forwarded-Proto は、バックエンドから見て元の接続がHTTPSだったことを伝えるために付けています。

2.4.46以前の書き方

古いApacheでは upgrade= 引数が使えないため、mod_rewrite で Upgrade ヘッダーを見て、WebSocketのリクエストだけを ws:// へ送ります。

RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/ws/(.*) "ws://127.0.0.1:3000/ws/$1" [P,L]

ProxyPass        "/ws/" "http://127.0.0.1:3000/ws/"
ProxyPassReverse "/ws/" "http://127.0.0.1:3000/ws/"

1つ目のルールがWebSocketの接続を mod_proxy_wstunnel に渡し、ProxyPass がポーリングなど通常のHTTPを扱います。新しいApacheでもこの書き方は動きますが、2.4.47以降は upgrade=websocket のほうが設定が少なく、読み違いも起きにくいです。

タイムアウトで接続が切れるとき

「接続はできるが、しばらく何も送らないと切れる」という症状は、プロキシのタイムアウトが原因であることが多いです。Apacheは、プロキシ先からデータが届かない時間が ProxyTimeout(未設定なら Timeout、既定は60秒)を超えると接続を閉じます(どの設定が参照されるかはバージョンやモジュールによって異なる場合があるため、公式ドキュメントで手元のバージョンの説明を確認してください)。チャットのように、何も起きない時間が長い用途ではこれに当たります。

対策は2つあり、併用するのが一般的です。

  • **ProxyPassに timeout= を付ける**: 上の例のように、WebSocketの経路だけ長めにします。サーバー全体の Timeout を延ばすと、通常のリクエストまで長く待つことになるため、経路ごとの指定が向いています。
  • アプリ側で定期的にpingを送る: 数十秒ごとに小さなメッセージを送り合えば、プロキシから見て無通信の時間が生まれません。多くのWebSocketライブラリには、ping・pongの間隔を設定する項目があります。

前段にロードバランサーやCDNがある場合は、そちらにも独自のアイドルタイムアウトがあります。Apacheの値だけ延ばしても、より短い前段の設定で切られることがあるので、経路上のすべての機器の値を確認してください。

MPMと同時接続数

WebSocketの接続は、ブラウザがページを開いている間ずっと続きます。preforkのMPMでは1つの接続が1つのプロセスを占有するため、同時接続が増えるとプロセスが足りなくなり、通常のページ表示まで待たされます。WebSocketを中継するサーバーでは、eventのMPMを使い、PHPはPHP-FPMで動かす構成が向いています。現在のMPMは apachectl -V の出力の Server MPM で確認できます。

接続数が多い用途では、MaxRequestWorkers などの上限も見直します。変更は負荷の様子を見ながら段階的に行ってください。

WordPressと同じドメインで動かす場合

WordPressのサイトに、チャットや通知のための別アプリを /ws/ のようなパスで同居させる構成はよくあります。このとき気をつけたい点は次のとおりです。

  • パスを分ける: ProxyPass に書いたパスは、WordPressの .htaccess の書き換えより先に処理されるため、通常はWordPress側の設定を変える必要はありません。WordPressの投稿やページのスラッグと重ならないパスを選びます。
  • バックエンドは外から直接触れないようにする: アプリは 127.0.0.1 で待ち受け、ファイアウォールでもポートを閉じておきます。
  • Originを確認する: WebSocketには、ブラウザのCORSのような制限がかかりません。アプリ側で Origin ヘッダーが自分のサイトかを確認し、他のサイトのページからの接続を拒否します。
  • 認証を引き継ぐ: ログイン中のユーザーだけに使わせたい場合は、WordPress側で発行した短時間有効なトークンを接続時に渡し、アプリ側で検証する設計にします。WordPressのログインCookieをアプリ側で独自に解析する方法は、実装を誤りやすいため避けたほうが安全です。

HTTPSのページから接続するときは、ブラウザ側のURLを wss://example.com/ws/ にします。ws:// のままだと、混在コンテンツとしてブラウザに拒否されます。Apacheからバックエンドへの中継は同じサーバー内なら http:// のままで構いません。

動作確認

設定を変えたら、文法を確認して再読み込みします。

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

curl で切り替えの申し出を送り、101 Switching Protocols が返るかを確かめます。

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

101が返った後は接続が開いたままになるので、Ctrl+Cで終了します。ブラウザでは、開発者ツールのネットワークタブでWebSocketの接続を選ぶと、状態コードや送受信したメッセージを確認できます。

うまくいかないときの切り分け

  1. 502や503が返る: Apacheがバックエンドにつながっていません。エラーログに failed to make connection to backend のような記録があれば、アプリが起動しているか、ポート番号が合っているかを ss -lntp で確認します。RHEL系では、SELinuxがApacheからの外向き接続を止めていることがあり、その場合は sudo setsebool -P httpd_can_network_connect 1 で許可します。
  2. 101ではなく200や404が返る: upgrade=websocket が付いていない、または ProxyPass のパスとブラウザが接続しているパスが一致していません。末尾のスラッシュの有無も確認します。
  3. 一定時間で切れる: 前述のタイムアウトです。切れるまでの秒数を測ると、どの機器の設定かを推測しやすくなります。
  4. ローカルでは動くが本番で動かない: 前段のCDNやロードバランサーがWebSocketを通さない設定になっていないかを確認します。

まとめ

  • Apache 2.4.47以降は ProxyPass に upgrade=websocket を付けるだけでWebSocketを中継できます。古いバージョンは mod_proxy_wstunnel と書き換えルールを使います。
  • 無通信で切れる場合は、経路ごとの timeout= とアプリ側のpingで対処し、前段の機器のタイムアウトも確認します。
  • 同時接続が多い用途では、eventのMPMとPHP-FPMの構成が向いています。
  • バックエンドは外から直接触れないようにし、Originと認証はアプリ側で確認します。

お気軽にご相談ください

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