Why Is This Port Open? Answering It in One Command with witr

Replace manual netstat workflows with witr’s single-command answers

* This page contains promotional content

Introduction: the late-night “what was this port again?” problem

When you run servers, you repeatedly end up in the situation where you see an alert in the middle of the night, SSH into the machine, and think “what is listening on this port again?”
Until now I combined netstat / ss / lsof / nmap / systemctl / docker ps and worked out the identity and the origin of the process by hand.
Recently I started using a CLI tool called witr (Why is this running), and because it explains in a single command even why a process exists, my investigation flow has changed considerably.

https://github.com/pranshuparmar/witr


The old flow: investigating on Debian, Docker and macOS

Debian (a systemd server)

For example, when I found that port 8080 was open on a production Debian server, it usually went something like this.

# check the port and the PID
ss -ltnp | grep 8080

# check the process name from the PID
ps aux | grep 12345

# check whether it is managed by systemd
systemctl status my-app.service

After that, I stitched together by hand — while looking at systemctl, journalctl and the service’s configuration files — which unit it was started from, which user it ran as, what startup options it had, and so on.

An environment with a lot of Docker

Once Docker is involved, there is one more step.

# only the host-side port is known
ss -ltnp | grep 5000

# the process looks like docker-proxy / containerd → investigate the container
docker ps
docker ps --format 'table {{.Names}}\t{{.Ports}}'

# look at the port situation inside the container
docker exec -it my-app ss -ltnp

I had to follow the chain of cause and effect myself: “host 0.0.0.0:5000 → docker-proxy → container my-app → the application inside the container”.

macOS (my development machine)

I was doing something similar on my macOS development machine.

# list of the ports that are LISTENing
lsof -iTCP -sTCP:LISTEN

# dig into just the port I care about
lsof -i:3000

When a dev server from a project I had not touched in a while was still running, I sometimes had to go back even further through ps / cwd / shell history, wondering “which directory’s Node was this again?”


Getting started with witr and installing it

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

The script automatically detects the OS (linux / darwin / freebsd) and the architecture (amd64 / arm64) and places the latest binary and the man page into /usr/local/bin and /usr/local/share/man.

If you want to install it manually, you can do it like this.

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/witr

macOS (Homebrew)

brew install witr

In a Homebrew environment it installs in one line, so it was also convenient that the same command ends up on both my development Mac and my servers.


Case 1: the “what is this port 5000?” incident on production Debian

One evening I received a notification that some of the ALB health checks were failing, and this is what happened when I logged into the production Debian box to check.

1. Starting from ss as usual

ss -ltnp | grep 5000

The output (illustrative) looked like this.

LISTEN 0      4096          0.0.0.0:5000       0.0.0.0:*    users:(("app-worker",pid=12345,fd=7))

This puts me in the usual state: “I can see that a process called app-worker is listening on port 5000, but which systemd unit was it again…?”

2. Asking witr why it is running

Here I tried out witr, which I had installed recently.

witr --port 5000

The values are masked, but the output has this kind of feel.

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.

With this single run it returned, all at once and in a readable form, the whole “chain of causation”:

  • which PID / user is holding the port
  • which systemd unit that is
  • which target that unit hangs off
  • the reason it started (whether it is enabled at boot or was started manually)

3. When I only want the summary

When I do not need the long explanation in the middle of handling an incident, the short mode was just right.

witr --port 5000 --short

Illustrative output:

Port 5000 → app-worker (PID 12345) started by systemd unit app-worker.service (enabled at boot)

It helped a lot that I could immediately conclude “this is a resident worker managed by systemd, so I should review the ALB configuration”.


Case 2: lost among the ports on a Docker-heavy staging environment

Next is an example from a staging server where a large number of Docker containers are running.

1. Finding a suspicious port on the host

At one point the logs showed “access to port 5001 from an unknown client”, so I checked the server side.

ss -ltnp | grep 5001

Illustrative output:

LISTEN 0      4096      0.0.0.0:5001       0.0.0.0:*    users:(("docker-proxy",pid=23456,fd=4))

And then it is “ah, docker-proxy… which container is this port again?”

2. Tracing all the way to the container in one shot with witr

witr --port 5001

Illustrative output:

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.

Normally I would

  • guess from the docker-proxy PID that docker is the suspect
  • stare at docker ps and look for the matching container in the ports column
  • check the startup command and the compose file with docker inspect
  • and, if necessary, trace it back to the systemd docker-compose unit

which was 3 to 4 steps, and it was striking that a single command explained the causal chain “port → docker-proxy → container → compose → systemd unit”.

If you want to dig further inside the container, you can also install witr inside the container and go look at the origin of the processes there,

docker exec -it web-frontend-1 bash
witr --port 8000

like this.


Case 3: the “who owns port 3000?” problem on local macOS

Finally, the story on my macOS development machine.

1. A dev server I had not touched in a while was still running

One day I tried to use port 3000 for another project and was told it was “already in use”.

lsof -i:3000

Illustrative output:

node    98765  myuser   21u  IPv4 0x1234567890abcdef      0t0  TCP *:hbci (LISTEN)

I can tell that something is running under Node, but then it is “which project’s dev server was this again?”

2. Asking witr

witr --port 3000

Illustrative output (the macOS-capable version):

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

It became obvious at a glance that “the dev server of an old Next.js project is still running under /old-project”, and it was cleaned up just by closing that shell or hitting ctrl+c.
Because it explains why something is running, including the cwd and the startup command, I find it quite useful on macOS as well.


Summary: from tools that show state to a tool that explains the reason

  • Traditional tools such as ss / netstat / lsof / nmap / systemctl / docker ps are extremely good at telling you the state, that is, what is running where.
  • On the other hand, “why does that process exist” and “which mechanism is responsible for it” had to be reasoned out by cross-checking the output of several tools myself.
  • witr turns the causal chain into text in a form that ultimately answers “Why is this running?”, no matter whether you enter from a port number, a PID or a process name, so on production Debian servers, on Docker-heavy staging environments and on my macOS development machine alike, “just run witr once and think afterwards” is becoming my new standard flow.

See also