Fix 'Bind for 0.0.0.0:80 failed: port is already allocated' in Docker

beginner๐Ÿณ Docker2026-03-17| Docker Engine 20+, Docker Compose v2, Linux/macOS/Windows (WSL2)

Error Message

Bind for 0.0.0.0:80 failed: port is already allocated
#docker#port#bind

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 compose project 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

Related Error Notes