One table for listening ports, the apps that own them, the Docker
containers behind them, and what the port is normally for. No more
piping lsof into grep and cross-referencing
docker ps by hand.
macOS & Linux · zero dependencies · MIT licensed · v0.3.0
every line above is output portmap actually prints
check, built for shell scriptsThe problem
lsof -i :3000 tells you a PID. docker ps tells you a
container. /etc/services tells you what the port is for.
None of them tell you all three, and the port you actually care about
is usually the one that belongs to something you forgot you started.
Port, protocol, bind address, app, PID, container and service name — assembled from every source your machine already has, in a single aligned table.
Published container ports are matched to real listeners by port, protocol and bind address. A published port with no listener still shows up, so you can see what Docker claims versus what is actually bound.
portmap check 5432 exits 0 when the port is free and 1 when it isn't. Wrap it in an if and your dev script stops racing itself.
Try it
This runs against a mock machine, right here in the page — a laptop with Postgres, Redis in Docker, a Node dev server and a couple of other things running. Type a port and see exactly what the real command would print.
One port in, one line out, and an exit code your shell can branch on. The real thing scans your own machine — this one scans the fake one below.
portmap
Run it with no arguments and you get every listening socket on the machine,
sorted, de-duplicated, and annotated. Rows that differ only by bind address
are merged into one, so a service listening on both
127.0.0.1 and ::1 is one row, not two.
A - means genuinely nothing known — not a blank the tool
forgot to fill in.
PORT PROTO ADDRESS APP PID CONTAINER SERVICE 3000 tcp * node 4821 - dev-http 5432 tcp 127.0.0.1 postgres 1234 pg postgres 6379 tcp * docker - redis redis 7000 udp * syncthing 9931 - - 8080 tcp 127.0.0.1 java 5510 - http-alt 27017 tcp 127.0.0.1 mongod 768 - mongodb
--watch
portmap --watch clears the screen and redraws on an interval —
--interval 1s if two seconds is too slow. Start your stack in
one pane and watch the ports appear in the other.
Combine it with --port to keep one port under a microscope
while a container restarts, and with --protocol when UDP noise
is in your way.
check & --json
check prints one human line and returns a real exit code:
0 free, 1 occupied, 2 bad input. That is enough to gate a
dev server, a test harness, or a CI step without parsing any text at all.
When you do want the data, --json gives you every field —
including the container ID and the bind address list — indented and ready
for jq.
#!/usr/bin/env bash if portmap check 3000 >/dev/null; then npm run dev else echo "port 3000 is taken:" portmap --port 3000 exit 1 fi $ portmap --port 6379 --json | jq -r '.[].container' redis
Reference
Pick one on the left. The panel shows what you type and what portmap prints back.
Flags
| Flag | What it does |
|---|---|
| --port <port> | Show only one listening port.portmap --port 5432 |
| --protocol <value> | Filter rows by protocol: all, tcp or udp. Default all. |
| --json | Print JSON instead of a table — indented, with the full address list, container ID and user. |
| --no-docker | Skip the docker ps call entirely. Faster, and quiet on machines without Docker. |
| --watch | Clear and redraw the table until interrupted. Not compatible with --json. |
| --interval <duration> | Refresh interval for --watch. Default 2s. Accepts 500ms, 1s, 1m. |
| --timeout <duration> | Cap the whole scan. Default 5s. A wedged lsof gets cancelled rather than hanging your terminal. |
| --version | Print the build version and exit. |
Exit codes
check.| Code | Meaning | Typical use |
|---|---|---|
| 0 | The port is free. | Start the service. |
| 1 | The port is occupied. | Print the owner and bail out, or pick another port. |
| 2 | Bad input, or the scan itself failed. | A real error — do not treat it as "free". |
Under the hood
Your machine already ships tools that know exactly what is listening, and they are maintained by people who care about it full time. portmap asks them, then does the boring join nobody wants to do by hand.
macOS: lsof, once for TCP listeners and once for UDP. Linux: ss first, falling back to netstat, then lsof — whichever is actually installed.
docker ps supplies published ports, container names and IDs. Each one is matched to a listener by port, protocol and a compatible bind address — wildcards included.
A built-in table of common services is overlaid with your real /etc/services, so 5432 reads postgres and your site-local entries still win.
Rows identical but for their bind address are collapsed into one with a merged address list, filters are applied, and the result is sorted before it reaches your terminal.
All four external calls run behind a single Runner interface,
which is why the parsers for lsof, ss,
netstat and docker are covered by tests against
canned fixtures rather than whatever happens to be running on the machine
at the time. Every call also inherits the --timeout context, so
nothing can block forever.
Getting started
The repository is named Homebrew-portman, so Homebrew's shorthand for it is rohitshidid/portman.
brew tap rohitshidid/portman
The formula builds the v0.3.0 tag with Go, which Homebrew pulls in as a build-only dependency and does not leave behind.
brew install rohitshidid/portman/portmap
portmap --version
No config file, no daemon, no first-run setup. Every command is a one-shot scan of the machine you are on.
portmap
portmap check 3000
portmap --watch
Or build it from source — Go 1.22, no modules to download:
git clone https://github.com/rohitshidid/Homebrew-portman.git
cd Homebrew-portman
go test ./...
go build -o portmap ./cmd/portmap
./portmap --help
Without sudo, the OS will not tell you which process owns a
socket belonging to another user. The port still appears —
the APP and PID columns just read -.
If a row looks suspiciously empty, re-run with sudo portmap and
it will fill in.
Honestly
It describes sockets on the machine it is running on. It does not probe anything over the network, and pointing it at someone else's host is not a thing it can do.
It reports. It does not open ports, close them, or stop the process holding one — that is still kill or docker stop, on purpose.
There is no background service and no history. --watch is a loop around the same one-shot scan you get from running the command yourself.
If lsof, ss and netstat are all missing, there is nothing to enumerate. If Docker is not running, the container column is simply empty — the scan still works.
lsof.Free, open source, and a single static binary.
macOS & Linux · MIT licensed · v0.3.0