How does port publishing work, and what's happening under the hood?
Quick Answer
-p host_port:container_port creates a mapping so that traffic arriving at the host's host_port is forwarded to the container's container_port — implemented via iptables NAT rules (specifically, DNAT) that Docker configures 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 itself 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, 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 (visible via docker port <container>). Specifying a host IP restricts which network interface the mapping is bound to — useful for only exposing a port locally 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. Specifically, this is a DNAT (Destination NAT) rule that rewrites the destination of incoming packets, redirecting traffic arriving at 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 conceptually similar to how Kubernetes's kube-proxy uses iptables (or IPVS/eBPF) rules to route Service traffic to backing Pods (see that stack's kube-proxy question). Docker's port publishing solves an analogous problem — getting external traffic to the right internal destination — using 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 at all (no -p flag was used). But web, being on the same Docker network, can still reach api:3000 directly. This is because containers on the same network can always reach each other's container-internal ports without any publishing needed. Port publishing (-p) is specifically about making a container's port reachable from outside the Docker network entirely — from the host, or from the internet if the host itself is internet-facing. It is not a requirement 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
Recall from the Dockerfile topic that EXPOSE alone is just documentation or metadata. -P (capital, distinct from lowercase -p) is what actually turns each EXPOSEd port into a real, published mapping, to a randomly chosen host port each. It reads the image's own declared EXPOSE list, rather than 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, reducing its exposed attack surface. Only the actual entry-point service usually needs one reaching the host or internet.