公開日 2026-08-13

Dockerで「localhostに繋がらない」を解決する(WSL2でコンテナの待受アドレスを見分ける)

docker compose ps が Up でも localhost に繋がらないとき、コンテナ内の待受アドレスと ports の右側を突き合わせて、同じ症状になる3ケースを切り分けて直す手順を実測で追う。

目次

  1. 1. 検証環境と、クライアントごとに変わるエラー文言
  2. 2. 「表示は正常なのに繋がらない」状態を再現する
  3. 3. ログを見ると、届いていないことが分かる
  4. 4. 決定的な診断: コンテナの中で何のアドレスを待っているか
  5. 5. 直し方: 待受アドレスを 0.0.0.0 にする
  6. 6. 同じ症状になる別の原因が2つある
  7. 7. 待受行と ports の右側を突き合わせて見分ける
  8. 8. 片付けと、対処

docker compose psUp で、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 refusedcurl: (7) Failed to connect が出ている場合は状態が違います。 そちらはホスト側でそもそも待ち受けが無いときの形で、docker compose psPORTS にも公開ポートが出ていないはずです。 その症状は 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

STATUSUpPORTS0.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 psdocker 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.ymlcommand を更新します。変えるのは待受アドレスだけです。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 Address0.0.0.0:8000 です。本文も返ってきました。0.0.0.0 は「このコンテナが持つすべてのインターフェースで待つ」指定です。Docker の転送が使うインターフェースもここに含まれます。

6. 同じ症状になる別の原因が2つある

Recv failure: Connection reset by peer は、待受アドレスの問題だけで出るわけではありません。docker compose psUp に見える点まで同じです。

原因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: アプリが起動していない。 commandsleep 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 -aSTATUS を見てください。Restarting (1) 1 second ago のように出ていれば、 待受アドレスではなく起動失敗が原因です。原因Bと同じく docker compose logs app へ進みます。

7. 待受行と ports の右側を突き合わせて見分ける

クライアント側のメッセージは3つとも同じです。docker compose ps の表示も区別に使えません。 切り分けの材料は docker compose exec app netstat -ltnLocal Address ですが、それだけでは足りません。 分岐が決まるのは、compose.ymlports の右側(コンテナ側のポート番号)と突き合わせてからです。

手順は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 -aSTATUS を見て 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. 片付けと、対処

検証用のコンテナを削除します。このデモは portscommand だけの構成なので、消えて困るものはありません。 down はコンテナとネットワークを削除するため、自分の構成へ読み替えるときは、コンテナの中だけに置いたデータが無いか先に確認してください。

cd ~/projects/docker-localhost-connection-refused-demo
docker compose down

症状ごとの対処は次の5件です。7章の表と対応しています。

  • UpPORTS は正常なのに繋がらない → 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の食い違い) にあります。

シリーズ 3/4

このシリーズ

Dockerのエラーを原文から切り分ける

  1. 1. Dockerで「port is already allocated」を解決する(WSL2/Windowsでの原因特定と対処)
  2. 2. Dockerで「Permission denied」を解決する(WSL2のバインドマウントとUIDの食い違い)
  3. 3. Dockerで「localhostに繋がらない」を解決する(WSL2でコンテナの待受アドレスを見分ける) 現在の記事
  4. 4. Dockerで「no space left on device」を解決する(WSL2でDockerのディスクとマウント先を見分ける)