はじめに:深夜の「このポート何だっけ?」問題
サーバー運用をしていると、深夜にアラートを見て SSH した先で「このポート何が開いてるんだっけ?」という状況に何度も遭遇します。
これまでは netstat / ss / lsof / nmap / systemctl / docker ps を組み合わせて、人力でそのプロセスの正体と起源を推理していました。
最近、witr (Why is this running) という CLI ツールを導入したところ、「プロセスがなぜ存在しているのか」まで 1 コマンドで説明してくれるようになり、調査フローがかなり変わりました。
https://github.com/pranshuparmar/witr
従来フロー:Debian・Docker・macOS での調査
Debian(systemd サーバー)
例えば、本番の Debian サーバーで 8080 番ポートが開いているのを見つけたときは、だいたいこんな流れでした。
# ポートと PID の確認
ss -ltnp | grep 8080
# PID からプロセス名を確認
ps aux | grep 12345
# systemd 管理かどうかを確認
systemctl status my-app.serviceこのあと、「どのユニットから起動されているか」「実行ユーザー」「起動オプション」などを、systemctl や journalctl、サービスの設定ファイルを見ながら手で繋ぎ合わせていました。
Docker 多めの環境
Docker が絡むと、さらに 1 ステップ増えます。
# ホスト側のポートだけ分かっている状態
ss -ltnp | grep 5000
# プロセスが docker-proxy / containerd っぽい → コンテナを調べる
docker ps
docker ps --format 'table {{.Names}}\t{{.Ports}}'
# コンテナ内のポート状況を見る
docker exec -it my-app ss -ltnp「ホストの 0.0.0.0:5000 → docker-proxy → コンテナ my-app → コンテナ内のアプリ」といった因果関係を、自分で追いかける必要がありました。
macOS(開発マシン)
開発用 macOS でも、似たようなことをやっていました。
# LISTEN しているポート一覧
lsof -iTCP -sTCP:LISTEN
# 気になるポートだけ掘る
lsof -i:3000しばらく触っていないプロジェクトの dev サーバーが残っていると、「これどのディレクトリの Node だったっけ?」と、さらに ps / cwd / シェルの履歴まで遡ることもありました。
witr の導入とインストール
https://github.com/pranshuparmar/witr
Debian(Linux)
curl -fsSL https://raw.githubusercontent.com/pranshuparmar/witr/main/install.sh -o install.sh
chmod +x install.sh
sudo ./install.shスクリプトが OS(linux / darwin / freebsd)とアーキテクチャ(amd64 / arm64)を自動判定し、最新バイナリと man ページを /usr/local/bin と /usr/local/share/man に配置してくれます。
手動で入れたい場合は、こんな感じでも入れられます。
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
[ "$ARCH" = "x86_64" ] && ARCH="amd64"
[ "$ARCH" = "aarch64" ] && ARCH="arm64"
curl -fsSL "https://github.com/pranshuparmar/witr/releases/latest/download/witr-${OS}-${ARCH}" -o witr
chmod +x witr
sudo mv witr /usr/local/bin/witrmacOS(Homebrew)
brew install witrHomebrew 環境なら一行で導入できるので、開発用 mac とサーバーの両方に同じコマンドが入るのも便利でした。
実例1:Debian 本番での「この 5000 番何?」事件
ある晩、ALB のヘルスチェックが一部失敗しているという通知を受けて、本番 Debian に入って確認したときの話です。
1. いつものように ss から入る
ss -ltnp | grep 5000出力(イメージ)はこんな感じでした。
LISTEN 0 4096 0.0.0.0:5000 0.0.0.0:* users:(("app-worker",pid=12345,fd=7))「app-worker ってプロセスが 5000 番で LISTEN しているのは分かったけど、systemd のどのユニットだったっけ…?」といういつもの状態になります。
2. witr を使って「なぜ動いているか」を聞いてみる
ここで、最近入れた witr を試してみました。
witr --port 5000マスクしたイメージですが、出力はこんな雰囲気です。
Port 5000 is bound by PID 12345 (app-worker) running as appuser
└─ Started by systemd unit app-worker.service
└─ WantedBy multi-user.target
└─ Enabled via /etc/systemd/system/app-worker.service
Summary:
Port 5000 → app-worker.service (systemd) → multi-user.target
This process exists because the app-worker service is enabled and started at boot.この 1 回の実行で、
- どの PID / ユーザーがポートを掴んでいるか
- それがどの systemd ユニットか
- そのユニットがどの target からぶら下がっているか
- 起動理由(ブート時に enable 済みなのか、手動 start なのか)
といった「因果関係」まで、一気に読める形で返してくれました。
3. 要約だけ欲しいとき
障害対応中に長い説明まではいらない場合は、ショートモードがちょうど良かったです。
witr --port 5000 --short出力イメージ:
Port 5000 → app-worker (PID 12345) started by systemd unit app-worker.service (enabled at boot)「これは systemd 管理の常駐ワーカーだから、ALB の設定を見直すべき」とすぐ判断できたのがかなり助かりました。
実例2:Docker だらけの検証環境でのポート迷子
次は、Docker コンテナが大量に動いている検証サーバーの例です。
1. ホストで怪しいポートを見つける
あるとき、ログに「5001 番ポートに対する unknown client からのアクセス」が出ていて、サーバー側を確認しました。
ss -ltnp | grep 5001出力イメージ:
LISTEN 0 4096 0.0.0.0:5001 0.0.0.0:* users:(("docker-proxy",pid=23456,fd=4))「あ〜 docker-proxy だな…このポート、どのコンテナだっけ?」となります。
2. witr で一気にコンテナの正体まで辿る
witr --port 5001出力イメージ:
Port 5001 is bound by PID 23456 (docker-proxy) on the host
└─ This proxies to container port 8000 in container web-frontend-1
└─ Container web-frontend-1 (image registry.example.com/web-frontend:2025-12-01)
└─ Started by docker-compose.service (systemd) from /srv/projects/frontend/docker-compose.yml
Summary:
Port 5001 → docker-proxy → container web-frontend-1 → docker-compose.service
This exists because docker-compose.service is running and published port 5001:8000.普段なら、
- docker-proxy の PID から docker が怪しいと推測
docker psを眺めて、ポート欄から対応するコンテナを探すdocker inspectで起動コマンドや compose ファイルを確認- 必要なら systemd の docker-compose ユニットまで辿る
という 3〜4 ステップを踏んでいたところを、1 コマンドで「ポート → docker-proxy → コンテナ → compose → systemd ユニット」という因果チェーンまで説明してくれたのが印象的でした。
コンテナ内をさらに深掘りしたい場合は、コンテナの中にも witr を入れておいて、
docker exec -it web-frontend-1 bash
witr --port 8000のように、コンテナ内プロセスの起源まで見に行くこともできます。
実例3:macOS ローカルでの「3000 番誰問題」
最後に、開発用 macOS の話です。
1. しばらく触っていない dev サーバーが残っていた
ある日、別プロジェクトで 3000 番を使おうとしたところ、「すでに使用中」と言われました。
lsof -i:3000出力イメージ:
node 98765 myuser 21u IPv4 0x1234567890abcdef 0t0 TCP *:hbci (LISTEN)Node で何かが動いていることは分かりますが、「これどのプロジェクトの dev サーバーだっけ?」となります。
2. witr に聞いてみる
witr --port 3000出力イメージ(macOS 対応版):
Port 3000 is bound by PID 98765 (node) running as myuser
└─ Started from interactive shell session:
└─ /bin/zsh -l
└─ Working directory: /Users/myuser/dev/old-project
└─ Command: node_modules/.bin/next dev
Summary:
Port 3000 → node (Next.js dev server) started manually in /Users/myuser/dev/old-project「昔の Next.js プロジェクトの dev サーバーが /old-project 配下で残っている」ことが一目瞭然になり、そのシェルを閉じるか ctrl+c するだけで片付きました。cwd や起動コマンドまで含めて「なぜ動いているか」を説明してくれるので、macOS でもかなり便利に使えています。
まとめ:状態を見るツールから、理由を説明するツールへ
- ss / netstat / lsof / nmap / systemctl / docker ps などの従来ツールは、「何がどこで動いているか」という状態を知るのに非常に優秀です。
- 一方で、「そのプロセスがなぜ存在しているのか」「どの仕組みが責任を持っているのか」は、複数ツールの出力を自分で突き合わせて推理する必要がありました。
- witr は、ポート番号・PID・プロセス名など、どこから入っても最終的に “Why is this running?” に答える形で因果チェーンをテキスト化してくれるので、Debian の本番サーバーでも Docker だらけの検証環境でも macOS の開発マシンでも、「とりあえず witr を 1 回叩いてから考える」というのが自分の新しい標準フローになりつつあります。