Port Mapping — exposing a container to the outside
Your app inside the container is happily listening on port 3000 — but nobody outside can reach it, because the container sits on a private IP. Port mapping is the bridge that connects the two.
The problem
Your container lives on a private bridge (say 172.20.0.2:3000). Your host sits on the real network with its own IP (say 10.0.1.42). Someone typing that host IP into a browser has no idea the container IP even exists — and no way to reach it. For that request to land, the host itself has to listen and forward it along.
The -p host:container syntax
docker run -d -p 9090:3000 my-app
Read it out loud: "anything that hits the host on port 9090, forward it into this container on port 3000". Host port first, container port second — that order trips people up constantly, so it's worth burning into memory.
Variations you'll see
| Flag | Behaviour |
|---|---|
-p 9090:3000 | Bind host port 9090 → container port 3000 (all host interfaces). |
-p 127.0.0.1:9090:3000 | Only accessible from the host itself, not from other machines. |
-p 9090:3000/udp | UDP instead of TCP. |
-p 3000 | Same port on host and container. |
-P (capital) | Auto-map every EXPOSEd container port to a random high host port. |
See what's mapped:
docker ps
docker port <container>
EXPOSE alone doesn't expose anythingThe EXPOSE directive in a Dockerfile is documentation. It tells other tools "this container listens on 3000". It does not publish the port. You still need -p at run time.
You'll hear this one a lot. The container is up, the app is listening, but hitting the host in a browser gives nothing. Nine times out of ten it's a missing publish. Run docker ps and look at the PORTS column: if you don't see something like 0.0.0.0:9090->3000/tcp, the container was started without -p — or the Dockerfile only has an EXPOSE line, which documents the port but never publishes it. Restart the container with the right -p host:container mapping and the outside world can finally get in.
What happens when two containers claim the same host port
The second one refuses to start. The host can only bind one process to a given port + interface at a time — no sharing. Just change the host side of the mapping and they'll both come up happily: -p 9090:3000 for one, -p 9091:3000 for the other.
Publish rules in Docker Compose
services:
web:
image: my-app
ports:
- "9090:3000" # public
- "127.0.0.1:9091:3001" # localhost only
- "9092:3002/udp" # UDP
Beyond port mapping — the load balancer
The moment you're running more than one container of the same app, giving each its own host port stops making sense — nobody wants to remember which replica lives on 9090 versus 9093. That's the job of load balancers and reverse proxies (nginx, Traefik, AWS ALB, Kubernetes Ingress): one public endpoint out front, fanning traffic out to all your container backends behind it. In Kubernetes, that front door is called a Service.
Key takeaways
- Port mapping (
-p host:container) exposes a container port to the outside via the host. - Under the hood it's a DNAT rule (
iptables). - You can bind to
127.0.0.1only, or switch to UDP. EXPOSEin a Dockerfile is documentation — it does not publish.- Beyond a handful of containers, use a load balancer instead of mapping every one.