docker『Connection Refused』の原因と対処法

docker『Connection Refused』の原因と対処法

「Connection refused」は、TCPレベルで「そのアドレス・ポートに対応するプロセスが現在いない」ことを意味する。Dockerでは、コンテナ内アプリが指定ポートで待ち受けていない、ホスト側ポートのマッピングミス、コンテナ間ネットワークの設定違い、ファイアウォールやプロキシの干渉、起動順の問題などで発生する。まず「どこからどこに」接続しているのかを明確にし、ホスト・コンテナ・ネットワークを切り分けることが重要になる。

エラーの意味と発生条件(まず押さえるポイント)

「Connection refused」は以下の条件で発生する。

・接続先IP/ホスト名は解決できている(DNSはOK)。

・そのIPの指定ポートには、リッスンしているプロセスがいない、もしくはカーネルが受付を拒否している。

・docker環境では、次のようなときに出やすい。

  • コンテナ内アプリがまだ起動していない、または異常終了している。
  • アプリが127.0.0.1で待ち受けていて、0.0.0.0で公開されていない。
  • -p / ports のマッピングが誤っている、またはポートが違う。
  • コンテナ間で間違ったホスト名やポートを指定している。
  • ホスト側のファイアウォールやセキュリティ製品が遮断している。

どこからどこに繋いでいるかを明確にする(3パターン)

dockerでの「Connection refused」は大きく3パターンに分けられる。

1) ホスト → コンテナ(ブラウザで http://localhost:8080 にアクセスなど)

2) コンテナ → 外部サービス(DBやAPIサーバ、SaaSなど)

3) コンテナ → コンテナ(docker compose で app → db など)

それぞれに対して、次のような順で確認するとよい。

・使っているIP/ホスト名・ポートの組み合わせは正しいか。

・実際に、そのIPのそのポートでプロセスがLISTENしているか。

・dockerのネットワークとポートマッピングの設定が意図通りか。

ホスト→コンテナ:ポートマッピングとリッスンアドレスを確認

# ホスト側で「どのポートが何のコンテナに繋がっているか」を確認
docker ps --format 'table {{.Names}}\t{{.Ports}}'

# Linux/macOSでポートのLISTEN状況を確認
ss -ltnp | grep 8080 || lsof -iTCP:8080 -sTCP:LISTEN

# コンテナ内でアプリがどこでLISTENしているかを確認
docker exec -it <container> sh -lc "netstat -ltnp || ss -ltnp"

典型的なミスとして、アプリが127.0.0.1:8080で待ち受けており、コンテナ外部からは接続できないケースがある。

この場合、アプリ側の設定を0.0.0.0:8080で待ち受けるよう変更する。

# 例: Express(Node.js) の場合
app.listen(8080, '0.0.0.0', () => {
  console.log('listening on 0.0.0.0:8080');
});

-p / ports の指定ミス(ポート番号の取り違え)

# NG例: アプリはコンテナ内で8080待ち受けなのに、80をマップしている
docker run -p 80:80 myapp

# OK例: コンテナ内ポートに合わせてマッピング
docker run -p 8080:8080 myapp

# docker-compose.yml の例
services:
  web:
    image: myapp
    ports:
      - "8080:8080"  # host:container

ブラウザやcurlでアクセスしているポートと、ports の host側ポートが一致しているかを必ず確認する。

コンテナ→コンテナ:ホスト名・ネットワーク・ポートを揃える

docker compose での基本は「サービス名がDNS名になる」というルール。

# docker-compose.yml の例
services:
  app:
    image: myorg/app
    depends_on:
      - db
  db:
    image: postgres:16
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=secret

この場合、appコンテナからは「db:5432」で接続できる。

# appコンテナ内からDBに繋ぐ例
psql -h db -U app -d appdb

# よくあるNG例: localhost:5432 に繋ごうとしてしまう(自コンテナの中を見にいく)
DATABASE_URL=postgres://app:secret@localhost:5432/appdb  # ← コンテナ間ではNG

コンテナ間通信は、localhostではなくサービス名(またはコンテナ名)を使う。

コンテナ→外部サービス:接続先IP/ポートとFirewallを確認

# コンテナ内から接続テスト
docker exec -it <container> sh -lc "
  nc -vz db.example.com 5432 || echo 'ng'
"

# DNS名が間違っていないかも確認
docker exec -it <container> sh -lc "ping -c1 db.example.com || nslookup db.example.com"

社内ネットワークやクラウド環境では、FW・セキュリティグループ・セグメントの制約で「そもそもポートが空いていない」ことも多い。この場合はdocker側ではなくインフラの設定を見直す。

アプリがまだ起動していない/クラッシュしている(起動順の問題)

コンテナ自体は起動しているが、アプリケーションがクラッシュしていたり初期化中で、ポートが開いていない時間帯にアクセスしている場合も「Connection refused」となる。

# ログでアプリの起動状況を見る
docker logs <container> --tail=200

# 再起動を繰り返している場合(Exit Codeが非0)
docker ps -a --filter "name=<service>" --format 'table {{.Names}}\t{{.Status}}\t{{.ExitCode}}'

docker compose ではhealthcheckを定義し、依存サービスが「健康」になるまで待つことで起動順の問題を緩和できる。

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL","pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10
  app:
    image: myorg/app
    depends_on:
      db:
        condition: service_health

localhostの勘違い(ホストとコンテナの名前空間の違い)

「localhost」は「そのプロセスが動いているOSの中」の127.0.0.1を指す。

・ブラウザから http://localhost:8080 にアクセス → ホストの127.0.0.1:8080に接続。

・コンテナ内から localhost:8080 にアクセス → コンテナ内の127.0.0.1:8080に接続。

よくあるミス:appコンテナからdbコンテナへ接続するのに「localhost:5432」を使ってしまう。

正しくは、docker composeで定義したサービス名(dbなど)を使う。

Docker Desktop / WSL2 / macOS / Windows の特殊ケース

Docker Desktop環境では、ホストOS・VM・コンテナの間にレイヤがある。

・Windowsで「localhost」にブラウザからアクセス → 基本的にはVMのポートフォワードを経由。

・WSL2内でcurl http://localhost:8080 → WSL2のLinux側のlocalhostに接続。

テストするときは、必ず「どの層でコマンドを打っているか」を意識する。

# WSL2内からホストWindows側にアクセスする場合のIP例
ipconfig.exe | grep "vEthernet (WSL)" -n -A5

環境によっては、127.0.0.1ではなく特定のブリッジIPに向けてアクセスする必要がある。

デバッグ用ワンライナー(curl/ncで接続可否を可視化)

# ホスト側から
curl -v http://localhost:8080/health || echo "ng"
nc -vz localhost 8080

# コンテナ側から
docker exec -it <container> sh -lc "
  echo '--- from container ---'
  nc -vz web 8080 || nc -vz localhost 8080
"

「どこから打つと通って、どこから打つとrefusedになるか」をテーブルにすると原因特定が早くなる。

NG→OK早見(よくあるパターン別の修正例)

# 1) アプリが127.0.0.1でLISTENしている
# NG:
app.listen(8080, '127.0.0.1');
# OK:
app.listen(8080, '0.0.0.0');

# 2) ポートマッピングミス
# NG: コンテナ内ポートと違う
docker run -p 8080:80 myapp
# OK: 正しいポートをマップ
docker run -p 8080:8080 myapp

# 3) コンテナ間接続でlocalhostを使う
# NG:
DATABASE_HOST=localhost
# OK:
DATABASE_HOST=db        # サービス名

# 4) DB起動前にappが繋ぎに行く
# NG: depends_onだけ
depends_on:
  - db
# OK: healthcheck + condition
depends_on:
  db:
    condition: service_healthy

チェックリスト(上から順に潰す)

1) どこからどこへ繋ぎにいっているか(ホスト→コンテナ/コンテナ→コンテナ/コンテナ→外部)を明確にしたか
2) 接続先IP/ホスト名とポートが正しいか、portsのhost:container対応と合っているか
3) コンテナ内のアプリが0.0.0.0:ポートでLISTENしているか(127.0.0.1ではないか)
4) コンテナ間接続でlocalhostを使っていないか(サービス名を使っているか)
5) アプリは起動に成功し、そのポートで待ち受けているか(ログと状態を確認したか)
6) 外部サービスに対して、Firewall/セキュリティグループ/プロキシが邪魔していないか
7) Docker Desktop/WSL2の場合、階層(ホスト/VM/WSL)を理解して疎通確認したか
8) curl/ncで、どのレイヤからも接続テストを行い、通る/通らない組み合わせを確認したか