docker compose ps は Up で、PORTS にも 0.0.0.0:8080->8000/tcp が出ている。それでもブラウザや curl から localhost:8080 に繋がらない。この記事では、その状態を Docker Desktop + WSL2 環境で再現し、コンテナの中の待受アドレスと ports の指定を突き合わせて原因を絞り込むところまでを、実際の出力で追います。非対象は HTTPS/リバースプロキシ経由の疎通、Kubernetes、外部ホストからのアクセス設計です。
1. 検証環境と、クライアントごとに変わるエラー文言
- Windows 11
- WSL2(Ubuntu)
- Docker Desktop 4.55.0(Docker Engine 29.1.3)
php:8.4-cli-alpine(PHP 8.4.24)- 確認に使ったクライアント: WSL 側
curl 8.5.0/ Windows 側curl.exe 8.21.0/ PowerShell 7.6.3
同じ原因でも、どこから叩いたかで文言が変わります。 WSL 側の curl ではこの形です。
curl: (56) Recv failure: Connection reset by peer
Windows の curl.exe だと番号ごと変わります。
curl: (52) Empty reply from server
PowerShell の Invoke-WebRequest が返すのは、原因の手掛かりを持たない1行だけです。
An error occurred while sending the request.
3つとも同じ状態を指しています。片方の文字列で検索して見つからないときは、もう片方でも引いてみてください。
一方、Connection refused や curl: (7) Failed to connect が出ている場合は状態が違います。
そちらはホスト側でそもそも待ち受けが無いときの形で、docker compose ps の PORTS にも公開ポートが出ていないはずです。
その症状は Dockerで「port is already allocated」を解決する(WSL2/Windowsでの原因特定と対処) の8章が扱います。
この記事が対象にするのは、公開ポートは出ているのに接続が切られる場合です。
2. 「表示は正常なのに繋がらない」状態を再現する
Windows のターミナルで wsl を実行して Ubuntu に入り、作業ディレクトリを作成します。
# Windows側から始める場合のみ実行
# wsl
mkdir -p ~/projects/docker-localhost-connection-refused-demo/app
cd ~/projects/docker-localhost-connection-refused-demo
code .
app/index.php を作成します。返す内容は疎通の確認だけに使う1行です。
<?php
echo "Hello from the container\n";
compose.yml を作成します。ports は正しく書いてあり、command の待受アドレスだけが 127.0.0.1 です。
services:
app:
image: php:8.4-cli-alpine
working_dir: /app
volumes:
- ./app:/app
ports:
- "8080:8000"
command: php -S 127.0.0.1:8000 -t /app
起動して状態を確認します。
docker compose up -d
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
docker-localhost-connection-refused-demo-app-1 php:8.4-cli-alpine "docker-php-entrypoi…" app 2 seconds ago Up 2 seconds 0.0.0.0:8080->8000/tcp, [::]:8080->8000/tcp
STATUS は Up、PORTS は 0.0.0.0:8080->8000/tcp です。公開ポートの割り当ても通っています。
docker port $(docker compose ps -q app)
8000/tcp -> 0.0.0.0:8080
8000/tcp -> [::]:8080
ここまでの表示に異常はゼロです。それでも繋がりません。
curl http://localhost:8080/
curl: (56) Recv failure: Connection reset by peer
docker compose ps と docker port は、コンテナの中で何が起きているかを見ていません。 どちらもホストからコンテナへの転送設定を表示しているだけで、転送先でプロセスが待っているかどうかは対象外です。
3. ログを見ると、届いていないことが分かる
接続を試したあとにログを読みます。
docker compose logs app
app-1 | [...] PHP 8.4.24 Development Server (http://127.0.0.1:8000) started
行頭の日時は省略しています。読む点は2つです。
- 起動行が待受アドレスを宣言しています。
http://127.0.0.1:8000と書いてあり、0.0.0.0ではありません - アクセスログが1行も増えていません。
curlを叩いたのにリクエストが記録されていない、つまりアプリまで届いていない
対比のため、後で直したあとの同じコマンドの結果を先に示します。正常なら接続のたびに3行増える形です。
app-1 | [...] PHP 8.4.24 Development Server (http://0.0.0.0:8000) started
app-1 | [...] 172.21.0.1:49184 Accepted
app-1 | [...] 172.21.0.1:49184 [200]: GET /
app-1 | [...] 172.21.0.1:49184 Closing
172.21.0.1 はコンテナから見たゲートウェイのアドレスです。ホストの curl は Docker の転送を経由して届くため、送信元がこの形になります。
4. 決定的な診断: コンテナの中で何のアドレスを待っているか
コンテナの中で待受アドレスを直接確認します。
docker compose exec app netstat -ltn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.11:42511 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:8000 0.0.0.0:* LISTEN
見る列は Local Address で、読む点は4つです。
- 照合するのは
portsの右側の番号を持つ行だけです。 ここでは"8080:8000"の右側なので8000の行を探します。番号が違う行は、いま繋がらない原因とは別のプロセスです - その
8000の行が127.0.0.1で始まっています。 コンテナ内のループバックだけで待っているので、コンテナ外から来た接続は受け取れません 127.0.0.11の行は待受判定から外します。 Compose が作るネットワークに接続したコンテナで動く Docker の内部 DNS です。docker runで既定のbridgeに繋いだ場合は出ないので、この行の有無をアプリの生死の判断には使わないでくださいForeign Address列の0.0.0.0:*は待受アドレスではありません。 「接続元を限定しない」という意味で、両方の行に出ます。ここを見て0.0.0.0で待っていると読み違えないでください
IPv6 で待つアプリでは表記が変わります。すべてのインターフェースなら :::8000、ループバック限定なら ::1:8000 です。
読み方は IPv4 と対応していて、0.0.0.0 と ::: が全インターフェース、127.0.0.1 と ::1 がループバック限定になります。
アプリ自体は動作中です。コンテナの中から叩けば応答します。
docker compose exec app php -r 'echo file_get_contents("http://127.0.0.1:8000/");'
Hello from the container
ports の右側と一致する行があり、それが 127.0.0.1(または ::1)で始まっていて、コンテナの中からは応答する。
この3つが揃ったときの原因が待受アドレスです。1つでも欠けるなら6章と7章で他の原因を見てください。
5. 直し方: 待受アドレスを 0.0.0.0 にする
compose.yml の command を更新します。変えるのは待受アドレスだけです。ports は触りません。
services:
app:
image: php:8.4-cli-alpine
working_dir: /app
volumes:
- ./app:/app
ports:
- "8080:8000"
command: php -S 0.0.0.0:8000 -t /app
起動し直して確認します。
docker compose up -d
docker compose exec app netstat -ltn
curl http://localhost:8080/
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.11:34619 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN
Hello from the container
これで Local Address は 0.0.0.0:8000 です。本文も返ってきました。0.0.0.0 は「このコンテナが持つすべてのインターフェースで待つ」指定です。Docker の転送が使うインターフェースもここに含まれます。
6. 同じ症状になる別の原因が2つある
Recv failure: Connection reset by peer は、待受アドレスの問題だけで出るわけではありません。docker compose ps が Up に見える点まで同じです。
原因A: ports の右側がアプリの待受ポートと違う。 "8080:9999" と書きながらアプリは 8000 で待っている状態です。
NAME PORTS
docker-localhost-connection-refused-demo-app-1 0.0.0.0:8080->9999/tcp, [::]:8080->9999/tcp
curl: (56) Recv failure: Connection reset by peer
netstat を見ると、待受は正しく 0.0.0.0 です。食い違っているのはポート番号になります。
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN
原因B: アプリが起動していない。 command が sleep infinity のまま、あるいはアプリが落ちた場合です。コンテナは Up のままなので、docker compose ps からは気づけません。
NAME STATUS PORTS
docker-localhost-connection-refused-demo-app-1 Up 2 seconds 0.0.0.0:8080->8000/tcp, [::]:8080->8000/tcp
curl: (56) Recv failure: Connection reset by peer
このとき netstat には Docker の内部 DNS しか出ません。待受行そのものが存在しない状態です。
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.11:35299 0.0.0.0:* LISTEN
コンテナが再起動を繰り返している場合は、netstat を打つ前に exec 自体が拒否されます。
Error response from daemon: Container e1da4c7e9e69... is restarting, wait until the container is running
このメッセージが出たら docker compose ps -a の STATUS を見てください。Restarting (1) 1 second ago のように出ていれば、
待受アドレスではなく起動失敗が原因です。原因Bと同じく docker compose logs app へ進みます。
7. 待受行と ports の右側を突き合わせて見分ける
クライアント側のメッセージは3つとも同じです。docker compose ps の表示も区別に使えません。
切り分けの材料は docker compose exec app netstat -ltn の Local Address ですが、それだけでは足りません。
分岐が決まるのは、compose.yml の ports の右側(コンテナ側のポート番号)と突き合わせてからです。
手順は2段です。まず ports の右側の番号を確認し、次に netstat にその番号の待受行があるかを見ます。
ports 右側の番号を持つ待受行 | その行のアドレス | 原因 | 直す場所 |
|---|---|---|---|
| ある | 127.0.0.1 または ::1 | 待受アドレスがループバック限定 | アプリの待受アドレスを 0.0.0.0 へ(5章) |
| ある | 0.0.0.0 または ::: | この記事の3つではない | 扱っていない。docker compose logs app とアプリ側のログから追う |
| 無い(別番号の待受行はある) | — | ports の右側の指定ミス | ports 右側をアプリの待受ポートに合わせる(6章 原因A) |
| 無い(待受行が1つも無い) | — | アプリが起動していない | docker compose logs app で起動失敗を読む(6章 原因B) |
exec が拒否される | — | 再起動を繰り返している | docker compose ps -a の STATUS を見て docker compose logs app へ |
2行目が、待受アドレスもポートも揃っているのに繋がらない場合です。この記事で再現したのは残りの3つで、それ以外の要因は検証していません。
127.0.0.11 の行は Docker の内部 DNS なので、どの判定でも数に入れないでください。
flowchart TD
A["localhost に繋がらない"] --> B["docker compose exec app netstat -ltn"]
B --> R{"exec は通ったか"}
R -->|"拒否された"| L["再起動を繰り返している"]
R -->|"通った"| C{"ports 右側の番号を持つ待受行"}
C -->|"ある: 127.0.0.1 か ::1"| D["待受アドレスを 0.0.0.0 へ"]
C -->|"ある: 0.0.0.0 か :::"| H["この記事の3つではない"]
C -->|"無い: 別番号の行はある"| E["ports の右側を合わせる"]
C -->|"無い: 待受行が1つも無い"| F["アプリが起動していない"]
F --> G["docker compose logs app を読む"]
L --> G
ports の左右そのものの意味は Docker初心者向け: compose.yml の中身を1項目ずつ読む入門 で扱っています。起動時点で PORTS に何も出ない場合は、そもそも公開に失敗しているので Dockerで「port is already allocated」を解決する(WSL2/Windowsでの原因特定と対処) を先に見てください。
8. 片付けと、対処
検証用のコンテナを削除します。このデモは ports と command だけの構成なので、消えて困るものはありません。
down はコンテナとネットワークを削除するため、自分の構成へ読み替えるときは、コンテナの中だけに置いたデータが無いか先に確認してください。
cd ~/projects/docker-localhost-connection-refused-demo
docker compose down
症状ごとの対処は次の5件です。7章の表と対応しています。
UpとPORTSは正常なのに繋がらない → 4章(portsの右側とnetstatを突き合わせる)- 待受行が
127.0.0.1か::1で、コンテナの中からは応答がある → 5章(待受アドレスを0.0.0.0へ) portsの右側と同じ番号の待受行が無い → 6章の原因A(portsの右側を合わせる)- 待受行が1つも無い、または
execが拒否される → 6章の原因B(ログで起動失敗を読む) portsの右側と一致する行が0.0.0.0か:::で待っている → この記事の3つには当てはまらない(7章の表2行目)
同じシリーズの、コンテナが作ったファイルを編集できなくなる話は Dockerで「Permission denied」を解決する(WSL2のバインドマウントとUIDの食い違い) にあります。