Fixing 'Port Is Already Allocated' Errors in Compose

docker compose up aborts with Bind for 0.0.0.0:8080 failed: port is already allocated because something already holds the host port your service wants to publish. This is the most common networking failure in Local Network & Port Mapping, and it has three distinct culprits: a leftover container, a host process, or a duplicate mapping in your own Compose files.

Diagnostic

Reproduce and identify the holder. The error names the exact host port:

Error response from daemon: driver failed programming external connectivity on endpoint app:
Bind for 0.0.0.0:8080 failed: port is already allocated

First check whether another container already publishes the port:

#!/usr/bin/env bash
# who-has-the-port.sh
set -euo pipefail
PORT=8080
echo "== Docker containers publishing :$PORT =="
docker ps --filter "publish=${PORT}" --format '{{.Names}}\t{{.Ports}}'
echo "== Host processes listening on :$PORT =="
ss -tulpn "sport = :${PORT}" 2>/dev/null || lsof -nP -iTCP:${PORT} -sTCP:LISTEN
# BAD: a stale container from a previous run still holds the port
== Docker containers publishing :8080 ==
oldstack-app-1   0.0.0.0:8080->8080/tcp
== Host processes listening on :8080 ==
COMMAND   PID  USER   FD  TYPE  DEVICE  NODE NAME
com.docke 4312 you    37u IPv4  0x...   TCP *:8080 (LISTEN)

Root cause

A host TCP port can be bound by exactly one process. The error fires when Docker's proxy tries to bind a port that is already held — usually a container from a prior docker compose up that was never torn down (Compose keeps containers across runs unless you down), a non-Docker host process such as a local dev server or a system service, or a second ports: mapping for the same host port elsewhere in your merged Compose files.

Resolution

  1. Stop the stale Compose stack that still owns the port. Running down (not just stop) releases the published ports and removes the containers.
#!/usr/bin/env bash
set -euo pipefail
docker compose down --remove-orphans
  1. If a container from a different project holds it, stop that one specifically.
#!/usr/bin/env bash
set -euo pipefail
docker stop "$(docker ps --filter "publish=8080" -q)"
  1. If a host process (not Docker) holds it, stop that process or change your published port.
#!/usr/bin/env bash
set -euo pipefail
# Identify, then stop the host process by PID from the diagnostic above
kill "$(lsof -nP -iTCP:8080 -sTCP:LISTEN -t)"
  1. If you cannot free the port, remap to a free host port and bind to loopback. The container port stays the same, so internal service-to-service URLs are unaffected.
# docker-compose.override.yml
services:
  app:
    ports:
      - "127.0.0.1:${APP_PORT:-8081}:8080"
  1. Bring the stack back up and confirm.
#!/usr/bin/env bash
set -euo pipefail
docker compose up -d --wait

Expected output

[+] Running 2/2
 ✔ Container app-db-1   Healthy
 ✔ Container app-app-1  Started
docker compose ps --format '{{.Name}}\t{{.Ports}}'
# app-app-1   127.0.0.1:8081->8080/tcp

Prevention

  1. Always tear down before switching branches or stacks. A make down that runs docker compose down --remove-orphans keeps host ports clean.
  2. Drive every published port through an .env default so two stacks can coexist, and reject hardcoded ports in review.
# docker-compose.yml
services:
  app:
    ports:
      - "${APP_PORT:-8080}:8080"
  1. Add a pre-up check that fails fast with a readable message instead of the raw daemon error.
#!/usr/bin/env bash
# bin/check-ports.sh — run before `docker compose up`
set -euo pipefail
for p in "${APP_PORT:-8080}" "${DB_PORT:-5432}"; do
  if ss -tuln "sport = :${p}" 2>/dev/null | grep -q ":${p}"; then
    echo "Port ${p} is in use; set a different value in .env before starting." >&2
    exit 1
  fi
done
echo "All required ports are free."

macOS (Docker Desktop): lsof may need sudo to see all listeners; prefer ss inside the Linux VM. Some Apple system services (AirPlay Receiver) hold :5000 — disable it or remap. WSL2: run the scan inside the distro; from PowerShell you query the Windows host stack and miss WSL2-bound listeners. Bind to 0.0.0.0 rather than [::] to avoid IPv6 binding failures. Apple Silicon (ARM64): behavior matches AMD64, but BSD lsof output formatting differs from GNU lsof; the ss path is more portable.

Rollback

#!/usr/bin/env bash
set -euo pipefail
git checkout -- docker-compose.override.yml 2>/dev/null || true
docker compose down --remove-orphans && docker compose up -d --wait