sehaj/learn
Docker/03 Running containers

03.02 · with code · 9 min

Publishing ports

How a request on your machine reaches a process inside a container, the three ways to write -p, and the bind-address bug everyone hits once.

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.

Reading -p
-p
127.0.0.1host address (optional)
:
8080host port
:
3000container port
Host side first, container side last. Leave out the address and Docker listens on every interface.

Three ways to publish

❯terminal
$ 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 PORTS
p3 0.0.0.0:55000->3000/tcp
p2 127.0.0.1:8081->3000/tcp
p1 0.0.0.0:8080->3000/tcp, [::]:8080->3000/tcp
  • -p 8080:3000 listens 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:3000 listens only on localhost. Use this for databases and admin tools on a laptop or server.
  • -P publishes every EXPOSEd port to a random high port. docker port p3 tells 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:

❯terminal
$ 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:8085
curl: (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.

Predict first

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?

Commit to an answer before you read on.
CheckpointPublish three ways

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

MidfreqMust-knowDebugging

Your container is running and the port is published, but you can't reach the app from the host. How do you debug it?

JuniorfreqMust-knowConcept

What's the difference between EXPOSE and -p?

Report a mistakeEdit this page