Docker Compose Port Already in Use — macOS Zombie Process

Docker devops

docker compose up dies with Bind for 0.0.0.0:3000 failed: port is already allocated. You killed the terminal. You closed the IDE. The port is still taken. On macOS this is especially annoying because Docker Desktop’s VM, node from a forgotten pnpm dev, and an old compose project can all fight for the same host port — and Activity Monitor will not always make the culprit obvious.

Who is actually listening

# macOS / Linux
lsof -nP -iTCP:3000 -sTCP:LISTEN

# if you prefer
sudo lsof -i :3000

Read the PID and COMMAND columns. Common owners:

COMMANDWhat it usually is
com.docker / vpnkitPublished container port still mapped
nodeDev server you thought you stopped
OrbStack / colimaAlternate runtime holding the publish
PythonFlask/FastAPI left in background

On Windows (PowerShell): Get-NetTCPConnection -LocalPort 3000 then Get-Process -Id <OwningProcess>.

Docker thinks it released the port — it did not

docker ps -a --filter publish=3000
docker compose ls
docker compose -p oldproject down

Compose project names matter. Running docker compose up from two directories creates two projects; Ctrl+C in one terminal does not tear down the other. docker compose down from the same directory (same project name) is required. Nuclear option for a stuck publish:

docker ps -q | xargs -r docker stop
# or remove the specific container id from docker ps

Docker Desktop on macOS occasionally needs Restart when the Linux VM’s port forward desyncs from the Mac’s socket table — rare, but real after sleep/wake with heavy compose usage.

Node zombies after compose

You mapped 3000:3000 and also ran Next on the host “for debugging.” Compose dies; Node lives. Or the opposite: hot reload spawned a child that ignored SIGHUP. After lsof shows node:

kill <PID>
# still there?
kill -9 <PID>

Prefer fixing the process group: start dev servers with foreground terminals, or use a process manager that tracks children. Avoid nohup npm run dev & on a laptop you will forget.

Compose file patterns that reduce collisions

services:
  web:
    ports:
      - "${HOST_PORT:-3000}:3000"

Give each developer / each stack a different HOST_PORT in .env. Do not hardcode 3000:3000 in five side projects.

For databases, collisions on 5432 are just as common — Postgres on the host plus postgres in compose. Same lsof workflow.

When the error is inside the container network

Error starting userland proxy and friends sometimes mean the container port is fine but the host proxy cannot bind. Check:

  1. Another compose stack published the same host port.
  2. A rootful process (rarely) holds it.
  3. Hyperkit/Virtio networking glitch — restart Docker Desktop.

Binding only to localhost can avoid fights with LAN-facing tools:

ports:
  - "127.0.0.1:3000:3000"

A sane recovery order

  1. lsof / Get-NetTCPConnection — identify PID.
  2. If Docker-related: docker ps + compose down for that project.
  3. If node/language runtime: graceful kill, then -9.
  4. Change HOST_PORT if two apps need to run together.
  5. Restart Docker Desktop only after the above fails post sleep/wake.

Port-in-use is not a Compose bug. It is a leftover owner. Find the owner before you rewrite the YAML — rewriting the YAML without freeing the socket just moves the conflict to 3001.