How does port publishing work, and what's happening under the hood?

6 minintermediateport-publishingnetworkingiptables

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.