The host port you asked Docker to publish is already bound by something. Docker cannot share a port, so the container refuses to start.
The owner is one of three things: another container, a stopped container Docker still has a reservation for, or an ordinary process on the host. Identifying which takes one command each.
Find the owner
Start with containers, since that is the usual answer:
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep 3000
api-old Exited (0) 2 days ago
api Up 4 minutes 0.0.0.0:3000->3000/tcp
If nothing there holds it, check the host:
sudo lsof -i :3000
COMMAND PID USER FD TYPE DEVICE NODE NAME
node 48213 alice 22u IPv4 0x8f3c1 TCP *:3000 (LISTEN)
# Linux alternative
sudo ss -tlnp | grep :3000
LISTEN 0 511 0.0.0.0:3000 0.0.0.0:* users:(("node",pid=48213,fd=22))
A node, python or java process is a development server someone started outside Docker. docker-proxy means Docker itself still holds it. See below.
Cause 1: Another container
docker ps --filter "publish=3000" --format '{{.Names}}'
docker stop api-old
Compose users: a project left running from another directory is the classic case, because Compose namespaces containers by project but not host ports.
docker compose ls
NAME STATUS CONFIG FILES
orders-api running(3) /Users/alice/work/orders/compose.yaml
orders-api-old running(2) /Users/alice/work/orders-old/compose.yaml
cd ~/work/orders-old && docker compose down
Cause 2: A stopped container still holding the reservation
The confusing one. A container that exited can leave Docker's port mapping in place, so lsof shows docker-proxy with no running container to explain it:
sudo lsof -i :3000
COMMAND PID USER FD TYPE NODE NAME
docker-pr 12043 root 4u IPv6 TCP *:3000 (LISTEN)
docker ps -a --filter "status=exited" --format '{{.Names}}\t{{.Ports}}'
docker rm api-old
docker stop is not enough here. The container must be removed. If removing it does not release the port, the daemon's networking state is stale:
sudo systemctl restart docker
On Docker Desktop, restart the application. That is a blunt instrument and it does resolve the case where nothing visible owns the port.
Cause 3: A host process
sudo lsof -i :5432
COMMAND PID USER FD TYPE NODE NAME
postgres 894 postgres 7u IPv6 TCP *:5432 (LISTEN)
A locally installed PostgreSQL, MySQL, Redis or nginx conflicting with the containerised one. This is extremely common on developer machines, where 5432, 3306, 6379 and 80 are frequently taken by services installed years earlier.
Two options. Stop the host service:
sudo systemctl stop postgresql # Linux
brew services stop postgresql@16 # macOS
Or, better, change the host port instead, so the two coexist:
docker run -d -p 15432:5432 postgres:17
psql -h localhost -p 15432 -U postgres
Only the left-hand number changes. The container still listens on 5432 internally, so nothing inside it or in your Compose network needs adjusting.
Fixing it properly in Compose
Hardcoded host ports are what make this recur. Make them configurable:
services:
api:
build: .
ports:
- "${API_PORT:-3000}:3000"
db:
image: postgres:17
ports:
- "${DB_PORT:-15432}:5432"
API_PORT=3001 docker compose up -d
Better still, do not publish ports you do not need from the host at all. Containers on the same Compose network reach each other by service name on the container port, so a database only needs publishing if you connect to it from your own machine:
services:
api:
ports:
- "3000:3000" # you browse this
db:
image: postgres:17
# no ports. The api reaches it at db:5432
That removes the conflict entirely and is closer to how the stack runs in production.
For throwaway containers, let Docker choose:
docker run -d -P nginx
docker port <container>
80/tcp -> 0.0.0.0:32768
Bind to localhost, not everything
Worth changing while you are here:
docker run -d -p 127.0.0.1:3000:3000 myapp
-p 3000:3000 binds 0.0.0.0, exposing the container to your whole network, and on Linux, Docker writes iptables rules that bypass ufw and firewalld, so a port you believed was firewalled is reachable from the LAN. Prefixing 127.0.0.1: limits it to your own machine.
A checklist
docker ps -a --format '…{{.Ports}}' | grep <port>. A container, running or exited?- Exited container holding it →
docker rm, not justdocker stop. sudo lsof -i :<port>orsudo ss -tlnp, who owns it on the host?docker-proxywith no container → restart the Docker daemon.- A host service → stop it, or change the host side of the mapping.
docker compose ls. A project running from another directory?- Long term: parameterise host ports, and stop publishing what you do not need.
- Bind
127.0.0.1:so containers are not exposed to your network.
Frequently Asked Questions
How do I find what is using a port that Docker says is already allocated?
Two commands cover it. docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' shows every container including stopped ones and what each publishes. If none of them holds the port, sudo lsof -i :3000 or sudo ss -tlnp | grep :3000 names the host process. A result of docker-proxy means Docker itself still has a reservation, usually from a stopped container that has not been removed.
Why is a port still in use after I stopped the container?
Because stopping a container does not always release Docker's port reservation; removing it does. Run docker rm <name> rather than only docker stop. If the port is still held with no container to explain it, lsof showing docker-proxy, the daemon's networking state is stale and restarting Docker clears it. On Docker Desktop that means restarting the application.
How do I run a container when the host port is taken by a local service?
Change the host side of the mapping rather than stopping the service: docker run -p 15432:5432 postgres:17 publishes to 15432 while the container still listens on 5432 internally. Nothing inside the container or elsewhere on its Docker network needs to change, and you connect with psql -h localhost -p 15432. This is usually better than shutting down a locally installed database you may want later.
Why does Compose keep hitting port conflicts?
Because host ports are global while Compose projects are namespaced. Two projects can both try to publish 3000, and a stack left running from another directory holds it. docker compose ls lists every running project with its config file path, which finds the culprit immediately. Parameterise the host port as "${API_PORT:-3000}:3000" so it can be overridden, and stop publishing services that only other containers need.
Do I need to publish a database port in Compose?
Only if you connect to it from your own machine. Containers on the same Compose network reach each other by service name on the container port, so an API talks to db:5432 whether or not 5432 is published to the host. Removing the ports entry for internal services eliminates this class of conflict entirely and matches how the stack runs in production, where nothing exposes a database publicly.
Is -p 3000:3000 safe on a shared network?
Not especially. That form binds 0.0.0.0, making the container reachable from anything that can route to your machine, and on Linux, Docker inserts iptables rules ahead of ufw and firewalld, so a port you believe is firewalled is in fact open on the LAN. Use -p 127.0.0.1:3000:3000 to bind only the loopback interface, which is what you almost always want for local development.