Docker Lesson 20 – Port Mapping | Dataplexa
Section II · Lesson 20

Port Mapping

Every container lives in its own isolated network namespace. By default, nothing on the outside can reach it — not your browser, not other machines, not your load balancer. Port mapping is the deliberate act of opening a door between the container's private network and the outside world.

You've used the -p flag in almost every lesson so far. This lesson explains exactly what it does at the network level, the patterns you'll use in production, the performance implications of different binding strategies, and the mistakes that cause silent port conflicts.

Port Mapping Is NAT

Under the hood, -p creates a NAT rule in the host's iptables. When a packet arrives on the host's port 8080, the kernel rewrites its destination to the container's IP and port 80, and forwards it into the container's network namespace. The reply packet gets its source address rewritten back to the host's IP before leaving. From the client's perspective, it's talking to the host. The container is invisible.

The Hotel Receptionist Analogy

Port mapping is like a hotel receptionist. A visitor arrives at the hotel and asks for room 302 — but they don't know which floor or corridor to take. The receptionist intercepts the visitor at the front desk (the host's port) and escorts them directly to the right room (the container's port). The visitor never navigates the hotel themselves — they just hand off at the front desk. The container is a private room. The host port is the front desk. The NAT rule is the receptionist.

Port mapping — how traffic flows

Browser
localhost:8080
Host iptables
NAT: 8080 → 172.17.0.2:80
nginx container
172.17.0.2:80

The client connects to the host on port 8080. iptables rewrites the destination to the container's internal IP and port 80. The container never exposes itself directly — the host acts as the proxy.

The -p Flag — All the Formats

The -p flag accepts several formats, each giving you different levels of control over how the port is exposed.

# Format 1 — host_port:container_port (most common)
docker run -d -p 8080:80 nginx:alpine
# Traffic hitting host port 8080 → forwarded to container port 80
# Binds to 0.0.0.0 — accessible from ALL network interfaces on the host

# Format 2 — ip:host_port:container_port (bind to a specific interface)
docker run -d -p 127.0.0.1:8080:80 nginx:alpine
# Only accessible from localhost — not from other machines on the network
# Useful for development services that should never be exposed externally

# Format 3 — container_port only (random host port)
docker run -d -p 80 nginx:alpine
# Docker picks a random available port on the host — use docker ps to find it
# Useful when running many instances and you don't care which host port they get

# Format 4 — multiple ports in one command
docker run -d \
  -p 80:80 \
  -p 443:443 \
  --name nginx-proxy \
  nginx:alpine
# Use multiple -p flags for applications that need more than one port exposed
# docker ps output after each run:

# Format 1 — bound to all interfaces
CONTAINER ID   IMAGE         PORTS                  NAMES
a3f2c8d91e44   nginx:alpine  0.0.0.0:8080->80/tcp   happy_hopper

# Format 2 — bound to localhost only
CONTAINER ID   IMAGE         PORTS                           NAMES
b7e1a4c52f88   nginx:alpine  127.0.0.1:8080->80/tcp          reverent_curie

# Format 3 — random host port assigned
CONTAINER ID   IMAGE         PORTS                    NAMES
c9d4e8f2a1b3   nginx:alpine  0.0.0.0:49153->80/tcp    kind_einstein

# Format 4 — multiple ports
CONTAINER ID   IMAGE         PORTS                                        NAMES
d2e3f4a5b6c7   nginx:alpine  0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp   nginx-proxy

What just happened?

The PORTS column in docker ps is the clearest summary of what's mapped. 0.0.0.0:8080->80/tcp means "any IP on the host, port 8080, forwarded to container port 80 over TCP." The 0.0.0.0 is the binding address — it means all interfaces, all IPs. 127.0.0.1:8080->80/tcp means only the loopback interface — no external traffic can reach it. Format 3's random port (49153) is assigned from the ephemeral port range — use docker port container-name to find it without parsing docker ps. Format 4 shows two ports mapped side by side, separated by a comma.

Binding to All Interfaces vs a Specific IP

The scenario: You're a DevOps engineer on a production server with two network interfaces — a public interface (eth0, IP 203.0.113.10) and a private interface (eth1, IP 10.0.0.5). You have an admin panel that should only be accessible from the private network — never from the public internet.

# Public-facing API — bind to all interfaces (accessible from anywhere)
docker run -d \
  --name order-api \
  -p 0.0.0.0:3000:3000 \
  order-api:v1.0.0
# 0.0.0.0 → bind to all network interfaces
# The API is reachable from the public internet on port 3000

# Admin panel — bind to private interface ONLY
docker run -d \
  --name admin-panel \
  -p 10.0.0.5:8080:8080 \
  admin-panel:v1.0.0
# 10.0.0.5 → bind ONLY to the private network interface
# Only machines on the 10.0.0.0/8 private network can reach this
# The public internet cannot touch it — even if they know the port

# Verify the difference with docker ps
docker ps --format "table {{.Names}}\t{{.Ports}}"
NAMES           PORTS
order-api       0.0.0.0:3000->3000/tcp
admin-panel     10.0.0.5:8080->8080/tcp

What just happened?

The PORTS column tells the whole story. order-api binds to 0.0.0.0 — any request arriving at port 3000 on any interface reaches it. admin-panel binds only to 10.0.0.5 — the iptables rule only accepts packets arriving on that specific interface's IP. A request from the public internet targeting 203.0.113.10:8080 hits port 8080 on the public interface, but the NAT rule doesn't exist for that IP — the packet is dropped. This is a lightweight but effective way to restrict service accessibility by network interface without a firewall.

Finding and Inspecting Port Mappings

docker port nginx-proxy
# Shows all port mappings for a specific container — cleaner than docker ps for this

docker ps --format "table {{.Names}}\t{{.Ports}}"
# Custom format output — shows only container name and port columns
# Much more readable than the full docker ps table on containers with many ports

docker inspect nginx-proxy --format '{{json .NetworkSettings.Ports}}'
# JSON output of all port bindings — useful for scripting and automation
# docker port nginx-proxy
80/tcp -> 0.0.0.0:80
443/tcp -> 0.0.0.0:443

# docker ps --format output
NAMES           PORTS
nginx-proxy     0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
order-api       0.0.0.0:3000->3000/tcp
admin-panel     10.0.0.5:8080->8080/tcp

# docker inspect --format output
{"80/tcp":[{"HostIp":"0.0.0.0","HostPort":"80"}],"443/tcp":[{"HostIp":"0.0.0.0","HostPort":"443"}]}

What just happened?

docker port is the cleanest way to check a single container's mappings — it prints one line per port in a human-readable format. The custom --format table on docker ps is useful when you have many containers and only care about names and ports — it strips all the noise. The docker inspect JSON output is the format automation scripts use — every CI/CD system and health check tool that needs to discover container ports programmatically queries this field.

Port Conflict — The Silent Failure

If you try to map a host port that's already in use — either by another container or by a process on the host — Docker refuses to start the container with a "port is already allocated" error. This is easy to hit when restarting containers during development. Before starting a new container, run docker ps to check what's already mapped. Or use random host ports (Format 3) to let Docker pick a free port automatically.

Teacher's Note

In production, never bind to 0.0.0.0 for services that shouldn't be publicly accessible — bind to the specific private interface IP instead. It's a one-word change in the -p flag that stops a lot of accidental exposure.

Practice Questions

1. Under the hood, Docker implements port mapping by creating NAT rules in which Linux kernel feature?



2. To see all port mappings for a specific container in a clean human-readable format — without parsing the full docker ps output — which command do you use?



3. To bind a container port so it is only accessible from the local machine and not from any external network, you specify which IP address in the -p flag?



Quiz

1. A container is started with -p 0.0.0.0:3000:3000. What does the 0.0.0.0 binding address mean?


2. A developer tries to start a container with -p 80:80 but another container is already using host port 80. What happens?


3. A production server has a public IP (203.0.113.10) and a private IP (192.168.1.10). An admin service should only be accessible from the private network. The correct -p configuration is:


Up Next · Lesson 21

Environment Variables

Ports get traffic into your container — environment variables configure what your app does with it. The right way to pass secrets, connection strings, and config without baking them into your image.