DEV Community

Peon Sh
Peon Sh

Posted on Originally published at peon.sh

Fix “Port Is Already in Use” Errors on Linux and Docker

EADDRINUSE and Docker port binding failures: find what holds the port, free it safely, and design so it never happens again.

The error and what it means
Whether it appears as EADDRINUSE in Node, "address already in use" from Docker, or "bind: address already in use" from nginx, the meaning is identical: exactly one process may listen on a given IP:port pair, and something already holds the one you want. The fix is never to reboot and hope; it is to identify the holder, decide whether it should be there, and act accordingly.

Find the holder
Modern Linux gives you the owning process in one command:

sudo ss -tlnp | grep :3000
# LISTEN 0 511 *:3000 users:(("node",pid=1234,fd=20))
# or the older equivalent
sudo lsof -i :3000
# if it's a container publishing the port
docker ps --format '{{.Names}}\t{{.Ports}}' | grep 3000

Common culprits, in order of frequency

  • A previous instance of your own app: a dev server you forgot, or an orphaned process after a crashed deploy, kill the specific PID, not everything matching a name
  • Another container publishing the same host port: two services both trying to own 8080:..., only one can win
  • System services on well-known ports: a distro-installed Apache or nginx squatting on 80/443, blocking your reverse proxy container (disable with systemctl disable --now)
  • systemd-resolved on port 53, relevant when running Pi-hole or other DNS containers
  • TIME_WAIT ghosts: right after a restart the port looks busy for up to a minute; SO_REUSEADDR in the app makes rebinding immediate, and ss shows no LISTEN holder in this case

The structural fix: stop publishing ports
On a server with a reverse proxy, host port conflicts are a symptom of an anti-pattern: app containers should not publish host ports at all. Each app listens on its internal port on the Docker network; the proxy is the only process binding 80 and 443, and it routes by hostname. Under this design, two apps can both use "port 3000" internally forever without conflict, because no one is competing for host ports.

This is how Peon deploys services by default: no published ports on app containers, proxy-only ingress. If you are hand-writing compose files, deleting the ports: section from app services (keeping it only on the proxy) is the single change that retires this whole error class.

Quick decision table

  • Holder is your old process: kill , then fix whatever leaves orphans (usually a missing SIGTERM handler)
  • Holder is another container: change one side’s published port, or better, unpublish both and route via the proxy
  • Holder is a system service you need: move your service to another port
  • Holder is a system service you do not need: disable it permanently
  • No holder visible: TIME_WAIT, wait 60 seconds or fix the app’s socket options

Top comments (0)