Port Mapping
Docker isolates each container in its own network namespace. To expose a service to the host (or beyond), you publish ports with -p. Knowing the difference between EXPOSE (documentation), -p (published), and Compose port mappings keeps your services reachable without leaking everything to the network.
Publish, bind, EXPOSE, networks, compose
EXAMPLE
# 1) The simplest case
docker run -p 8080:80 nginx
# Format: -p HOST_PORT:CONTAINER_PORT
# Visit http://localhost:8080 → reaches nginx on container port 80.
# 2) Bind to a specific host interface
docker run -p 127.0.0.1:8080:80 nginx # localhost only — invisible from network
docker run -p 192.168.1.10:8080:80 nginx # bind to a single IP
docker run -p 0.0.0.0:8080:80 nginx # default — ALL interfaces
# Use 127.0.0.1 binding by default for dev to avoid accidentally exposing services to the LAN.
# 3) Random host port
docker run -P nginx # publish all EXPOSEd ports on random host ports
docker port <container> # show the mapping
# 4) Multiple ports
docker run -p 8080:80 -p 8443:443 nginx
docker run -p 8080:80/tcp -p 5353:53/udp myapp # explicit protocol
# 5) Port range
docker run -p 5000-5005:5000-5005 myapp # 6 ports
# 6) EXPOSE in Dockerfile — DOCUMENTATION ONLY
FROM node:20-alpine
EXPOSE 3000
CMD ['node', 'server.js']
# EXPOSE doesn't publish anything to the host. It signals to humans + tools (-P) what the container LISTENS on.
# You still need -p at runtime to make it reachable from outside.
# 7) Compose port mappings
services:
web:
image: nginx
ports:
- '8080:80' # host:container
- '127.0.0.1:8443:443' # bind to host loopback
- '5000-5005:5000-5005' # range
- '8090:80/tcp'
- '5353:53/udp'
api:
build: ./api
expose: ['3000'] # not published; reachable from other services in same network
# 8) expose: in compose vs ports:
# • ports — publishes to host network
# • expose — only makes the port reachable INSIDE the compose network (between services)
# Use expose for backend services that only the front-end calls; ports for things users hit.
# 9) Container-to-container networking — no port mapping needed
# In a user-defined network, services reach each other by name on the container's listening port.
services:
web: { build: ./web, ports: ['80:80'] }
api: { build: ./api, expose: ['3000'] }
db: { image: postgres:16, expose: ['5432'] }
# In web's code: fetch('http://api:3000/'), connect to db:5432.
# 10) Networks
docker network create app-net
docker run -d --network app-net --name db postgres
docker run -d --network app-net --name api -p 3000:3000 myapi
# api reaches db at hostname 'db' on port 5432; outside reaches api at host 3000.
# 11) Listing + inspecting
docker ps # current PORTS column
docker port <name> # mappings
docker network inspect bridge # routing rules
docker exec -it api netstat -tlnp # what's listening inside
# 12) IPv6
# Default daemon disables IPv6; enable in /etc/docker/daemon.json:
{
"ipv6": true,
"fixed-cidr-v6": "2001:db8:1::/64"
}
# Then -p 8080:80 publishes on both v4 and v6 (or use [::]:8080:80 explicitly).
# 13) Host network mode (Linux only)
docker run --network host nginx
# Skip the port mapping; container shares host network namespace.
# Pros: faster; cons: no isolation; same port conflicts as bare metal. Use sparingly.
# 14) Macvlan / IPvlan — container gets its own IP on the LAN
# Advanced. Use when you need containers visible like physical hosts.
# 15) Common bugs
# • Reachable from LAN by accident — default 0.0.0.0; bind 127.0.0.1 unless you mean public
# • Port already in use — pick another host port; docker doesn't auto-pick
# • Connection refused inside compose — using 'localhost' from one service to another instead of service name
# • Wrong protocol — UDP service published as TCP; specify /udp
# • Different host port between compose runs — pin in compose to avoid 'works on my machine'
# • Windows / Mac Docker Desktop — host networking limited; LAN access works via the VM IP
# • SELinux / firewall blocking — check ufw / iptables for blocked ports
# • Exposing 0.0.0.0 in production — firewall + WAF in front, or bind to private IP only
Why it matters
Use -p HOST:CONTAINER to publish ports, bind to 127.0.0.1 in dev to avoid LAN exposure, and let containers talk via Compose service names instead of mapping every internal port. EXPOSE is documentation; ports: publishes; expose: in compose keeps the port internal to the network.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Exercise
Map host port 8080 to container port 3000.
docker run
8080:3000 my-api
Two characters.
Discussion
Loading…