portmap icon

What's on that port?

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

~ — zsh

every line above is output portmap actually prints

0third-party dependencies — the standard library and nothing else
4sources joined: lsof, ss, netstat and docker
5sdefault scan timeout, so a hung tool can never hang you
0/1/2exit codes from check, built for shell scripts

The problem

Four tools, three answers, one question.

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.

📋

One table, joined for you

Port, protocol, bind address, app, PID, container and service name — assembled from every source your machine already has, in a single aligned table.

🐳

Docker included, not guessed

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.

⚙️

Made to be scripted

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

Check a port without installing anything.

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.

portmap check <port>

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

The default output is the whole product.

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.

portmap
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

Watch the table instead of re-running it.

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.

portmap --watch --interval 1s

      

check & --json

Built to be called by other things.

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.

a dev script
#!/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

Every invocation, with its output.

Pick one on the left. The panel shows what you type and what portmap prints back.


        

Flags

Eight flags. You will use two.

FlagWhat it does
--port <port>Show only one listening port.portmap --port 5432
--protocol <value>Filter rows by protocol: all, tcp or udp. Default all.
--jsonPrint JSON instead of a table — indented, with the full address list, container ID and user.
--no-dockerSkip the docker ps call entirely. Faster, and quiet on machines without Docker.
--watchClear 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.
--versionPrint the build version and exit.

Exit codes

Three codes, from check.

CodeMeaningTypical use
0The port is free.Start the service.
1The port is occupied.Print the owner and bail out, or pick another port.
2Bad input, or the scan itself failed.A real error — do not treat it as "free".

Under the hood

It doesn't reinvent the socket table.

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.

Step 01

Enumerate

macOS: lsof, once for TCP listeners and once for UDP. Linux: ss first, falling back to netstat, then lsof — whichever is actually installed.

Step 02

Annotate

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.

Step 03

Label

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.

Step 04

Merge & sort

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.

Every command goes through one interface

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

Two commands, and there is nothing to configure.

Tap the repository

The repository is named Homebrew-portman, so Homebrew's shorthand for it is rohitshidid/portman.

brew tap rohitshidid/portman

Install

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

Run it

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

A note on what you can see

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

Four things it is not.

✕
It is not a port scanner.

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 is not a firewall or a manager.

It reports. It does not open ports, close them, or stop the process holding one — that is still kill or docker stop, on purpose.

✕
It is not a daemon.

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.

✕
It is only as good as its sources.

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.

Stop grepping lsof.

Free, open source, and a single static binary.

macOS & Linux · MIT licensed · v0.3.0