Port Mapping & Expose
Port mapping is how you make containerized applications accessible from outside the Docker host. This guide covers publishing ports, the EXPOSE instruction, port binding options, and when to use host networking.
By default, containers have their own private network namespace. They can communicate with each other freely on user-defined networks, but they are not accessible from outside the Docker host. This is a security feature—containers are isolated from the outside world.
Port mapping creates a bridge between the host's network and the container's network. It forwards traffic from a port on the Docker host to a port inside the container. This allows external clients (browsers, API clients, other services) to connect to your containerized applications.
External Client
│
▼
Host Port: 8080 ──────┐
│ │ (Port Mapping)
▼ ▼
Docker Host Container Port: 80
(iptables forwarding)
The EXPOSE instruction in a Dockerfile is documentation. It tells users which ports the container listens on, but it does NOT actually publish the port. Publishing still requires the -p or -P flag when running the container.
EXPOSE is useful for: documenting container requirements, being used by -P (random port mapping), and being read by tools like Docker Compose. However, it has no effect on networking—containers can still communicate without EXPOSE, and ports are not opened on the host.
# Dockerfile with EXPOSE
FROM nginx:alpine
EXPOSE 80
EXPOSE 443/tcp
# EXPOSE is just documentation - doesn't actually open ports!
# Build the image
docker build -t mynginx .
# Run with EXPOSE but no -p - NOT accessible from host
docker run -d --name nginx1 mynginx
# Cannot access from host browser - no port mapping!
# Run with -p to actually publish
docker run -d --name nginx2 -p 8080:80 mynginx
# Now accessible at http://localhost:8080
# Run with -P to publish all EXPOSED ports to random host ports
docker run -d --name nginx3 -P mynginx
docker port nginx3
# 80/tcp -> 0.0.0.0:32768 (random port)
The -p (or --publish) flag maps a host port to a container port. The full syntax is: -p [host_ip:]host_port:container_port[/protocol]. You can use multiple -p flags to publish multiple ports.
Common patterns:
-p 8080:80- Map host port 8080 to container port 80 (TCP default)-p 127.0.0.1:8080:80- Bind only to localhost, not accessible from other machines-p 8080-8085:80-85- Map a range of ports-p 53:53/udp- Specify UDP protocol-p 8080:80/tcp -p 8080:80/udp- Both TCP and UDP on same port
# Basic port mapping
docker run -d --name web -p 8080:80 nginx
# Bind to specific host IP (only localhost)
docker run -d --name web-local -p 127.0.0.1:8080:80 nginx
# Multiple ports
docker run -d --name app \
-p 8080:3000 \
-p 5432:5432 \
-p 6379:6379 \
myapp
# UDP port mapping (DNS)
docker run -d --name dns -p 53:53/udp -p 53:53/tcp coredns/coredns
# Port range mapping
docker run -d --name streaming -p 8000-8010:8000-8010/udp mystreamer
# Check published ports
docker port web
# 80/tcp -> 0.0.0.0:8080
The -P (uppercase) flag publishes all ports that are EXPOSED in the Dockerfile to random high-numbered ports on the host. This is useful when you don't care which specific host ports are used, or when running multiple instances of the same container to avoid port conflicts.
To see which random ports were assigned, use docker port container-name. You can also configure the default port range (usually 32768-60999).
# Dockerfile with multiple EXPOSE
FROM node:18-alpine
EXPOSE 3000
EXPOSE 9229 # debug port
EXPOSE 8080/udp
# Run with -P (publish all exposed ports to random host ports)
docker run -d --name myapp -P myapp
# Check assigned ports
docker port myapp
# 3000/tcp -> 0.0.0.0:32768
# 9229/tcp -> 0.0.0.0:32769
# 8080/udp -> 0.0.0.0:32770
# Access application at http://localhost:32768
# Useful for running multiple instances without port conflicts
for i in 1 2 3; do
docker run -d --name app-$i -P myapp
done
Host networking mode (--network host) removes network isolation entirely. The container uses the host's network stack directly—no port mapping required. The container's ports are automatically available on the host's IP address.
Host networking offers the highest performance (no NAT overhead) and is useful for performance-critical applications or when you need the container to bind to many ports. However, it reduces security and can cause port conflicts.
Important: Host network mode only works on Linux. On Docker Desktop (macOS/Windows), it behaves like the default bridge network.
# Run with host networking (Linux only)
docker run -d --name web --network host nginx
# Nginx is now accessible directly on host port 80
# No -p flag needed! http://localhost:80 works
# Check that container uses host's network namespace
docker exec web ip addr show
# Shows same interfaces as running 'ip addr show' on host
# Use case: performance-sensitive applications
docker run -d --name metrics --network host prom/prometheus
# Use case: binding to many ports
docker run -d --name game-server --network host my-game-server
When you try to map a host port that's already in use, Docker will fail to start the container. You'll see an error like "port is already allocated" or "bind: address already in use". Here's how to handle port conflicts:
- Stop the container using that port, or
- Choose a different host port, or
- Use
-Pto let Docker pick a random port, or - Use host networking mode (if appropriate)
# This will fail if port 80 is already in use
docker run -d --name web -p 80:80 nginx
# Error: driver failed programming external connectivity...
# Check what's using a port
sudo lsof -i :80 # Linux/macOS
netstat -ano | findstr :80 # Windows
# Stop the conflicting container
docker stop conflicting-container
# Or use a different host port
docker run -d --name web -p 8080:80 nginx
# Or let Docker pick a random port
docker run -d --name web -P nginx
# Check what port was assigned
docker port web
# List all port mappings for a container
docker port myapp
# 80/tcp -> 0.0.0.0:8080
# 443/tcp -> 0.0.0.0:8443
# Filter by container port
docker port myapp 80
# 0.0.0.0:8080
# Filter by protocol
docker port myapp 53/udp
# 0.0.0.0:32768
# List all containers with their ports
docker ps --format "table {{.Names}}\t{{.Ports}}"
# Show all published ports across all containers
docker ps --format "{{.Ports}}" | grep -v "^$"
In Docker Compose, port mapping is defined in the `ports` section. The syntax is similar to the `-p` flag. Docker Compose also supports the `expose` directive (documentation only, similar to EXPOSE in Dockerfile).
# docker-compose.yml
version: '3.8'
services:
web:
image: nginx
ports:
- "8080:80" # host:container
- "127.0.0.1:8443:443" # bind to specific IP
- "3000-3005:3000-3005" # port range
- "53:53/udp" # UDP protocol
expose:
- "80"
- "443"
app:
image: myapp
ports:
- "8080:3000"
# expose without publishing (internal only)
expose:
- "3000"
Web Application: Expose web server to the world
docker run -d --name web -p 80:80 -p 443:443 nginx
Database (not recommended to expose directly): For development only
docker run -d --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:15
Multiple Services in One Container: Map multiple ports
docker run -d --name app \
-p 8080:3000 \
-p 9229:9229 \
-p 5432:5432 \
myapp
Development with Hot Reload: Expose debug port
docker run -d --name dev \
-p 3000:3000 \
-p 9229:9229 \
-v "$PWD":/app \
node:18 npm run dev
- Use EXPOSE in Dockerfiles for documentation - It helps users know which ports to publish.
- Don't expose databases directly to the host - In production, databases should only be accessible to other containers on internal networks.
- Bind to localhost for development - Use
-p 127.0.0.1:8080:80to prevent external access during development. - Use distinct host ports for multiple containers - Avoid conflicts by using different host ports or
-P. - Prefer specific host ports for production - Random ports (
-P) are hard to configure for load balancers and DNS. - Use host networking sparingly - It disables Docker's network isolation. Only use when performance is critical.
- Document port mappings in docker-compose.yml - This makes it clear how services are exposed.
-p IP:host_port:container_port. For example, -p 192.168.1.100:8080:80 binds only to that specific IP address on the host.-p 53:53/udp for UDP, or -p 8080:80/tcp for TCP (default). You can also map both protocols: -p 53:53/tcp -p 53:53/udp.docker ps. Did you use -p 8080:80 correctly? Is the container's application actually listening on port 80? Check with docker logs container./etc/docker/daemon.json and setting userland-proxy and ip settings.--network host behaves like the default bridge network. Use standard port mapping instead. Port mapping is how your containers connect to the outside world. Use -p for specific ports, -P for random ports, and reserve host networking for special cases.