How does port publishing work, and what's happening under the hood?
Quick Answer
-p host_port:container_port maps traffic arriving at the host's host_port to the container's container_port. Docker implements this with iptables NAT rules (specifically DNAT), configured automatically when the container starts. Without publishing a port, a container's service is still reachable from other containers on the same Docker network — just not from the host machine or the outside world.
Detailed Answer
What -p actually does
docker run -d -p 8080:80 nginx
This maps port 8080 on the host to port 80 inside the container. A request to http://<host-ip>:8080 gets forwarded to whatever is listening on port 80 inside the container — in this example, that's nginx's default port.
docker run -d -p 80 nginx # publishes container port 80 to a RANDOM available host port
docker run -d -p 127.0.0.1:8080:80 nginx # only binds to the host's loopback interface, not all interfaces
Omitting the host port lets Docker pick a random available one — check it with docker port <container>. Specifying a host IP restricts which network interface the mapping is bound to. That's useful for exposing a port only on the host itself, without making it reachable from the broader network.
The mechanism: iptables DNAT rules
When you publish a port, Docker automatically inserts iptables rules into the host's netfilter configuration. This is a DNAT (Destination NAT) rule: it rewrites the destination of incoming packets, redirecting traffic from the host's published port to the container's actual internal IP and port on its Docker network.
Incoming request: <host-ip>:8080
│
▼ (iptables DNAT rule, inserted automatically by Docker)
Container's internal IP:80 (on the bridge network)
This is similar to how Kubernetes's kube-proxy uses iptables (or IPVS/eBPF) rules to route Service traffic to backing Pods. Both solve the same problem — getting external traffic to the right internal destination — with the same underlying Linux networking primitive.
Why unpublished ports still work between containers
docker network create my-network
docker run -d --network my-network --name api myapi:1.0 # port 3000, NOT published to the host
docker run -d --network my-network --name web mywebapp:1.0
api's port 3000 isn't reachable from the host machine or the internet, since no -p flag was used. But web, on the same Docker network, can still reach api:3000 directly. Containers on the same network can always reach each other's container-internal ports, no publishing needed. Port publishing (-p) is specifically about making a container's port reachable from outside the Docker network — from the host, or from the internet if the host is internet-facing. It's not required for container-to-container communication within the same network.
Publishing all exposed ports at once
docker run -P myapp # capital -P: publish every port listed in the Dockerfile's EXPOSE
# instructions, each to a random host port
EXPOSE alone is just documentation. -P (capital, distinct from lowercase -p) actually turns each EXPOSEd port into a real, published mapping, each to a random host port. It reads the image's own declared EXPOSE list instead of requiring you to specify each mapping manually.
An internal-only service — a database only ever accessed by other containers on the same network — should typically not publish a port at all, to reduce its exposed attack surface. Only the actual entry-point service usually needs one reaching the host or internet.