Root Cause Diagnosis
The error Bind for 0.0.0.0:PORT failed: port is already allocated means Docker’s own port-publishing table already has that host port and protocol claimed, either by another running container or by a stale docker-proxy and NAT rule left behind after a daemon crash. Attaching a service to an external: true bridge network does not affect this. Published ports are allocated per host IP, port, and protocol regardless of network, so two Compose projects (or a scaled service) that map the same host port collide even when they share one external network. A conflict with a non-Docker host process instead produces listen tcp ...: bind: address already in use, which requires a different fix.
Quick Fix
Find what holds the port, then remove that container or change your host port.
# Docker-level owner (running or created containers publishing the port)
docker ps -a --filter "publish=8080" --format '{{.ID}} {{.Names}} {{.Status}} {{.Ports}}'
# Host-level owner (non-Docker process, or docker-proxy)
sudo ss -ltnp 'sport = :8080'
# Typical fix when it is an old or duplicate project
docker compose -p OLD_PROJECT_NAME down --remove-orphans
docker compose up -d
If the port must stay in use, change the left side of the mapping in docker-compose.yml:
ports:
- "8081:80" # HOST:CONTAINER
Step-by-Step Resolution
1. Identify the owner of the port
docker ps -a --filter "publish=8080"
sudo ss -ltnp 'sport = :8080'
sudo lsof -iTCP:8080 -sTCP:LISTEN
pgrep -a docker-proxy
Read the result as follows:
- A container listed by
docker psmeans a Docker-level conflict. Continue at step 2. docker-proxywith no matching container means stale allocation. Continue at step 6.- Any other process (nginx, apache2, a dev server) means a host conflict. Stop it or pick another host port.
2. Remove duplicate projects and orphans
Compose names projects after the directory by default, so running the same stack from two directories, or after renaming a directory, creates two projects.
docker compose ls -a
docker compose -p PROJECT_NAME down --remove-orphans
docker rm -f CONTAINER_ID
3. Declare the external network correctly
The external network must already exist. Attaching to it does not require, and should not use, published ports for container-to-container traffic.
docker network create --driver bridge shared_net # only if it does not exist
docker network ls --filter driver=bridge
docker-compose.yml
services:
api:
image: myorg/api:latest
networks: [shared_net]
expose:
- "3000" # reachable by other containers on shared_net; not published
proxy:
image: nginx:stable
networks: [shared_net]
ports:
- "80:80" # only the edge service publishes to the host
- "443:443"
networks:
shared_net:
external: true
name: shared_net
Services on the same network reach each other by service name and container port (http://api:3000). Delete ports: from every service that does not need host access.
4. Parameterize or restrict host ports
services:
web:
image: myorg/web:latest
ports:
- "${WEB_HOST_PORT:-8080}:80"
- "127.0.0.1:9000:9000" # bind to loopback only
WEB_HOST_PORT=8081 docker compose up -d
127.0.0.1:8080 and 0.0.0.0:8080 conflict with each other, and so do [::]:8080 and 0.0.0.0:8080 on some setups. Use consistent bindings across projects.
5. Handle scaled services
A fixed host port cannot be shared by replicas. Use a range or an ephemeral port.
ports:
- "8080-8090:80" # each replica takes the next free port in the range
# or
- "80" # random host port; find it with: docker compose port web 80
docker compose up -d --scale web=3
For real load balancing, put a reverse proxy on the shared network, publish only the proxy’s port, and let it route to web by service name.
6. Clear stale allocations
Use this when pgrep -a docker-proxy shows a proxy process for a port that no container claims, or when the error persists after removing all containers.
sudo systemctl restart docker
sudo iptables -t nat -L DOCKER -n | grep 8080
sudo ss -ltnp 'sport = :8080'
If the daemon runs with live-restore enabled in /etc/docker/daemon.json, restarting keeps containers running, so stop the affected containers explicitly first.
7. Verify the result
docker compose up -d
docker compose ps
docker port CONTAINER_NAME
curl -sI http://127.0.0.1:8080 | head -n 1
Why This Fails for Some Users
- Host process conflict misread as Docker conflict:
address already in usecomes from the kernel and points to a non-Docker listener. Common ones are systemd-resolved on 53, cockpit on 9090, and an existing nginx or apache on 80 and 443. Find them withss -ltnp, then stop or disable them. network_mode: host: Port mappings are ignored. The application binds directly on the host, so a conflict shows up as an application startup error, not a Docker error. Change the port in the application’s own configuration.- Protocol mismatch: TCP and UDP are allocated separately.
53:53publishes both TCP and UDP for DNS-style services. A conflict may be on only one of them, so check withss -lunp. - macOS Monterey and later: The AirPlay Receiver listens on ports 5000 and 7000. Disable it under System Settings > General > AirDrop & Handoff, or use another host port.
- Windows with Docker Desktop and WSL2: Hyper-V and Windows NAT reserve dynamic port ranges. Errors here often say a socket access was forbidden rather than “already allocated”. List the ranges and restart the NAT service if the port sits inside one:
netsh interface ipv4 show excludedportrange protocol=tcp net stop winnat net start winnatUse a host port outside the excluded ranges. - Rootless Docker or Podman: Ports below 1024 need
sysctl net.ipv4.ip_unprivileged_port_start=80(persist it in /etc/sysctl.d/99-rootless.conf). Rootless setups forward ports through RootlessKit or slirp4netns instead ofdocker-proxy, sopgrep docker-proxyfinds nothing even when a conflict exists. - Userland proxy disabled: With
"userland-proxy": falsein /etc/docker/daemon.json, nodocker-proxyprocesses appear, and allocations live only in iptables NAT rules. Inspect withsudo iptables -t nat -S DOCKER, orsudo nft list ruleseton nftables systems. - Firewall reloads:
firewall-cmd --reloadorufw reloadcan wipe Docker’s chains. Published ports then stop working or behave inconsistently untilsystemctl restart dockerrecreates them. - Compose project name collisions: Compose v1 and v2 derive default names differently (
myapp_web_1vsmyapp-web-1), so an old v1 stack and a new v2 stack can coexist and hold the same ports. Setname:at the top of docker-compose.yml or use-pconsistently. - Swarm mode: Ports published with
mode: ingressare reserved on every node’s routing mesh, so a stack service can block a plain container’s port even on a node where the service has no task. Usemode: hostfor node-local publishing. - Containers stuck in
Createdstate: A failed start can leave a container inCreatedthat appears to hold the mapping. Remove it withdocker rm CONTAINER_IDand start again. - Multiple external networks: Attaching a service to several external bridge networks does not create additional host ports, and it cannot be used to work around a conflicting mapping.