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:
- You edited
docker-compose.ymlports to "start another copy", but the containers from the previousdocker compose upare still there — rundocker compose downfirst; - 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
docker ps --filter "publish=<port>"— does a container hold it?ss -tlnp | grep :<port>— does a host process hold it? (run both checks)- Old containers:
docker rm -f; compose scenarios:docker compose downfirst - Nobody visible but still failing?
systemctl restart dockerclears stale docker-proxy - Port reuse: 127.0.0.1 conflicts with 0.0.0.0, but not with a specific NIC IP
- Confirm you are not mixing
ports:diagnostics withnetwork_mode: hostservices
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.
Related Tools
Related Articles
Linux Crontab in Practice: Commands, Logs, and Debugging
Crontab beyond syntax: three must-know commands, /etc/cron.d vs user crontabs, five-step debugging, PATH/timezone/% escaping, MAILTO, and what to do when you need second-level scheduling.
docker run to docker-compose: The Complete Migration Guide (with Flag Map)
Migrate docker run commands to docker-compose.yml: five advantages of compose over raw run, a flag-by-flag mapping table (ports, volumes, env, restart, networks), the classic pitfalls (bare -e passthrough, anonymous volumes, --rm semantics), and an online converter that runs locally in your browser.
Permission denied (publickey): 6 Reasons Git Push Fails Over SSH (and the Fix for Each)
git clone or push rejected with Permission denied (publickey) fatal: Could not read from remote repository? Covers missing keygen, key not loaded in the agent, public key not added to GitHub/GitLab, multi-account key routing with ~/.ssh/config, deploy key limits, and wrong remote URLs — with ssh -v diagnostics.