Port Publishing and Binding
By default, containers are isolated -- nothing from the outside can reach them. Port publishing maps a port on the host to a port inside the container, creating an entry point for traffic.
How Port Publishing Works
flowchart LR
subgraph Internet
Client["Client"]
end
subgraph Host["Docker Host"]
HP["Host Port 8080"]
subgraph Container["Container"]
CP["App on Port 80"]
end
end
Client -->|"Request to :8080"| HP
HP -->|"-p 8080:80"| CP
style HP fill:#fff3e0,stroke:#ef6c00
style CP fill:#e3f2fd,stroke:#1565c0
Publish Syntax
The general form is:
-p [host_ip:]host_port:container_port[/protocol]
| Syntax | What It Does | Accessible From |
|---|---|---|
-p 8080:80 | Map host 8080 → container 80 | All network interfaces (0.0.0.0) |
-p 127.0.0.1:8080:80 | Map host 8080 → container 80, localhost only | Host machine only |
-p 0.0.0.0:443:8443/tcp | Explicit all-interfaces binding with protocol | All interfaces, TCP only |
-P | Map all EXPOSEd ports to random high ports | All interfaces |
The Security Default
When you write -p 8080:80 without specifying an IP, Docker binds to all interfaces (0.0.0.0). This means the port is accessible from the network, not just locally.
For services that should only be reached from the host itself (admin panels, debugging tools), always bind to 127.0.0.1:
docker run -d -p 127.0.0.1:9000:9000 admin-tool
Common Patterns
Web Server (Public)
docker run -d \
--name web \
-p 80:80 \
-p 443:443 \
nginx:alpine
Database (Internal Only -- No Publishing)
# No -p flag. Only reachable from other containers on the same network.
docker run -d \
--name db \
--network app-net \
postgres:16
Admin Tool (Localhost Only)
docker run -d \
--name pgadmin \
-p 127.0.0.1:5050:80 \
dpage/pgadmin4
Compose Port Mapping
services:
web:
image: nginx:alpine
ports:
- "80:80" # Public
- "443:443" # Public
api:
image: my-api:1.0.0
# No ports -- internal only, reached via Docker DNS
admin:
image: admin-ui:1.0.0
ports:
- "127.0.0.1:9000:9000" # Localhost only
EXPOSE vs -p (Publish)
| Concept | What It Does |
|---|---|
EXPOSE 80 in Dockerfile | Documentation only -- declares what port the app listens on. Does not publish anything |
-p 8080:80 at runtime | Actually creates the port mapping on the host |
EXPOSE is a hint. Only -p (or ports: in Compose) opens the door.
Verifying Published Ports
# See all containers and their port mappings
docker ps
# See ports for a specific container
docker port my-container
# Detailed port info
docker inspect -f '{{json .NetworkSettings.Ports}}' my-container
# Check what's actually listening on the host
ss -tulpen | grep 8080
Troubleshooting Port Conflicts
If a container fails to start with "port already in use":
# Find what's using the port on the host
ss -tulpen | grep :8080
# Check if another container has it
docker ps --format '{{.Names}}\t{{.Ports}}' | grep 8080
Solutions: stop the conflicting service, change the host port, or remove the old container.
The Reverse Proxy Pattern
In production, the most common setup publishes only the reverse proxy (ports 80 and 443) and keeps all other services internal:
flowchart LR
Internet -->|"443"| RP["Reverse Proxy<br/>(published)"]
RP -->|"Docker DNS"| API["API<br/>(internal)"]
RP -->|"Docker DNS"| Web["Web App<br/>(internal)"]
API -->|"Docker DNS"| DB["Database<br/>(internal)"]
style RP fill:#fff3e0,stroke:#ef6c00
style DB fill:#e8f5e9,stroke:#2e7d32
This minimizes the attack surface -- only one service is exposed to the internet.
Key Takeaways
- Use
-p host:containerto publish ports. Without-p, the container is internal only. - Bind to
127.0.0.1for admin tools and services that should not be network-accessible. EXPOSEin a Dockerfile is documentation only -- it does not publish ports.- In production, publish only the reverse proxy and keep everything else on internal Docker networks.
- Always verify published ports with
docker portorss -tulpenafter deploying.
What's Next
- Continue to DNS and Service Discovery to learn how containers find each other by name.