できない.dev

nginx「502 Bad Gateway」の原因と直し方

502 Bad Gateway は、error.log の理由を読み、転送先の起動と proxy_pass の宛先を直せば消える。
nginx が転送先から正しい応答を受け取れないと返すエラーで、多いのは connect() failed (111: Connection refused) である。

公開:

要約

nginx の 502 Bad Gateway は、転送先のアプリ(upstream)から正しい応答を受け取れないときに返る。
画面には理由が出ないので、error.log の行を読んで原因を分ける。

nginx の公式イメージでは error.log が標準エラーへ向いているので、docker logs で読める。
直接入れた場合は、多くの環境で /var/log/nginx/error.log に出る。

docker logs my-nginx 2>&1 | grep -E '\[(error|crit)\]'
tail -n 20 /var/log/nginx/error.log
ログの文言起きていること
connect() failed (111: Connection refused) while connecting to upstreamその宛先で待ち受けているものが無い
connect() to unix:/run/app.sock failed (2: No such file or directory)ソケットファイルが無い
upstream sent too big header while reading response header from upstream応答ヘッダがバッファに収まらない
SSL_do_handshake() failed (SSL: error:0A00010B:SSL routines::wrong version number)平文の転送先へ TLS で接続している

解決策

1. 転送先を起動して宛先を合わせる

ログの upstream: には、nginx が実際に接続した宛先が出ている。
そのアドレスへ nginx と同じ場所から接続して確かめる。

curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/

Failed to connect が出るなら、そのアドレスとポートで待ち受けているものが無い。
アプリを起動するか、proxy_pass を実際の待ち受けに合わせる。

2. コンテナでは localhost へ転送しない

同じネットワークのコンテナへは、サービス名かコンテナ名で転送する。
アプリ側の待ち受けは 0.0.0.0 にする。

location / {
    proxy_pass http://app:3000;
}

アプリがホストで動いているなら host.docker.internal へ転送する。
Linux の Docker Engine では、docker run に次のオプションを足すとこの名前が使える。

docker run --add-host=host.docker.internal:host-gateway nginx

3. 作り直したコンテナの名前を引き直す

reload すれば名前を引き直す。

nginx -s reload

毎回の reload を避けるなら、宛先を変数に入れて resolver を指定すると、リクエストのたびに名前が解決される。127.0.0.11 は、Docker のユーザー定義ネットワークの DNS である。

server {
    resolver 127.0.0.11 valid=10s;
    location / {
        set $upstream http://app:3000;
        proxy_pass $upstream;
    }
}

4. 応答ヘッダのバッファを増やす

upstream sent too big header が出ているときは、location にバッファの設定を足す。

location / {
    proxy_pass http://app:3000;
    proxy_buffer_size 16k;
    proxy_buffers 8 16k;
    proxy_busy_buffers_size 32k;
}

5. proxy_pass のスキームを合わせる

転送先が平文の HTTP なら、proxy_pass は http:// で書く。
見分け方は OpenSSL の記事にある。

よくある原因

  1. 転送先のアプリが止まっているか、proxy_pass のポート番号やソケットのパスが実際の待ち受けと違う。
    ログには 111: Connection refused と出る。
  2. コンテナの nginx から localhost へ転送している。
    コンテナの中の localhost は nginx のコンテナ自身を指す。
    アプリが 127.0.0.1 だけで待ち受けている場合も、別のコンテナからの接続は同じ 111 で拒否される。
  3. 転送先のコンテナを作り直して IP アドレスが変わった。proxy_pass に直接書いたホスト名は設定の読み込み時に解決されるので、nginx は古いアドレスへ接続し続ける。
  4. アプリの応答ヘッダが大きく、proxy_buffer_size に収まらない。
    既定値はメモリの 1 ページ分(4k か 8k)で、Set-Cookie などのヘッダが増えると超える。
  5. proxy_pass を https:// にしているが、転送先は平文の HTTP で待ち受けている。