Docker Compose Port Already in Use — macOS Zombie Process
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:
| COMMAND | What it usually is |
|---|---|
com.docker / vpnkit | Published container port still mapped |
node | Dev server you thought you stopped |
OrbStack / colima | Alternate runtime holding the publish |
Python | Flask/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:
- Another compose stack published the same host port.
- A rootful process (rarely) holds it.
- 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
lsof/Get-NetTCPConnection— identify PID.- If Docker-related:
docker ps+compose downfor that project. - If
node/language runtime: graceful kill, then-9. - Change
HOST_PORTif two apps need to run together. - 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.