The Scenario
You run docker compose up or docker run -p 80:80 ... and get slapped with:
Error response from daemon: driver failed programming external connectivity on endpoint my_container:
Bind for 0.0.0.0:80 failed: port is already allocated
Something else claimed port 80 first. Docker can't wrestle it away, so your container never starts.
What's Actually Happening
When Docker maps a port, it creates a binding on the host at 0.0.0.0:80 and forwards traffic into the container. Only one process can hold that binding at a time. If anything else got there first โ another container, a system nginx, a forgotten dev server โ Docker loses and throws this error.
The usual suspects:
- A previous Docker container that didn't stop cleanly (SIGKILL, OOM kill, power loss)
- System nginx or Apache installed directly on the host
- Another
docker composeproject already running on port 80 - A Node.js or Python dev server still running in another terminal
Step 1 โ Find What's Using the Port
On Linux / macOS
sudo ss -tlnp | grep :80
# or
sudo lsof -i :80
Sample output from ss:
LISTEN 0 128 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1234,fd=6))
From lsof:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 1234 root 6u IPv4 12345 0t0 TCP *:http (LISTEN)
Both tell you the process name and PID. That's all you need.
On Windows (PowerShell)
netstat -ano | findstr :80
Then look up the PID:
tasklist | findstr <PID>
Check if it's another Docker container
docker ps --format 'table {{.Names}}\t{{.Ports}}'
Scan the Ports column for anything mapped to :80.
Step 2 โ Quick Fix
If it's a Docker container
# Stop a specific container
docker stop <container_name_or_id>
# Or stop everything running
docker stop $(docker ps -q)
Then re-run your original command.
If it's a system service (nginx, apache)
# Linux โ systemd
sudo systemctl stop nginx
sudo systemctl stop apache2
# macOS โ brew services
brew services stop nginx
If it's a random process
# Kill by PID from the lsof/ss output
sudo kill -9 <PID>
Step 3 โ Verify the Port is Free
sudo ss -tlnp | grep :80
No output? Port is clear. Start your container:
docker compose up -d
Confirm it's bound correctly:
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
Expected output:
NAMES STATUS PORTS
my_app Up 3 seconds 0.0.0.0:80->80/tcp
Permanent Fix โ Stop the Conflict from Coming Back
Option A: Change the host port in your compose file
Port 80 on the host isn't sacred. Map to 8080 instead and call it a day:
services:
app:
image: nginx
ports:
- "8080:80" # Host 8080 โ Container 80
Access it at http://localhost:8080. For production behind a load balancer, the host port barely matters anyway.
Option B: Disable the conflicting system service permanently
# Stop nginx and keep it from starting on boot
sudo systemctl disable --now nginx
# Just stop it for now without disabling
sudo systemctl stop nginx
Option C: Put a reverse proxy in front of everything
Running multiple services that all want port 80? Don't expose them directly. One proxy container takes port 80 and routes traffic internally โ everything else stays off the host network.
services:
proxy:
image: traefik:v3
ports:
- "80:80"
app:
image: my-app
# No ports exposed โ traffic routes through proxy
Edge Case: Port Shows as Free but Docker Still Fails
This catches people off guard. A stopped container can still hold onto its port binding โ especially after a hard kill or OOM event. The fix is to remove the container entirely, not just stop it:
# List all containers including stopped ones
docker ps -a
# Remove the stuck container
docker rm <container_name_or_id>
After an unclean shutdown with Docker Compose, skip the manual cleanup and just run:
docker compose down
docker compose up -d
compose down tears down networks and clears bindings properly. A plain docker stop skips that step, which is why the ghost binding lingers.
Quick Reference
- Find port owner (Linux):
sudo ss -tlnp | grep :80 - Find port owner (macOS):
sudo lsof -i :80 - Stop conflicting Docker container:
docker stop <name> - Stop conflicting system service:
sudo systemctl stop nginx - Full cleanup:
docker compose down && docker compose up -d

