Docker 18 🐳 Exposing Ports and Network Metadata: EXPOSE vs Host Port Mapping (-p)
Docker containers are isolated by default. A process inside a container can make outbound connections, but the outside world cannot reach it unless something explicitly exposes the container’s port. The EXPOSE instruction and the -p flag are the two mechanisms involved, but they serve fundamentally different purposes. EXPOSE is documentation: it declares which ports the application inside the image intends to use. The -p flag is the action: it publishes a container port to the host, creating the iptables rules that forward traffic from the host to the container. Understanding the distinction prevents the common mistake of assuming that EXPOSE alone makes a service reachable.
The distinction is reinforced by the Docker documentation: EXPOSE “informs Docker that the container listens on the specified network ports at runtime” but does not publish them . The Microsoft training material states it more directly: “The EXPOSE instruction doesn’t publish the port to the host machine. To make the application accessible externally, use docker run -p <host_port>:<container_port>” . The port becomes reachable only when the -p or -P flag creates the forwarding rules.
This chapter covers the EXPOSE instruction, the -p flag and its syntax, the -P flag for automatic mapping, the iptables rules that implement port publishing, the Docker networking metadata that docker port and docker inspect expose, and the patterns for publishing ports securely.
Key point: EXPOSE declares the port the application listens on; it does not publish it. -p host:container publishes a specific port mapping. -P publishes every exposed port to a random host port. The publishing is implemented through iptables DNAT rules that rewrite the destination address from the host port to the container’s IP and port. Use docker port to inspect the actual mappings. Bind to 127.0.0.1 with -p to restrict access to the host only.
Why EXPOSE and port publishing are separate
The documentation problem. A Dockerfile author knows which port the application listens on. EXPOSE records that knowledge in the image metadata, so operators who run the image know which port to publish. Without EXPOSE, the operator would have to read the application’s documentation or source code to find the port .
The security problem. Publishing a port makes it reachable from outside the container. The default behavior binds to 0.0.0.0, which means every network interface on the host. This is insecure by default . If EXPOSE published automatically, running any image would expose its services without the operator’s explicit consent. The separation ensures that publishing is a deliberate runtime decision, not a build-time accident.
The flexibility problem. The same image can be run with different port mappings depending on the environment. A development container might publish port 8080 to the host, while a production container might not publish it at all because a reverse proxy handles the traffic. EXPOSE is fixed in the image; the -p mapping is chosen at runtime.
The metadata problem. The EXPOSE instruction adds metadata to the image, and docker inspect shows the exposed ports. Tools like docker run -P use this metadata to create automatic mappings. Without EXPOSE, -P has nothing to publish .
The networking problem. Publishing a port creates iptables rules that forward traffic from the host to the container. The rules are specific to the host’s networking configuration and the container’s IP address. They cannot be baked into the image because the container’s IP is assigned at runtime.
a. The EXPOSE instruction
EXPOSE declares that the container listens on a specific network port. It is documentation and metadata; it does not publish the port.
EXPOSE 80
EXPOSE 443
EXPOSE 8080
The instruction accepts a port number and an optional protocol (tcp or udp). The default protocol is TCP.
EXPOSE 80/tcp
EXPOSE 53/udp
Multiple ports can be exposed with multiple EXPOSE lines or with a range:
EXPOSE 8000-8005
The exposed ports are visible in the image metadata:
docker inspect --format '{{.Config.ExposedPorts}}' my-image
# map[80/tcp:{} 443/tcp:{}]
They are also visible in the docker history output, which shows the EXPOSE instruction as a layer with zero bytes of filesystem change.
The EXPOSE instruction does not create any iptables rules, does not bind any host port, and does not make the service reachable. It is a declaration of intent .
The Docker documentation recommends using the conventional port for the application: EXPOSE 80 for a web server, EXPOSE 27017 for MongoDB, and so on .
b. The -p flag: publishing a specific port
The -p flag (or --publish) publishes a container port to a host port. It creates the iptables rules that forward traffic.
docker run -p 8080:80 nginx
The syntax is -p <host_port>:<container_port>. The host port is the port on the host machine, and the container port is the port inside the container. Traffic to host:8080 is forwarded to the container’s port 80.
The -p flag can bind to a specific host IP address:
docker run -p 127.0.0.1:8080:80 nginx
This restricts the published port to the loopback interface. Only processes on the host can reach it; external clients cannot .
The host port can be omitted, in which case Docker chooses a random port from the ephemeral range:
docker run -p 80 nginx
Docker assigns a port from the range configured by /proc/sys/net/ipv4/ip_local_port_range, typically 32768 to 61000 . The docker port command shows the actual mapping.
The -p flag can also specify the protocol:
docker run -p 8080:80/tcp -p 8080:80/udp nginx
A single container can have multiple -p flags, each publishing a different port.
c. The -P flag: publishing all exposed ports
The -P flag (or --publish-all) publishes every port declared with EXPOSE to a random host port.
EXPOSE 1000
EXPOSE 2000
EXPOSE 3000
docker run -P my-image
Docker creates a mapping for each exposed port. The docker port command shows the results:
1000/tcp -> 0.0.0.0:49156
2000/tcp -> 0.0.0.0:49157
3000/tcp -> 0.0.0.0:49158
The -P flag is the blanket operation: it uses the EXPOSE metadata to determine which ports to publish, and it chooses the host ports automatically .
The -P flag is useful when the host ports do not matter, such as in development or testing. It is not recommended for production because the random ports change on every run and are hard to document .
d. How port publishing works
When you run docker run -p 8080:80, Docker creates iptables rules in the NAT table. The key rule is a DNAT (Destination Network Address Translation) rule:
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
This rule matches packets destined for port 8080 on any host interface and rewrites the destination to the container’s IP address (e.g., 172.17.0.2) and port 80 .
The rules are placed in the DOCKER chain, which is invoked from the PREROUTING and OUTPUT chains:
PREROUTINGhandles packets arriving from external clients.OUTPUThandles packets generated by local processes on the host.
Both chains invoke the DOCKER chain for local destinations, so the same DNAT rule works for both paths .
The POSTROUTING chain contains a MASQUERADE rule for the container network, which rewrites the source address of outgoing container traffic:
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
This allows containers to make outbound connections without the outside world knowing their internal IP addresses .
The FORWARD chain must allow the forwarded packets. Docker adds a rule to accept traffic destined for the container’s port .
When userland-proxy is enabled (the default), a docker-proxy process also binds the host port. It handles hairpin NAT (a container connecting to its own published port through the host) and some IPv6 scenarios. When userland-proxy is disabled, only iptables rules are used .
e. Inspecting port metadata
The docker port command shows the actual mappings for a running container:
docker port my-container
# 80/tcp -> 0.0.0.0:8080
# 443/tcp -> 0.0.0.0:8443
The docker ps command shows the port mappings in the PORTS column:
docker ps
# CONTAINER ID IMAGE ... PORTS
# abc123 nginx ... 0.0.0.0:8080->80/tcp
The docker inspect command shows the full networking metadata:
docker inspect --format '{{json .NetworkSettings.Ports}}' my-container
The output is a JSON object mapping container ports to host bindings:
{
"80/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "8080"
}
]
}
The ExposedPorts field in the image metadata shows the ports declared with EXPOSE:
docker inspect --format '{{json .Config.ExposedPorts}}' my-image
# {"80/tcp":{}}
The docker port command reads the NetworkSettings.Ports field and formats it for human reading. The docker ps command reads the same field for the PORTS column.
f. Security considerations
Publishing a port makes the service reachable from outside the container. The default binding is 0.0.0.0, which means every network interface on the host, including the public interface .
To restrict access to the host only, bind to the loopback interface:
docker run -p 127.0.0.1:8080:80 nginx
This creates the DNAT rule only for packets destined to 127.0.0.1, so external clients cannot reach the service .
A host firewall can also block forwarded packets. The FORWARD chain in the filter table must allow the traffic. If the host’s firewall policy is DROP by default, Docker’s rules may not be sufficient .
Port conflicts are another consideration. If another process on the host is already listening on the host port, the container may fail to start or the mapping may not be created. The docker ps output shows the mapping, and ss -tlnp on the host shows which process owns the port .
The application inside the container must listen on an address that can receive the forwarded packet. If it binds to 127.0.0.1 inside its own network namespace, the DNAT rule delivers the packet to the container’s eth0, but the listener is on loopback only, so the connection is refused. The application must bind to 0.0.0.0 or the container’s IP address .
Complete Example Session
# ============================================
# PART 1: EXPOSE IN A DOCKERFILE
# ============================================
FROM nginx:1.25-alpine
EXPOSE 80
# ============================================
# PART 2: EXPOSE MULTIPLE PORTS
# ============================================
FROM node:20-alpine
EXPOSE 3000
EXPOSE 9229
# ============================================
# PART 3: PUBLISH A SPECIFIC PORT
# ============================================
docker run -d -p 8080:80 nginx
docker ps
# PORTS: 0.0.0.0:8080->80/tcp
# ============================================
# PART 4: PUBLISH TO A RANDOM HOST PORT
# ============================================
docker run -d -p 80 nginx
docker port <container>
# 80/tcp -> 0.0.0.0:49153
# ============================================
# PART 5: PUBLISH ALL EXPOSED PORTS
# ============================================
docker run -d -P my-image
docker port <container>
# 3000/tcp -> 0.0.0.0:49156
# 9229/tcp -> 0.0.0.0:49157
# ============================================
# PART 6: BIND TO LOOPBACK ONLY
# ============================================
docker run -d -p 127.0.0.1:8080:80 nginx
# ============================================
# PART 7: INSPECT THE PORT MAPPING
# ============================================
docker inspect --format '{{json .NetworkSettings.Ports}}' <container>
# ============================================
# PART 8: VIEW THE EXPOSED PORTS OF AN IMAGE
# ============================================
docker inspect --format '{{json .Config.ExposedPorts}}' nginx:1.25-alpine
# ============================================
# PART 9: VIEW THE IPTABLES RULES
# ============================================
sudo iptables -t nat -L DOCKER -n
# DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
# ============================================
# PART 10: CHECK THE APPLICATION LISTENER
# ============================================
docker exec <container> ss -tlnp
# LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
These ten parts cover EXPOSE in a Dockerfile, multiple exposed ports, publishing a specific port, publishing to a random port, publishing all exposed ports, binding to loopback, inspecting the mapping, viewing image metadata, viewing the iptables rules, and checking the application listener.
Quick Reference
EXPOSE vs -p
| Aspect | EXPOSE | -p |
|---|---|---|
| Type | Dockerfile instruction | Runtime flag |
| Purpose | Document the port | Publish the port |
| Creates iptables rules | No | Yes |
| Makes port reachable | No | Yes |
| Visible in image metadata | Yes | No |
| Overridable at runtime | N/A | Yes |
Port Mapping Syntax
| Syntax | Meaning |
|---|---|
-p 8080:80 | Host port 8080 to container port 80 |
-p 127.0.0.1:8080:80 | Bind to loopback only |
-p 80 | Random host port to container port 80 |
-p 8080:80/tcp | TCP protocol |
-p 8080:80/udp | UDP protocol |
-P | Publish all exposed ports |
Inspection Commands
| Command | Purpose |
|---|---|
docker port <container> | Show port mappings |
docker ps | Show ports column |
docker inspect --format '{{json .NetworkSettings.Ports}}' | Full mapping |
docker inspect --format '{{json .Config.ExposedPorts}}' | Image exposed ports |
Iptables Rules
| Chain | Purpose |
|---|---|
| PREROUTING | DNAT for external traffic |
| OUTPUT | DNAT for local traffic |
| POSTROUTING | MASQUERADE for outbound |
| FORWARD | Allow forwarded packets |
Security
| Approach | Effect |
|---|---|
Default -p 8080:80 | Binds to 0.0.0.0 |
-p 127.0.0.1:8080:80 | Binds to loopback only |
No -p | Port not reachable externally |
Best Practices
✅ Do This:
# Document the port with EXPOSE
EXPOSE 8080
# Publish explicitly at runtime
docker run -p 8080:8080 myapp
# Restrict to loopback when external access is not needed
docker run -p 127.0.0.1:8080:8080 myapp
# Inspect the mapping
docker port my-container
❌ Don’t Do This:
# Assume EXPOSE makes the port reachable
EXPOSE 80 # ❌ no publishing happens
# Bind to 0.0.0.0 when only the host needs access
docker run -p 8080:80 myapp # ❌ external access
# Forget to check the application bind address
# App binds to 127.0.0.1 inside the container # ❌ connection refused
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Port not reachable | Only EXPOSE, no -p | Add -p at runtime |
| Connection refused | App binds to 127.0.0.1 in container | Bind to 0.0.0.0 |
| Port conflict | Another process owns the host port | Change the host port |
| Firewall blocks traffic | Host FORWARD policy is DROP | Allow the forwarded packets |
| Random port changes | Used -P or -p 80 | Use explicit -p |
| External access unexpected | Default binds to 0.0.0.0 | Bind to 127.0.0.1 |
Real-World Examples
1. Publish a Web Server
docker run -d -p 80:80 nginx
2. Publish to a Random Port
docker run -d -p 80 nginx
3. Bind to Loopback
docker run -d -p 127.0.0.1:8080:80 nginx
4. Publish All Exposed Ports
docker run -d -P my-image
5. Inspect the Mapping
docker port my-container
6. View Image Exposed Ports
docker inspect --format '{{json .Config.ExposedPorts}}' my-image
7. View Container Port Bindings
docker inspect --format '{{json .NetworkSettings.Ports}}' my-container
8. Check Iptables Rules
sudo iptables -t nat -L DOCKER -n
9. Check Application Listener
docker exec my-container ss -tlnp
10. Multiple Port Mappings
docker run -d -p 8080:80 -p 8443:443 nginx
Visual
EXPOSE vs -p
┌──────────────────────────────────────────────────────────────┐
│ DOCKERFILE: RUNTIME: │
│ EXPOSE 80 docker run -p 8080:80 │
│ └── metadata only └── creates iptables rules │
│ │
│ Image metadata: Host port: │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 80/tcp: {} │ │ 8080 → 80/tcp │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ No iptables rules DNAT rule created │
│ Not reachable Reachable │
└──────────────────────────────────────────────────────────────┘
Iptables DNAT
┌──────────────────────────────────────────────────────────────┐
│ Client → host:8080 │
│ │ │
│ ▼ │
│ PREROUTING chain │
│ └── DNAT: rewrite destination to 172.17.0.2:80 │
│ │ │
│ ▼ │
│ FORWARD chain │
│ └── ACCEPT packet destined for 172.17.0.2:80 │
│ │ │
│ ▼ │
│ Container eth0 → application listener on port 80 │
└──────────────────────────────────────────────────────────────┘
Port Mapping Syntax
┌──────────────────────────────────────────────────────────────┐
│ -p 8080:80 host 8080 → container 80 │
│ -p 127.0.0.1:8080:80 host loopback 8080 → container 80 │
│ -p 80 random host port → container 80 │
│ -P all EXPOSEd ports → random host ports │
└──────────────────────────────────────────────────────────────┘
Security Default
┌──────────────────────────────────────────────────────────────┐
│ DEFAULT -p 8080:80 │
│ └── binds to 0.0.0.0:8080 │
│ reachable from any network interface │
│ │
│ RESTRICTED -p 127.0.0.1:8080:80 │
│ └── binds to 127.0.0.1:8080 │
│ reachable only from the host │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
EXPOSE | Documents the port in image metadata |
-p host:container | Publishes a specific mapping |
-p 127.0.0.1:host:container | Binds to loopback only |
-p container | Random host port |
-P | Publishes all exposed ports |
| Default binding | 0.0.0.0 (all interfaces) |
| Iptables chain | DNAT in the DOCKER chain |
docker port | Shows the actual mappings |
docker inspect | Shows image and container port metadata |
| App bind address | Must be 0.0.0.0 to receive forwarded packets |
Key takeaways:
EXPOSEdoes not publish the port. It declares the port in the image metadata for documentation and for-Pto use. The port becomes reachable only when the-pflag creates the forwarding rules .-p host:containerpublishes a specific mapping. The host port is on the host machine, and the container port is inside the container. Traffic to the host port is forwarded to the container port .- The default binding is
0.0.0.0. This makes the port reachable from every network interface. To restrict access to the host only, bind to127.0.0.1. -Ppublishes all exposed ports. It uses theEXPOSEmetadata to determine which ports to publish and chooses random host ports .- Docker uses iptables DNAT rules. The rules rewrite the destination address from the host port to the container’s IP and port. The rules are in the
DOCKERchain, invoked fromPREROUTINGandOUTPUT. - The application must listen on
0.0.0.0. If it binds to127.0.0.1inside the container, the forwarded packets are refused because the listener is not on the container’seth0. docker portanddocker inspectshow the metadata. Thedocker portcommand formats the mappings for humans;docker inspectshows the full JSON for automation.
Remember: EXPOSE and -p are two halves of the same idea. EXPOSE is the declaration: this port is what the application uses. -p is the action: forward traffic from the host to the container. Neither is sufficient alone, and confusing them leads to services that seem configured but are unreachable. The publishing is implemented through iptables DNAT rules that rewrite the destination address. The default binding is 0.0.0.0, which is insecure by default. Bind to 127.0.0.1 when only the host needs access. And always verify that the application inside the container is listening on 0.0.0.0, not 127.0.0.1, or the forwarded traffic will be refused.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!