Publishing Ports
A container's ports are reachable from other containers on its network, but not from the host or the outside world until you publish them. -p 8080:80 tells the daemon to install an iptables DNAT rule that forwards a host port to a container port. That is the whole mechanism: a forwarding rule the kernel applies on the way in.
-p 8080:80 doeshost:8080container:80Publishing is a deliberate decision about attack surface, not a checkbox you tick on every container. proxy publishes 80 and 443 because the world must reach it; web and db publish nothing, because only sibling containers on driftwood-net need them. Every published port is a door, and the fewer doors the better.
The -p hostPort:containerPort Syntax
-p 80:80 maps host port 80 to the container's port 80 by way of a DNAT rule. The left side is the host port a client connects to; the right side is the port the process is listening on inside the container. They need not match — -p 8080:80 lets a client connect to host port 8080 while the container's server listens on 80, which is how you run several containers' port-80 services on one host by mapping each to a different host port.
Publish vs Expose
EXPOSE in a Dockerfile — and the --expose flag — is documentation only. It records which ports the image listens on and changes nothing about reachability. Only -p/--publish (or Compose's ports:) actually opens a host port. Conflating the two is the single most common port complaint: "I exposed 8000 in the Dockerfile but I can't reach it." EXPOSE never published anything; without a matching -p there is no host port at all.
The Bind Address: 0.0.0.0 vs 127.0.0.1
-p 80:80 binds to 0.0.0.0 by default — every host interface — so the service is reachable from anywhere that can route to the host. On a cloud VM with a public IP, that means the public internet, immediately, the moment the container starts. Prefixing the host with a loopback address — -p 127.0.0.1:5432:5432 — binds only to the host's loopback, so the port is reachable from the host itself but not from the network. The difference between those two forms is the difference between a local-only database and one exposed to the entire internet.
# proxy: the single public entry point — publish 80 and 443 on all interfaces docker run -d --name proxy --network driftwood-net -p 80:80 -p 443:443 nginx # web and db: reached only over driftwood-net by siblings — publish nothing docker run -d --name web --network driftwood-net driftwood:latest docker run -d --name db --network driftwood-net postgres:16 # a host-only admin port, never exposed to the network docker run -d --name adminer --network driftwood-net -p 127.0.0.1:8081:8080 adminer
The stack's exposure is one container wide. proxy takes 80 and 443 on 0.0.0.0; web and db take no host ports and are reachable only by name over driftwood-net; the admin tool binds 127.0.0.1 so it answers the host's own browser and nothing on the LAN.
Why Publish Only proxy
proxy is the single entry point, so it is the only container that publishes anything — 80 and 443. web (gunicorn on :8000) and db (postgres on :5432) are reached only over driftwood-net by their siblings, by name, and publish nothing to the host. The result is that the application's entire externally reachable attack surface is the reverse proxy. Anything trying to reach the database has to first get through proxy and then through web — there is no direct door.
Host Port Conflicts and Ranges
Two containers cannot publish the same host port at once — the second fails to bind, because a host port is a single resource. When you do not care which host port is used, -p 80 with no host port lets the daemon assign a random high one, and docker port web shows what it chose. That is handy for ephemeral test containers where you just need a port, not a specific one, and want to avoid collisions across many short-lived runs.
- Publishing the database with
-p 5432:5432on a cloud host — that binds PostgreSQL to0.0.0.0and exposes it to the internet; anyone scanning the host's IP can reach it.dbshould publish nothing and be reached only overdriftwood-net. - Believing
EXPOSE 8000in the Dockerfile makes the port reachable from the host — it is documentation; without a matching-pnothing is published and the connection is refused. - Binding to
0.0.0.0when only the host needs access — a port meant for a local tool or a host-side process should bind127.0.0.1:so it is never reachable from the network. - Getting the two sides of
-pbackwards —-p 8080:80means connect to host 8080 and the container listens on 80; reversing it forwards host 80 to a container port nothing is listening on, and the connection refuses.
- Publish only the ports the outside world genuinely needs — for Driftwood that is
proxy's 80 and 443, and nothing else — so the attack surface is one container. - Bind host-only services to
127.0.0.1:explicitly so a development database or admin endpoint is never reachable from the network or internet. - Reach internal services container-to-container over a user-defined network by name and port, never by publishing them to the host.
- Use
EXPOSEin Dockerfiles as documentation of listening ports, and-por Composeports:as the actual, reviewed exposure decision.
-p semantics; rootless, it publishes via a userspace proxy (slirp4netns/pasta) rather than host iptables
Kubernetes replaces ad-hoc publishing with Service types (NodePort, LoadBalancer) and Ingress
iptables DNAT · docker-proxy the rule plus userspace helper that publishing installs under the hood
Knowledge Check
In -p 8080:80, what does each side mean?
- The host port a client connects to is 8080; the container's process listens on 80
- The container's process listens on 8080 and the host forwards inbound traffic from port 80
- 8080 is the HTTP protocol version and 80 is the actual TCP port number
- Both numbers must be identical or Docker rejects the mapping at start
Why can you reach a port you "exposed" in the Dockerfile only after also publishing it?
- Because
EXPOSEis documentation only — only-pactually opens a host port - Because
EXPOSEopens the port but only for TCP, and you needed UDP - Because
EXPOSEbinds the port to127.0.0.1and-pmoves it to0.0.0.0 - Because
EXPOSEopens the port but a separate firewall rule blocks it until-p
What is the security difference between -p 5432:5432 and -p 127.0.0.1:5432:5432 on a cloud VM?
- The first binds all interfaces and exposes the port to the internet; the second binds loopback only
- The first forwards traffic to the container while the second instead forwards to the host's own local PostgreSQL
- The second form encrypts the connection with TLS while the first sends every byte in plaintext
- The first opens a single port and the second opens a whole contiguous range of ports
Why does only proxy publish ports in the Driftwood stack?
- It is the single entry point, so publishing only it keeps the attack surface to one container
webanddbare technically unable to publish any of their ports to the host- Without publishing their own ports,
webanddbcould not talk to each other or to proxy at all - Docker permits only a single container per host to publish ports to the outside
You got correct