The core idea
A container has its own network stack, so a server listening on port 3000 inside it is listening on the
container's port 3000, not your machine's. Publishing a port (-p) tells Docker to forward traffic from a port
on the host to a port in the container.
Three ways to publish
$ docker run -d --name p1 -p 8080:3000 notes-api$ docker run -d --name p2 -p 127.0.0.1:8081:3000 notes-api$ docker run -d --name p3 -P notes-api$ docker ps --format 'table {{.Names}}\t{{.Ports}}'NAMES PORTSp3 0.0.0.0:55000->3000/tcpp2 127.0.0.1:8081->3000/tcpp1 0.0.0.0:8080->3000/tcp, [::]:8080->3000/tcp
-p 8080:3000listens on every interface of your machine, so anything that can reach your machine can reach the app. Convenient, and often more open than you meant.-p 127.0.0.1:8081:3000listens only on localhost. Use this for databases and admin tools on a laptop or server.-Ppublishes everyEXPOSEd port to a random high port.docker port p3tells you which.
docker port NAME lists a container's mappings at any time.
The bind-address bug
Here's a server that listens on 127.0.0.1 inside its container:
$ docker run -d --name loopy -p 8085:3000 node:22-slim \node -e 'require("http").createServer((q,s)=>s.end("hi")).listen(3000,"127.0.0.1")'$ curl localhost:8085curl: (56) Recv failure: Connection reset by peer$ docker exec loopy node -e 'fetch("http://127.0.0.1:3000").then(r=>r.text()).then(console.log)'hi
It works from inside and fails from outside. 127.0.0.1 inside the container is the container's loopback, which
forwarded traffic never arrives on. Docker delivers published traffic to the container's network interface, so the app
must listen on 0.0.0.0 (all interfaces). The notes API does this by not passing an address to listen.
Many frameworks default to 127.0.0.1 in development mode, which is why this bites so often: Vite needs --host,
Flask needs --host=0.0.0.0, Rails needs -b 0.0.0.0.
You run -p 8080:3000. Inside the container the app logs 'listening on http://localhost:3000'. curl localhost:8080 gets 'connection reset'. What's the fix?
Run the notes API three times with the three forms above. Hit each with curl. Then reproduce the bind-address bug with the one-liner and fix it by changing "127.0.0.1" to "0.0.0.0".
STUCK? ▸
port is already allocated: another container or program has that host port. docker ps shows which container; lsof -i :8080 shows any program.
Recap
Interview questions
Your container is running and the port is published, but you can't reach the app from the host. How do you debug it?
What's the difference between EXPOSE and -p?