Something is already listening on the host port you published. Find it in ten seconds, then decide whether to move it or move your container.
A previously started service, another container, or a leftover zombie from a crashed run holds the port.
Two compose projects or two containers publishing the same host port — the second one loses.
sudo ss -ltnp | grep -E ':8080\b'
docker ps --format '{{.Names}} {{.Ports}}' | grep 8080
docker run -p 8081:80 myapp # or: docker stop <holder>
sudo fuser -k 8080/tcp # use with care — it kills the holder
For compose projects that fight over ports, prefer letting services talk over the internal network (service names, no -p) and publishing only the front door once.
Something outside Docker: another host process (check ss -ltnp | grep :port) or a stopped-but-not-removed container's network config. ss shows the PID directly; docker ps -a shows the stopped container.
Stopped containers still hold their published port mappings until removed. docker rm the old container, or docker compose down instead of stop — the port frees with the container object.
A production-shaped compose stack: healthchecks, resource limits, log rotation. Never debug a boot race again.
Docker Production Starter — $19 →One-time. Yours to modify. Instant download from the NinjaOps template store.