| |

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:

  • PREROUTING handles packets arriving from external clients.
  • OUTPUT handles 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

AspectEXPOSE-p
TypeDockerfile instructionRuntime flag
PurposeDocument the portPublish the port
Creates iptables rulesNoYes
Makes port reachableNoYes
Visible in image metadataYesNo
Overridable at runtimeN/AYes

Port Mapping Syntax

SyntaxMeaning
-p 8080:80Host port 8080 to container port 80
-p 127.0.0.1:8080:80Bind to loopback only
-p 80Random host port to container port 80
-p 8080:80/tcpTCP protocol
-p 8080:80/udpUDP protocol
-PPublish all exposed ports

Inspection Commands

CommandPurpose
docker port <container>Show port mappings
docker psShow ports column
docker inspect --format '{{json .NetworkSettings.Ports}}'Full mapping
docker inspect --format '{{json .Config.ExposedPorts}}'Image exposed ports

Iptables Rules

ChainPurpose
PREROUTINGDNAT for external traffic
OUTPUTDNAT for local traffic
POSTROUTINGMASQUERADE for outbound
FORWARDAllow forwarded packets

Security

ApproachEffect
Default -p 8080:80Binds to 0.0.0.0
-p 127.0.0.1:8080:80Binds to loopback only
No -pPort 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

PitfallWhy It HappensFix
Port not reachableOnly EXPOSE, no -pAdd -p at runtime
Connection refusedApp binds to 127.0.0.1 in containerBind to 0.0.0.0
Port conflictAnother process owns the host portChange the host port
Firewall blocks trafficHost FORWARD policy is DROPAllow the forwarded packets
Random port changesUsed -P or -p 80Use explicit -p
External access unexpectedDefault binds to 0.0.0.0Bind 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

ItemValue
EXPOSEDocuments the port in image metadata
-p host:containerPublishes a specific mapping
-p 127.0.0.1:host:containerBinds to loopback only
-p containerRandom host port
-PPublishes all exposed ports
Default binding0.0.0.0 (all interfaces)
Iptables chainDNAT in the DOCKER chain
docker portShows the actual mappings
docker inspectShows image and container port metadata
App bind addressMust be 0.0.0.0 to receive forwarded packets

Key takeaways:

  • EXPOSE does not publish the port. It declares the port in the image metadata for documentation and for -P to use. The port becomes reachable only when the -p flag creates the forwarding rules .
  • -p host:container publishes 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 to 127.0.0.1 .
  • -P publishes all exposed ports. It uses the EXPOSE metadata 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 DOCKER chain, invoked from PREROUTING and OUTPUT .
  • The application must listen on 0.0.0.0. If it binds to 127.0.0.1 inside the container, the forwarded packets are refused because the listener is not on the container’s eth0 .
  • docker port and docker inspect show the metadata. The docker port command formats the mappings for humans; docker inspect shows 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!