portop

htop, but for ports

portop

See which process is using a port on Linux, macOS or Windows — and stop it right from your keyboard. See its systemd unit on Linux or Windows service on Windows, its Docker container, and Compose port conflicts too.

Latest release CI License: MIT

Why portop

The everyday basics of seeing what's on your ports — done once, done well, with the context ss and lsof leave out.

●Live, color-coded table

LISTEN, ESTABLISHED and transient states colored at a glance, with per-process CPU% sampled like top.

✕Kill, safely

k then confirm — graceful SIGTERM or forced SIGKILL. Never a stray keypress away.

{}Service & Docker aware

Every row shows the owning systemd .service unit (Linux) or Windows service, and the Docker container behind it — no extra daemons.

●New-port alerts

Freshly opened listening ports are highlighted live, with an optional desktop notification.

±Baseline drift detection

--save-baseline then --diff from a cron job — exit code 3 the moment something unexpected starts listening.

↔Compose port audit

--compose DIR checks a Docker Compose folder and reports expected, unused or conflicting published ports.

◈Web dashboard

portop --web shows socket and process details in the browser, with light and dark themes and confirmed SIGTERM/SIGKILL. Add --web-auth for a sign-in page.

◆Cockpit add-on

The same view under Tools → Ports in Cockpit on Linux. Signal a process after confirmation, using your Cockpit login and permissions.

◆Live settings screen

, opens it: cycle 13 themes (Dracula, Nord, Solarized, Gruvbox, Catppuccin, Tokyo Night, Monokai, Darcula, VS Code, Ubuntu, Rosé Pine, ...) with the arrow keys as the whole UI re-skins live, rebind any of 20 actions — saved automatically, no file to touch.

Beyond wrapping ss and lsof

Neither tells you this on its own — portop does, from the same dashboard.

Baseline drift detection--diff

Save a snapshot of what's listening, then diff against it later — from a cron job or systemd timer. Exit code 3 the instant a port you didn't expect shows up.

Who actually owns this portenter

systemd unit and Docker container, resolved straight from the process's cgroup — no D-Bus, no Docker SDK, no daemon of its own.

Compose ports vs reality--compose

Point it at a Compose folder to see whether configured published ports are actually listening, unused, or owned by another process/container. With Docker access, it reads Compose labels so docker-proxy can still be tied back to the service.

Honest about permissionsi

Can't resolve a row owned by root (docker-proxy, systemd-resolved)? portop tells you exactly why and that sudo would fix it, instead of a silent blank.

Adapts to your terminal80c

Columns drop themselves — least useful first — as the terminal narrows, so rows never wrap into a mess. Works down to 80 columns.

Well-known port names:22

Parsed straight from /etc/services: :22 shows as ssh, :443 as https, right in the table.

Scriptable--json

A clean JSON snapshot for piping into jq, a dashboard, or your own monitoring — same data the TUI shows, no scraping.

In action

portop
portop demo: filtering to a port, viewing process details, the kill confirmation, and the help overlay

Filtering to a port, checking who owns it, and the confirm-before-kill flow — a real recorded session, not staged.

Beyond the terminal

The same live view of ports and processes, for when a terminal isn't at hand: in a browser, or inside Cockpit.

Web dashboard--web
Signing in to the portop web dashboard, filtering to redis and opening its process details

portop --web serves a dashboard on 127.0.0.1:8088, with search, process details and confirmed SIGTERM/SIGKILL. Add --web-auth for a sign-in page; with it, the dashboard can listen beyond loopback and serve HTTPS with --web-tls-cert/--web-tls-key.

Cockpit add-onTools → Ports
Opening Tools, Ports (portop) in Cockpit and inspecting the redis-server process

On Linux, copy cockpit/portop to ~/.local/share/cockpit/portop and open Tools → Ports (portop). It runs through Cockpit's bridge with your login and permissions, so there's no extra service to expose. Setup steps.

Installation

Linux: x86-64, ARM64, ARMv7 and ARMv6. macOS: Intel and Apple Silicon. Windows: x64 and ARM64, via winget, the Microsoft Store or a zip. All release targets and limitations are listed below.

SystemArchitectureTargetFormatsRequirements
LinuxIntel/AMD 64-bitlinux_amd64.tar.gz, .deb, .rpm64-bit x86 OS; amd64 baseline (v1).
LinuxARM64 / AArch64 (64-bit)linux_arm64.tar.gz, .deb, .rpm64-bit ARM OS; ARMv8.0 baseline.
LinuxARMv7 (32-bit)linux_armv7.tar.gz, .deb, .rpm32-bit ARM Linux with hardware floating point (VFPv3).
LinuxARMv6 (32-bit)linux_armv6.tar.gz, .deb, .rpm32-bit ARM Linux with hardware floating point (VFP); includes the CPU target used by original Pi 1 / Zero.
macOSIntel (64-bit)darwin_amd64.tar.gz64-bit Intel Mac; no 32-bit macOS build.
macOSApple Silicon (64-bit)darwin_arm64.tar.gzNative ARM64 Mac; no Rosetta required.
WindowsIntel/AMD 64-bitwindows_amd64.zip, winget, Microsoft StoreWindows 10 1809 or later.
WindowsARM64windows_arm64.zip, winget, Microsoft StoreWindows 11 on ARM.

Archive names: portop_<version>_<target>.tar.gz (.zip on Windows). Linux packages use the same target suffix with .deb or .rpm. Replace the example's linux_amd64 with your target.

Compatibility and limitations
  • Choose for your OS: a 64-bit ARM board with a 32-bit OS needs armv7, or armv6 for older CPUs. ARM64 requires a 64-bit OS.
  • ARM32: hardware floating point is required (VFP for ARMv6, VFPv3 for ARMv7). ARMv5, soft-float armel, big-endian ARM, 32-bit x86, 32-bit Windows and unlisted targets are not supported by these releases.
  • Packages: ARMv6 and ARMv7 DEBs both use armhf metadata; choose by filename and install only one variant. ARMv6 distribution support varies. RPM labels are x86_64, aarch64, armv6hl and armv7hl. If your RPM manager does not accept the architecture, use the matching archive rather than forcing installation.
  • OS versions: prebuilt binaries need no Go or C runtime installation. An old Linux image may still have a kernel too old for the runtime. Check Go's OS requirements; CPU support is not a promise that every historical OS version works.
  • Linux: readable /proc/net is required. Containers show their own network namespace. Root may be needed to identify other users' processes. Systemd and Docker enrichment depend on accessible services and metadata.
  • Docker Compose audit: fixed ports: entries are checked against host listeners. Docker access lets portop confirm the Compose service behind published ports and docker-proxy; without it, a listening port can still be reported but the expected service may be unknown.
  • macOS: uses system lsof and ps; scans may be slower. Some sockets are omitted without sudo. Systemd, Docker Desktop VM process metadata and thread counts are unavailable. Non-notarized binaries may need first-launch approval: see the macOS tab.
  • Windows: sockets and their owning PID come from the IP Helper API, with no elevation needed. The SYSTEMD column becomes SERVICE (the Windows services hosted by the process). Docker Desktop containers are matched by published port through the docker CLI. Details of other users' or elevated processes need an Administrator terminal; both kill choices end the process.
  • Validation: build/emulation checks do not replace physical ARM or native Mac testing. See the validation report for the checks performed.

Detects all six Linux and macOS targets above. On Linux ARM64 kernels, getconf LONG_BIT distinguishes a 32-bit userland; if unavailable, detection uses the kernel architecture. For custom setups, download the script and override detection:

curl -fsSL https://raw.githubusercontent.com/padovanl/portop/main/install.sh -o install.sh
PORTOP_ARCH=armv7 sh install.sh

Overrides: amd64, arm64, armv6, armv7. macOS accepts amd64 and arm64 only.

# detects arch, verifies the release checksum
curl -fsSL https://raw.githubusercontent.com/padovanl/portop/main/install.sh | sh
# replace <version> with the latest release number, no leading "v"
curl -fLO https://github.com/padovanl/portop/releases/latest/download/portop_<version>_linux_amd64.deb
sudo dpkg -i portop_<version>_linux_amd64.deb
# download the .rpm for your architecture from Releases
sudo dnf install ./portop_<version>_linux_amd64.rpm
# openSUSE: sudo zypper install ./portop_<version>_linux_amd64.rpm

Apple Silicon and Intel: the installer selects the matching archive and verifies its checksum.

# automatic installation
curl -fsSL https://raw.githubusercontent.com/padovanl/portop/main/install.sh | sh
portop
Prefer a manual download?

Use darwin_arm64 for Apple Silicon or darwin_amd64 for Intel. Replace <version> with the release number.

archive="portop_<version>_darwin_arm64.tar.gz" # Intel: darwin_amd64
curl -fLO "https://github.com/padovanl/portop/releases/latest/download/$archive"
tar -xzf "$archive"
sudo mkdir -p /usr/local/bin
sudo install -m 755 portop /usr/local/bin/portop
portop
First launch: if macOS blocks portop

The binaries are not notarized by Apple. A browser download may trigger Gatekeeper. After checking that the binary comes from this repository's release, approve this app:

  1. Run portop. If macOS shows “portop” Not Opened, click Done.
  2. Open System Settings → Privacy & Security, find the message about portop, and click Open Anyway (or Allow Anyway, depending on macOS).
  3. Run portop again if prompted, confirm Open Anyway, and authenticate when macOS asks.

Adapted from the pkgtui first-launch guide. See Apple's instructions for your macOS version.

Uses the system's lsof and ps. Run sudo portop to include other users' processes; some sockets may otherwise be missing. Systemd, Docker Desktop VM process metadata and thread counts are unavailable on macOS.

x64 and ARM64. Pick one of the three options; each puts portop on your PATH.

# winget (Windows Package Manager)
winget install padovanl.portop
portop

Or get it from the Microsoft Store (search for portop): updates arrive automatically and portop works from any terminal.

Prefer the zip from the release?

Download portop_<version>_windows_amd64.zip (or windows_arm64) from the latest release, extract portop.exe and put it in a folder on your PATH. In PowerShell:

# replace <version> with the latest release number, no leading "v"
$zip = "portop_<version>_windows_amd64.zip"   # ARM64: windows_arm64
Invoke-WebRequest "https://github.com/padovanl/portop/releases/latest/download/$zip" -OutFile $zip
$dir = "$env:LOCALAPPDATA\Programs\portop"
Expand-Archive $zip -DestinationPath $dir -Force
# add it to your user PATH, then open a new terminal and run portop
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";$dir", "User")

Run it in Windows Terminal or any modern console. Socket owners are visible without elevation; open an Administrator terminal to see details of elevated or other users' processes.

curl -fLO https://github.com/padovanl/portop/releases/latest/download/portop_<version>_linux_amd64.tar.gz
tar -xzf portop_<version>_linux_amd64.tar.gz
sudo install -m 755 portop /usr/local/bin/portop
go install github.com/padovanl/portop/cmd/portop@latest

All architecture targets, Linux .deb/.rpm packages, Windows zips and checksums on the releases page.

Every key, one dashboard

The complete keybinding reference — also in-app any time with ?, and fully remappable via config.yml.

KeyAction
↑ / ↓Move the cursor
PgUp / PgDnMove one visible page at a time
MouseHover/select rows, wheel-scroll, open actions, and click headings to sort
g / GJump to top / bottom
enterProcess details — cmdline, exe, cwd, user, RSS, start time
kKill process, then y=SIGTERM or f=SIGKILL
oOpen http(s)://localhost:PORT in the browser
f / /Filter/search by port, process or PID
vToggle IPv4 / IPv6 / both
eShow/hide ESTABLISHED connections
sCycle sort column
cCopy selected row to clipboard
nClear the new-port highlight
rRefresh now
,Settings — live theme picker, rebind any key
?Help
qQuit