Skip to content
DevOps2026-09-086 min read

Docker port is already allocated: Finding and Freeing the Taken Port

Symptom: the container won't start — a port couldn't be handed out

# During docker run
docker: Error response from daemon: driver failed programming external
connectivity on endpoint ...: Bind for 0.0.0.0:8080 failed: port is already allocated.

# Or the lower-level form
Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use

One-line diagnosis: the host port you're mapping to is already taken. Mind the subject — not the container-internal port (that side is NAT, repeats are fine), but the host port on the left side of -p.

Step one: is the squatter a container or a host process?

# Suspect 1: another container mapped the same port
docker ps --filter "publish=8080"

# Suspect 2: a regular process on the host (nginx, a local dev server...)
sudo ss -tlnp | grep :8080

Run both; whoever is listening becomes obvious. The remedies differ completely:

A container holds it:

docker ps -a --filter "publish=8080"    # include stopped ones
docker rm -f <container-name-or-id>    # remove the old container, port freed
docker stop <container-name>           # or just stop it — that frees the port too

A host process holds it: either stop it (systemctl stop nginx) or rerun your container on a different port.

Frequent scenario 1: the compose config changed, the old container stayed

Compose manages containers by project name (default = directory name). Two common collisions:

  1. You edited docker-compose.yml ports to "start another copy", but the containers from the previous docker compose up are still there — run docker compose down first;
  2. Two different directories share a name (say both app/): identical compose project names fight over containers and ports. Isolate with an explicit project name:
docker compose -p projectA up -d

Frequent scenario 2: container removed, port still held (stale docker-proxy)

Rarely, docker ps shows no occupant while ss shows a docker-proxy process listening — restarting the Docker service clears the leftover:

sudo systemctl restart docker

That's the last resort: all containers restart with it, so assess the impact first on production.

A counterintuitive detail: different bind addresses can coexist

docker run -p 127.0.0.1:8080:80 --name a ...   # loopback only
docker run -p 192.168.1.5:8080:80 --name b ... # a specific NIC

The conflict check is on the "address + port" combination, not the port alone. 127.0.0.1:8080 conflicts with 0.0.0.0:8080 (the latter includes the former), but 127.0.0.1:8080 and 192.168.1.5:8080 can coexist. This detail saves you one round of "nobody is using it, why the conflict" — the invisible binding on the other side is usually a 0.0.0.0.

Relation to host-side EADDRINUSE

The same event from two viewpoints: your application process fails to listen on a taken port with EADDRINUSE; your container fails to map one with port is already allocated. The diagnostic flow is shared — see Port already in use: EADDRINUSE troubleshooting. The opposite direction, "can't connect to a port" (ECONNREFUSED), is a different problem entirely: ECONNREFUSED troubleshooting.

The 30-second diagnosis script

Before changing anything, run: docker ps --format '{{.Names}}\t{{.Ports}}' (which container holds the port now), lsof -i :<port> -P -n or ss -tlnp | grep <port> (which process — including docker-proxy — listens), and docker network ls + a compose ps. This answers the only question that matters: is the port held by a container you forgot (orphan from another project), by docker-proxy of a stopped-but-not-removed container, or by a host process (your dev server). Each has a different remedy; guessing wastes a restart cycle that costs five minutes of warm caches.

Why stopped containers still hold ports

docker stop keeps the container object; a docker run -p published port belongs to that object, and some network modes keep docker-proxy alive for stopped containers until they are removed. The surprise case: docker compose down in project A while project B shares the default bridge with overlapping port mappings — compose only manages its own. The habit that prevents recurrence: unique host ports per project in compose (8081, 8082, ...) rather than everyone fighting for 8080, and docker container prune as part of project teardown.

IPv6: the invisible port holder

lsof -i :8080 showing nothing while Docker still says allocated usually means the port is bound on IPv6 (:::8080) while you checked IPv4. Docker publishes on both stacks by default; a host service bound to 0.0.0.0:8080 does not conflict with :::8080 until something requests dual-stack. ss -tlnp | grep 8080 shows both rows. The fix is binding explicitly (-p 127.0.0.1:8080:80) — which you should do anyway for anything that must not be publicly reachable.

Compose ports: vs expose: (and host networking)

expose: only publishes metadata to linked containers — it never binds a host port, so it cannot cause port is already allocated. Conflicts come from ports: (or the equivalent -p on docker run). Host networking (network_mode: host / --network host) is the other extreme: the process binds the host stack directly, so Docker’s publish map is empty and the failure mode flips back to classic EADDRINUSE inside the container logs. When compose files mix ports: on one service with network_mode: host on another, diagnose each service with the matching tool (docker ps --filter publish= vs ss on the host).

Reading the conflict from docker inspect

When filters feel noisy, inspect the squatter:

docker inspect -f '{{.Name}} {{json .HostConfig.PortBindings}} {{json .NetworkSettings.Ports}}' <id>

PortBindings is what you asked for; NetworkSettings.Ports is what the daemon actually programmed. A mismatch after a partial failure (daemon restart mid-create) is exactly when stale docker-proxy shows up — remove the container, then recreate rather than only restarting the process inside it.

Mapping helpers before you rewrite compose

If you are converting a long docker run -p ... into compose, paste it into Run to Compose and confirm the generated ports: list has unique host sides. For greenfield Dockerfiles, the Dockerfile Generator keeps EXPOSE documentation separate from publish — remember EXPOSE alone never allocates a host port.

Checklist

  1. docker ps --filter "publish=<port>" — does a container hold it?
  2. ss -tlnp | grep :<port> — does a host process hold it? (run both checks)
  3. Old containers: docker rm -f; compose scenarios: docker compose down first
  4. Nobody visible but still failing? systemctl restart docker clears stale docker-proxy
  5. Port reuse: 127.0.0.1 conflicts with 0.0.0.0, but not with a specific NIC IP
  6. Confirm you are not mixing ports: diagnostics with network_mode: host services

Triage notes

Step one requires running both commands (docker ps --filter publish and ss -tlnp) because the squatter falls into two classes — a container or a host process — with completely different remedies; that fork is the article's spine. The intro corrects the subject on purpose: what's occupied is the host port on the left of -p's colon, while the container-internal side is NAT'd and free to repeat. The counterintuitive detail that different bind addresses can coexist gets its own section because conflict is judged on the address-plus-port pair — which is exactly why "nobody seems to be using it, yet it conflicts": the other party is usually an invisible 0.0.0.0 binding.


Provided by ToolVault. Related tools: Dockerfile Generator, Run to Compose, CIDR Calculator, HTTP Status Codes. Related reading: Port already in use (EADDRINUSE), ECONNREFUSED troubleshooting. See the homepage for more developer tools.