localhost に繋がらないときの切り分け:curl・ss・ps で原因を特定する

よるほろぐ編集部

· 約 15 分

ブラウザに「接続できません」と表示されたとき、すぐにアプリを再起動しても原因は分かりません。プロセスが停止している、別のポートで待ち受けている、URLのパスが違う、すでに別プロセスがポートを使っているなど、見た目が似た障害でも直す場所は異なります。

この記事では、Pythonの簡易HTTPサーバーをループバックアドレスだけで動かし、次の3つの障害を意図的に再現します。

  • 待ち受けプロセスがなく、curlが接続を確立できない
  • サーバーには届くが、存在しないパスへアクセスしてHTTP 404になる
  • 使用中のポートで2つ目のサーバーを起動し、Address already in useになる

ゴールはエラーメッセージを暗記することではありません。curlssps、サーバーログを順番に見て、どの層までは正常か を判断できるようになることです。実験は127.0.0.1だけを使い、終了時にプロセスと一時ファイルを片付けます。

検証した範囲

2026年7月10日にLinux環境で、正常時のHTTP 200、停止時のcurl終了コード7、存在しないパスのHTTP 404と終了コード22、二重起動時のErrno 98: Address already in use、後片付け後の接続拒否を再現しました。検証環境にはsspsがなかったため、この2コマンドの使い方は公式マニュアルと照合し、未確認の出力例は掲載していません。

最初に観測結果の意味を決める

同じ「繋がらない」でも、観測結果から次に見る場所を変えます。

観測結果分かったこと次に確認する場所
curlの終了コードが7TCP接続を確立できていない待ち受けソケット、プロセス、ポート番号
HTTP 404curl --failの終了コードが22サーバーまでは到達しているURLのパス、公開ディレクトリ、アプリのルーティング
起動時にAddress already in use同じアドレスとポートを別ソケットが使用中ssで待ち受け元を特定してから判断
HTTP 200で期待した本文が返る接続経路と対象リソースは正常上位のアプリ処理やブラウザ側を確認

次の図は、調査の分岐を表しています。まずHTTP応答の有無を確認し、応答がなければソケットとプロセスへ、応答があればステータスコードとアプリ側へ進みます。

flowchart TD
  A["curlで対象URLを確認"] --> B{"HTTP応答があるか"}
  B -->|ない| C["ssでLISTENを確認"]
  C -->|ない| D["プロセス停止またはポート違い"]
  C -->|ある| E["接続先アドレスやプロキシを確認"]
  B -->|ある| F{"期待したステータスか"}
  F -->|404| G["URLと公開対象を確認"]
  F -->|2xx| H["接続経路は正常"]

この順番なら、HTTP 404に対してファイアウォールを変更したり、ポート競合に対してURLを書き換え続けたりする遠回りを避けられます。

隔離した実験環境を準備する

この手順はBash、Python 3、curl、sspsを前提にします。最初に利用できるか確認します。

command -v bash python3 curl ss ps

ssは一般にiproute2psprocpsの一部として配布されています。見つからない場合は、利用しているディストリビューションのパッケージ管理手順で導入してから進めます。

一時ディレクトリと実験用ポートを決めます。既存の作業ディレクトリをHTTP公開しないよう、--directoryで公開範囲を限定します。

LAB_DIR=$(mktemp -d)
PORT=18080
URL="http://127.0.0.1:${PORT}"
printf 'lab-ok\n' > "${LAB_DIR}/index.html"

Python公式ドキュメントは、http.serverを本番用途に推奨していません。ここでは障害を観察する短時間のローカル実験だけに使います。また、Pythonのデフォルトは全インターフェースへのbindなので、--bind 127.0.0.1を必ず指定します。

障害1:待ち受けがなく接続を確立できない

まだサーバーを起動せず、curlを実行します。環境変数でHTTPプロキシが設定されていてもローカル実験へ影響しないよう、--noproxy '*'を付けます。

curl --noproxy '*' \
  --connect-timeout 2 \
  --silent --show-error \
  "${URL}/"
echo "exit=$?"

検証環境では次の結果になりました。

curl: (7) Failed to connect to 127.0.0.1 port 18080: Could not connect to server
exit=7

curl公式マニュアルでは、終了コード7は「ホストへの接続に失敗した」ことを表します。この時点ではHTTPリクエストを処理するサーバーまで到達していません。HTMLやアプリのルーティングを調べる前に、待ち受けの有無を確認します。

ss -ltnp "sport = :${PORT}"
  • -lは待ち受けソケットだけを表示します。
  • -tはTCP、-nは名前解決せず数値表示、-pは利用プロセスを表示します。
  • 権限や環境によっては、-pを付けても他ユーザーのプロセス情報が見えないことがあります。

何も表示されなければ、そのアドレスとポートで待ち受けているTCPソケットはありません。サービス停止、起動失敗、ポート番号違いを疑います。

正常な基準を作る

障害だけを見ても、正常との差が分かりません。次にサーバーを起動し、PIDを保存します。

python3 -m http.server "${PORT}" \
  --bind 127.0.0.1 \
  --directory "${LAB_DIR}" \
  > "${LAB_DIR}/server.log" 2>&1 &
SERVER_PID=$!

終了時に取り残さないよう、すぐに後片付け関数を登録します。

cleanup() {
  if kill -0 "${SERVER_PID}" 2>/dev/null; then
    kill "${SERVER_PID}"
    wait "${SERVER_PID}" 2>/dev/null || true
  fi
  rm -rf -- "${LAB_DIR}"
}
trap cleanup EXIT

$!は直前にバックグラウンド起動したプロセスのPIDです。psで、保存したPIDが意図したコマンドか確認します。

ps -fp "${SERVER_PID}"

続いてソケットを確認します。

ss -ltnp "sport = :${PORT}"

127.0.0.1:18080LISTENなら、このマシン自身からIPv4で接続を受けられる状態です。0.0.0.0:18080は全IPv4インターフェースでの待ち受けを意味するため、ローカル実験では設定しません。

HTTPステータスと本文を別々に確認します。

curl --noproxy '*' \
  --silent --show-error \
  --output /dev/null \
  --write-out 'HTTP %{response_code}\n' \
  "${URL}/"

curl --noproxy '*' --silent --show-error "${URL}/"

確認できた結果は次のとおりです。

HTTP 200
lab-ok

ここまで通れば、プロセス、待ち受けソケット、TCP接続、HTTP処理、対象ファイルの読み出しまでが正常です。

障害2:HTTP 404は接続失敗ではない

存在しないパスを要求します。--failを付けると、curlはHTTP 400以上を終了コード22として扱います。

curl --noproxy '*' \
  --fail --silent --show-error \
  --output /dev/null \
  --write-out 'HTTP %{response_code}\n' \
  "${URL}/missing.txt"
echo "exit=$?"

検証結果です。

curl: (22) The requested URL returned error: 404
HTTP 404
exit=22

HTTP 404が返ったということは、TCP接続とHTTPの応答は成立しています。RFC 9110では、404はオリジンサーバーが対象リソースの現在の表現を見つけられない、または存在を開示しないことを示します。PythonのSimpleHTTPRequestHandlerでは、要求を対応するファイルとして開けない場合などに404へ変換されます。

この状態で直す候補は、ネットワークではなく次の項目です。

  • URLのパスや大文字・小文字が正しいか
  • --directoryで指定した公開元にファイルがあるか
  • Webフレームワークのルートが定義されているか
  • リバースプロキシが別のパスへ書き換えていないか

サーバーログには、正常なGET /と失敗したGET /missing.txtが記録されます。

tail -n 20 "${LAB_DIR}/server.log"

HTTP応答があるのにサービス再起動やファイアウォール変更を繰り返すのは、観測結果と修正対象が一致していません。

障害3:ポート競合では先に使用者を特定する

サーバーを動かしたまま、同じアドレスとポートで2つ目を起動します。

python3 -m http.server "${PORT}" \
  --bind 127.0.0.1 \
  --directory "${LAB_DIR}"
echo "exit=$?"

検証環境では、プロセスは終了コード1で停止し、末尾に次の原因が表示されました。

OSError: [Errno 98] Address already in use
exit=1

ここでポートを使っていそうなプロセスを名前で一括終了してはいけません。先にソケットからPIDを確認します。

ss -ltnp "sport = :${PORT}"
ps -fp "${SERVER_PID}"

表示されたPIDとコマンドが自分の起動したサーバーなら、今回は競合ではなく二重起動を試した結果だと判断できます。実運用では、既存サービスを止めるべきか、新しいサービスのポートを変えるべきかを、サービスの所有者と設定ファイルを確認して決めます。

PIDを確認せずkillしない

pkill python3や、検索結果をそのままkill -9へ渡す操作は、無関係なプロセスまで終了させる可能性があります。まずsspsで対象を特定し、通常終了のSIGTERMで止められるか確認します。

localhost特有の見落としを確認する

localhostは環境によってIPv4の127.0.0.1とIPv6の::1の両方へ解決されます。サービスが片方だけで待ち受けている場合、アドレスファミリーの違いが結果に影響します。

curlの詳細表示では、実際に試した接続先を確認できます。

curl --noproxy '*' --verbose "http://localhost:${PORT}/"

Trying [::1]:18080Trying 127.0.0.1:18080のどちらへ接続しているかを見ます。比較するときは、URLを127.0.0.1へ固定するか、curlの-4または-6を使い、条件を一つずつ変えます。

また、HTTP_PROXYHTTPS_PROXYが設定された開発環境では、localhostへの通信までプロキシへ送られる場合があります。今回の実験で--noproxy '*'を付けたのは、接続先以外の変数を排除するためです。実運用では、必要なホストだけをNO_PROXYへ設定します。

後片付けまでを成功条件にする

実験を終えたら、登録した関数を明示的に実行し、EXIT時の二重実行を解除します。

cleanup
trap - EXIT

待ち受けと接続を再確認します。

ss -ltnp "sport = :${PORT}"

curl --noproxy '*' \
  --connect-timeout 2 \
  --silent --show-error \
  "${URL}/"
echo "exit=$?"

ssに対象ポートが表示されず、curlが再び終了コード7になれば、プロセスとソケットの後片付けまで完了しています。検証環境でも、終了後に同じ接続拒否へ戻ることを確認しました。

実運用へ持ち込むときの順番

Pythonの実験サーバーをNginx、Node.js、コンテナ、systemdサービスへ置き換えても、観測の順番は変わりません。

  • curlでTCP接続前の失敗か、HTTP応答後の失敗かを分ける
  • ssで想定したアドレスとポートがLISTENか確認する
  • psでPIDと実行コマンドを確認する
  • HTTPステータスとレスポンス本文を確認する
  • 対象プロセスのログを確認する
  • 設定変更後、同じ観測で復旧を確かめる

広いネットワークのDNS・ルーティング・ICMPも含めて調べる場合は、ネットワークの基本:ss・curl・pingで調べるへ進んでください。プロセスの状態や安全な終了方法はプロセスとジョブ管理、常駐サービスのログと再起動制御はsystemdで最小権限のサービスを作るで詳しく扱っています。

この切り分けで重要なのは、エラーを消すための変更を急がないことです。接続前、ソケット、プロセス、HTTP、アプリの順に観測し、正常だと分かった層を戻らないように進めると、変更箇所を最小限にできます。

出典

共有: X でシェア はてブ

編集・検証

よるほろぐ編集部

Linux・コンテナ・CLI・開発環境の実践ガイドを編集・検証します。

編集方針を読む