Nginxをリバースプロキシにして、Dockerのコンテナで動くアプリにつないだとき、502 Bad Gatewayが出る原因と直し方を、Nginxのerror.logのメッセージごとにまとめた記事です。Docker Composeで、Nginxと小さなアプリを並べて原因を1つずつ起こし、curlの応答とerror.logに出た行を載せています。似た場面で出る504 Gateway Time-outも、実際にどちらになるかを確かめました。
結論
error.logのメッセージと、今回返ったステータス、原因、直し方です。Nginxは公式のDockerイメージ nginx:1.30.6(Debian版)、アプリはPythonで書いた小さなHTTPサーバーです。
| error.logのメッセージ | ステータス | 原因(今回起こしたもの) | 直し方 |
|---|---|---|---|
connect() failed (111: Connection refused) while connecting to upstream | 502 | ポートが違う / localhost を指している / アプリが 127.0.0.1 で待ち受けている | 正しいポートにする / localhost ではなくComposeのサービス名を書く / アプリを 0.0.0.0 で待ち受けさせる |
upstream prematurely closed connection while reading response header from upstream | 502 | アプリが応答を返す前に接続を閉じた | アプリのログで、応答を返せなかった原因を調べる |
upstream timed out (110: Connection timed out) while reading response header from upstream | 504 | アプリの応答が proxy_read_timeout より遅い | アプリを速くする / proxy_read_timeout を延ばす |
upstream timed out (110: Connection timed out) while connecting to upstream | 504(60秒後) | アプリのコンテナが止まっている | アプリのコンテナを起動する |
[emerg] host not found in upstream "app" | 応答なし(Nginxが起動しない) | Nginxの起動時に、アプリのコンテナが動いていない | アプリを先に起動する / upstream の resolve を使う |
no live upstreams while connecting to upstream | 502 | resolve を使っていて、アプリのコンテナが止まっている | アプリのコンテナを起動する(Nginxの再起動はいらない) |
502と504は、どちらも「Nginxが後ろのサーバー(upstream)から応答をもらえなかった」ときに出ますが、意味が違います。HTTPの仕様(RFC 9110)では、502は後ろのサーバーから正しくない応答を受け取ったとき、504は後ろのサーバーから時間内に応答がなかったときのステータスです。今回も、接続を断られた・途中で切られた場合は502、タイムアウトの場合は504でした。アプリのコンテナが止まっている場合は、すぐに502になるのではなく、60秒待ってから504になりました。
動作確認した環境
- Nginx 1.30.6(Dockerイメージ
nginx:1.30.6、digestで固定。2026年11月9日時点の安定版) - Python 3.15.2(Dockerイメージ
python:3.15.2-alpine、digestで固定。標準ライブラリだけを使用) - Docker 29.7.2、Docker Compose v5.5.1(Windows 11 の Docker Desktop、x86_64)
- curl 8.14.1(Git for Windows に付属のもの)、curl.exe 8.21.0(Windowsに付属のもの)
- 確認日: 2026年11月9日
この記事は、MacとWindowsの両方の読者を対象にしています。NginxとアプリはDockerのコンテナで動かすので、使うのは docker と curl のコマンドだけです。この記事のコマンドは、Claude(AI)が実行して確認しました。本文の出力は、Windows 11 のGit Bashから上から順に実行した結果です。PowerShellで実行した結果は、最後の章にまとめました。Macでは確認していません。
準備
空のフォルダに、次の3つのファイルを作ります。まず compose.yaml です。app は普通に動くアプリ、app-local は同じアプリを 127.0.0.1 で待ち受けさせたものです(原因の3つ目で使います)。ホストのポートは、確認用に19101にしています。
services:
nginx:
image: nginx:1.30.6@sha256:42c68eab6f7e22611786a9fe1d1c019e275c50185bd54b8f75dc2b9184322703
ports:
- "19101:80"
volumes:
- ./default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
- app-local
app:
image: python:3.15.2-alpine@sha256:a0f2e268f2ce8f3709611a90054ec6a8ae31f6251f5896b8648b9bb99835e30a
command: ["python", "/app/app.py"]
volumes:
- ./app.py:/app/app.py:ro
app-local:
image: python:3.15.2-alpine@sha256:a0f2e268f2ce8f3709611a90054ec6a8ae31f6251f5896b8648b9bb99835e30a
command: ["python", "/app/app.py"]
environment:
HOST: 127.0.0.1
volumes:
- ./app.py:/app/app.py:ro
depends_on は、app と app-local を先に起動するための設定です。Docker Composeの公式ドキュメントによると、Composeが待つのはコンテナが動き始めるまでで、アプリが応答できるようになるまでは待ちません。確認の1回目は depends_on なしで docker compose up -d を実行し、Nginxがアプリより先に起動して、すぐに止まりました(原因の6つ目と同じエラーです)。
次に、後ろに置くアプリ app.py です。どのパスにも200を返しますが、/close は応答を返さずに接続を閉じ、/slow は5秒待ってから応答します。
import os
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HOST = os.environ.get("HOST", "0.0.0.0")
PORT = 8000
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/close":
# 応答を返さずに接続を閉じる(処理の途中でアプリが落ちた場合の代わり)
self.close_connection = True
return
if self.path == "/slow":
time.sleep(5)
body = f"hello from app ({self.path})\n".encode()
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
print(f"listening on {HOST}:{PORT}", flush=True)
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
最後に、Nginxの設定 default.conf です。原因ごとに location を分けています。/ が正しい設定で、ほかはわざと間違えたものです。
server {
listen 80;
location / {
proxy_pass http://app:8000;
}
location /wrong-port/ {
proxy_pass http://app:3000/;
}
location /localhost/ {
proxy_pass http://localhost:8000/;
}
location /bind-local/ {
proxy_pass http://app-local:8000/;
}
location /slow/ {
proxy_pass http://app:8000/slow;
proxy_read_timeout 3s;
}
location /slow-fixed/ {
proxy_pass http://app:8000/slow;
proxy_read_timeout 10s;
}
}
WindowsのGit Bashで実行する場合は、先に export MSYS_NO_PATHCONV=1 を実行してください。Git Bashは、/var/log/nginx のような引数をWindowsのパスに変換することがあり、Dockerの公式ドキュメントでもこの設定が案内されています。本文の確認でも、Git Bashでこの設定をして実行しました。
起動して、正しい設定の / にcurlでアクセスします。docker compose up -d の出力は省略しています。起動の直後は、アプリがまだ待ち受けを始めていないことがあるので、確認では docker compose up -d と docker compose restart の後に、3秒待ってから次のコマンドを実行しました。
$ docker compose up -d
$ docker compose ps -a --format "table {{.Service}}\t{{.Status}}"
SERVICE STATUS
app Up 4 seconds
app-local Up 3 seconds
nginx Up 3 seconds
$ curl -sS -i http://localhost:19101/
HTTP/1.1 200 OK
Server: nginx/1.30.6
Date: Mon, 09 Nov 2026 00:24:03 GMT
Content-Type: text/plain
Content-Length: 19
Connection: keep-alive
hello from app (/)
error.logの見方
502が出たら、まずNginxのerror.logを見ます。公式のNginxのイメージでは、ログのファイルが標準出力と標準エラー出力へのリンクになっていました。そのため、ファイルを開かなくても docker compose logs nginx でerror.logの行が見られます。
$ docker compose exec nginx ls -l /var/log/nginx
total 0
lrwxrwxrwx 1 root root 11 Nov 5 07:18 access.log -> /dev/stdout
lrwxrwxrwx 1 root root 11 Nov 5 07:18 error.log -> /dev/stderr
docker compose logs nginx には、アクセスログの行と、起動時の [notice] の行も混ざります。以下では、各原因で増えた行のうち [error]、[warn]、[emerg] の行を載せます。
原因別のエラーと直し方
ポートが違う(Connection refused)
/wrong-port/ です。アプリは8000番で待ち受けているのに、proxy_pass に3000番を書いています。curlには、Nginxの502のページが返りました。
$ curl -sS -i http://localhost:19101/wrong-port/
HTTP/1.1 502 Bad Gateway
Server: nginx/1.30.6
Date: Mon, 09 Nov 2026 00:24:04 GMT
Content-Type: text/html
Content-Length: 157
Connection: keep-alive
<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.30.6</center>
</body>
</html>
[error.log]
2026/11/09 00:24:04 [error] 22#22: *3 connect() failed (111: Connection refused) while connecting to upstream, client: 172.19.0.1, server: , request: "GET /wrong-port/ HTTP/1.1", upstream: "http://172.19.0.2:3000/", host: "localhost:19101"
upstream: に、Nginxが実際につなごうとした先(app のIPアドレスと3000番)が出ます。Connection refused は、そのアドレスに相手のコンテナはあるものの、そのポートで待ち受けているプログラムがないときに出ます。
Nginxのコンテナの中から、後ろのアプリに直接つないでみると、どちらに問題があるかを切り分けられます。今回のNginxのイメージ(Debian版)には、curlが入っていました。
$ docker compose exec nginx curl -sS http://app:3000/
curl: (7) Failed to connect to app port 3000 after 0 ms: Could not connect to server
$ docker compose exec nginx curl -sS http://app:8000/
hello from app (/)
直し方は、proxy_pass のポートを、アプリが待ち受けているポート(今回は8000)にすることです。/ の proxy_pass http://app:8000; では200が返っています。Composeの ports に書くホスト側のポートではなく、コンテナの中でアプリが待ち受けているポートを書きます。
以降は、curlの出力の1行目だけを載せます。ヘッダーの残りと本文は、上と同じ形でした。
localhost を指している(Connection refused)
/localhost/ です。proxy_pass http://localhost:8000/; と書いています。
$ curl -sS -i http://localhost:19101/localhost/
HTTP/1.1 502 Bad Gateway
[error.log]
2026/11/09 00:24:08 [error] 23#23: *5 connect() failed (111: Connection refused) while connecting to upstream, client: 172.19.0.1, server: , request: "GET /localhost/ HTTP/1.1", upstream: "http://127.0.0.1:8000/", host: "localhost:19101"
2026/11/09 00:24:08 [warn] 23#23: *5 upstream server temporarily disabled while connecting to upstream, client: 172.19.0.1, server: , request: "GET /localhost/ HTTP/1.1", upstream: "http://127.0.0.1:8000/", host: "localhost:19101"
2026/11/09 00:24:08 [error] 23#23: *5 connect() failed (111: Connection refused) while connecting to upstream, client: 172.19.0.1, server: , request: "GET /localhost/ HTTP/1.1", upstream: "http://127.0.0.1:8000/", host: "localhost:19101"
2026/11/09 00:24:08 [warn] 23#23: *5 upstream server temporarily disabled while connecting to upstream, client: 172.19.0.1, server: , request: "GET /localhost/ HTTP/1.1", upstream: "http://127.0.0.1:8000/", host: "localhost:19101"
コンテナの中の localhost は、そのコンテナ自身です。Nginxのコンテナの中には8000番で待ち受けているものがないので、断られました。今回の実行では、1回のリクエストで接続を2回試していて、[error] と [warn] の行が2回ずつ出ました。
直し方は、localhost ではなく、Composeのサービス名(今回は app)を書くことです。Composeでは、同じネットワークのコンテナどうしがサービス名でつながります(Dockerの公式ドキュメント)。
アプリが 127.0.0.1 で待ち受けている(Connection refused)
/bind-local/ です。Nginxの設定は正しく、サービス名とポートも合っていますが、アプリ(app-local)が 127.0.0.1 で待ち受けています。
$ curl -sS -i http://localhost:19101/bind-local/
HTTP/1.1 502 Bad Gateway
[error.log]
2026/11/09 00:24:11 [error] 24#24: *8 connect() failed (111: Connection refused) while connecting to upstream, client: 172.19.0.1, server: , request: "GET /bind-local/ HTTP/1.1", upstream: "http://172.19.0.3:8000/", host: "localhost:19101"
error.logの形は、ポートが違う場合と同じです。違いは、アプリのログに出ている待ち受けのアドレスで分かります。
$ docker compose logs app-local
app-local-1 | listening on 127.0.0.1:8000
$ docker compose logs app | grep listening
app-1 | listening on 0.0.0.0:8000
PowerShellでは grep の代わりに Select-String を使い、docker compose logs app | Select-String listening とします(同じ出力になることを確かめました)。
127.0.0.1 で待ち受けると、そのコンテナの中からの接続しか受け付けないので、Nginxのコンテナからの接続は断られます。直し方は、アプリを 0.0.0.0(すべてのアドレス)で待ち受けさせることです。0.0.0.0 で待ち受けている app では200が返っています。アプリのフレームワークや開発用サーバーで待ち受けのアドレスを指定する方法は、それぞれのドキュメントを確認してください。
応答の前にアプリが接続を閉じた(upstream prematurely closed connection)
/close です。/ の設定のまま、アプリが応答を返さずに接続を閉じるパスにアクセスします。処理の途中でアプリのプロセスが落ちた場合などの代わりです。
$ curl -sS -i http://localhost:19101/close
HTTP/1.1 502 Bad Gateway
[error.log]
2026/11/09 00:24:14 [error] 25#25: *10 upstream prematurely closed connection while reading response header from upstream, client: 172.19.0.1, server: , request: "GET /close HTTP/1.1", upstream: "http://172.19.0.2:8000/close", host: "localhost:19101"
接続はできたものの、応答のヘッダーを読む前にアプリが接続を閉じたというメッセージです。Nginxの設定ではなくアプリの側の問題なので、同じ時刻のアプリのログ(docker compose logs app)で、例外やプロセスの終了がないかを調べます。
アプリが遅い(504 Gateway Time-out)
/slow/ です。アプリは5秒かけて応答しますが、Nginxの proxy_read_timeout を3秒にしています。-w で、かかった時間も表示しました。
$ curl -sS -i -w "time: %{time_total}s\n" http://localhost:19101/slow/
HTTP/1.1 504 Gateway Time-out
(中略)
time: 3.221455s
[error.log]
2026/11/09 00:24:21 [error] 26#26: *12 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 172.19.0.1, server: , request: "GET /slow/ HTTP/1.1", upstream: "http://172.19.0.2:8000/slow", host: "localhost:19101"
タイムアウトは、502ではなく504になりました。proxy_read_timeout の既定値は60秒です。公式ドキュメントによると、これは応答全体にかかる時間ではなく、2回の読み込みの間の時間です。
直し方は、まずアプリが遅い原因を調べることです。時間のかかる処理だと分かっている場合は、proxy_read_timeout を延ばします。10秒にした /slow-fixed/ では、5秒後に200が返りました。
$ curl -sS -i -w "time: %{time_total}s\n" http://localhost:19101/slow-fixed/
HTTP/1.1 200 OK
Server: nginx/1.30.6
Date: Mon, 09 Nov 2026 00:24:29 GMT
Content-Type: text/plain
Content-Length: 23
Connection: keep-alive
hello from app (/slow)
time: 5.218313s
Alpine版のイメージ nginx:1.30.6-alpine でも同じ設定を試したところ、ステータスは同じ504で、メッセージは upstream timed out (110: Operation timed out) になりました。エラーの説明の文字列は、イメージのC言語のライブラリによって変わるようです。検索するときは upstream timed out の部分で探すと見つけやすいです。
アプリのコンテナが止まっている(504、またはNginxが起動しない)
Nginxが動いたまま、アプリのコンテナを止めます。
$ docker compose stop app
$ curl -sS -i -w "time: %{time_total}s\n" http://localhost:19101/
HTTP/1.1 504 Gateway Time-out
(中略)
time: 60.271900s
[error.log]
2026/11/09 00:25:36 [error] 28#28: *16 upstream timed out (110: Connection timed out) while connecting to upstream, client: 172.19.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://172.19.0.2:8000/", host: "localhost:19101"
すぐに502にはならず、60秒待ってから504になりました。メッセージの最後が while connecting to upstream なので、アプリが遅いときとは違い、接続の段階でタイムアウトしています。60秒は proxy_connect_timeout の既定値です。
upstream: には、止まる前の app のIPアドレス(172.19.0.2)が出ています。Nginxは、proxy_pass に書いた名前を起動時にIPアドレスに変えて、その後も使い続けていました。止まったコンテナのアドレスからは返事がないので、接続を待ち続けたと考えられます。今回の環境(WindowsのDocker Desktop)での結果で、ほかの環境では違うエラーになるかもしれません(未確認)。
アプリが止まったまま、Nginxを再起動すると、Nginxは起動しませんでした。
$ docker compose restart nginx
$ docker compose ps -a --format "table {{.Service}}\t{{.Status}}"
SERVICE STATUS
app Exited (137) About a minute ago
app-local Up About a minute
nginx Exited (1) Less than a second ago
$ curl -sS -i http://localhost:19101/
curl: (7) Failed to connect to localhost port 19101 after 2257 ms: Could not connect to server
[error.log]
2026/11/09 00:25:40 [emerg] 1#1: host not found in upstream "app" in /etc/nginx/conf.d/default.conf:5
nginx: [emerg] host not found in upstream "app" in /etc/nginx/conf.d/default.conf:5
Nginxは起動時に app の名前を引きますが、止まっているコンテナの名前は引けないので、エラーで終了しました。この場合、ブラウザやcurlには502すら返らず、接続できないエラーになります。準備の章で書いた、depends_on なしで起動したときも、このエラーでした。
直し方は、アプリのコンテナを起動することです。Nginxも止まっているときは、docker compose up -d で両方を起動します。
$ docker compose up -d
$ curl -sS http://localhost:19101/
hello from app (/)
Docker Composeのドキュメントにも、設定を変えたときなどにコンテナを作り直すと新しいIPアドレスになり、古いアドレスは使えなくなると書かれています。IPアドレスではなくサービス名で指定するのが前提ですが、Nginxが起動時に引いたアドレスを使い続ける点には注意が必要です。
アプリが止まっていてもNginxを起動できるようにする(upstream の resolve)
Nginx 1.27.3 からは、upstream の server につける resolve を、無料版でも使えます(それまでは有料版の機能)。公式ドキュメントによると、resolve は名前に対応するIPアドレスの変化を見て、Nginxを再起動せずに設定を変えます。zone(共有メモリ)と resolver の指定が必要です。127.0.0.11 は、Dockerのネットワークの中で使える組み込みのDNSサーバーのアドレスです(Dockerの公式ドキュメント)。default.conf を次の内容にしました。
upstream app_backend {
zone app_backend 64k;
resolver 127.0.0.11 valid=10s;
server app:8000 resolve;
}
server {
listen 80;
location / {
proxy_pass http://app_backend;
}
}
アプリを止めてからNginxを再起動すると、今度はNginxが起動し、すぐに502が返りました。
$ docker compose stop app
$ docker compose restart nginx
$ docker compose ps -a --format "table {{.Service}}\t{{.Status}}"
SERVICE STATUS
app Exited (137) 5 seconds ago
app-local Up 2 minutes
nginx Up 3 seconds
$ curl -sS -i http://localhost:19101/
HTTP/1.1 502 Bad Gateway
[error.log]
2026/11/09 00:26:02 [error] 21#21: app could not be resolved (3: Host not found)
2026/11/09 00:26:02 [error] 22#22: *2 no live upstreams while connecting to upstream, client: 172.19.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://app_backend/", host: "localhost:19101"
app の名前が引けず、つなぐ先が1つもない(no live upstreams)ので、502になっています。アプリのコンテナを起動して20秒待つと、Nginxを再起動しなくても200が返りました。
$ docker compose start app
$ sleep 20
$ curl -sS http://localhost:19101/
hello from app (/)
名前を引き直す間隔は、valid の値で変わります。今回は10秒にして、20秒待ってから確かめました。
確かめ終わったら、docker compose down でコンテナとネットワークを消します(出力は省略)。
$ docker compose down
WindowsのPowerShellで実行する場合
Windows 11 のPowerShell 7.6 で、同じ3つのファイルを改行をCRLF(Windowsのエディタで保存したとき)にして作り、docker compose up -d から試しました。PowerShellでは、curl ではなく curl.exe と打ちます。Windows PowerShell 5.1 では、curl は Invoke-WebRequest の別名になっていました(Get-Command curl で確認)。
PS> curl.exe -sS -i http://localhost:19101/wrong-port/
HTTP/1.1 502 Bad Gateway
(中略)
PS> curl.exe -sS -i -w "time: %{time_total}s\n" http://localhost:19101/slow/
HTTP/1.1 504 Gateway Time-out
(中略)
time: 3.213056s
CRLFのファイルでもNginxとアプリは起動し、200、502、504の応答は本文と同じでした。error.logの [error] の行は、docker compose logs nginx | Select-String "\[error\]" で取り出し、connect() failed (111: Connection refused) と upstream timed out (110: Connection timed out) の行が本文と同じ形で出ることを確かめました。Windows PowerShell 5.1 でも、curl.exe で502が返ることを確かめました。Macのターミナルでも本文と同じコマンドを使う想定ですが、確認はしていません。
参考
- Module ngx_http_proxy_module(
proxy_pass、proxy_read_timeout、proxy_connect_timeout) - Module ngx_http_upstream_module(
resolve、zone、resolver) - RFC 9110 HTTP Semantics(15.6.3 502 Bad Gateway、15.6.5 504 Gateway Timeout)
- Networking in Compose | Docker Docs
- Control startup and shutdown order in Compose | Docker Docs(
depends_on) - Networking overview | Docker Docs(組み込みのDNSサーバー
127.0.0.11) - Troubleshoot topics for Docker Desktop | Docker Docs(Git Bashのパス変換)
- nginx: download(安定版の確認)

コメント