Docker Compose — Ports and Expose
Port mapping controls how services are accessible from outside the Compose network. This page covers the ports: and expose: options.
ports: — Publish to Host
The ports: key maps a container port to a host port, making the service accessible from your machine:
services:
web:
image: nginx
ports:
- "8080:80" # host:container
┌──────────────┐ ┌────────────────┐
│ Your Browser │ │ Container │
│ │ │ │
│ localhost ├────────► nginx │
│ :8080 │ │ :80 │
│ │ │ │
└──────────────┘ └────────────────┘
HOST CONTAINER
Short Syntax
ports:
- "8080:80" # HOST:CONTAINER
- "443:443" # HTTPS
- "3000:3000" # Same port on both
- "9090:80" # Different host port
- "127.0.0.1:5432:5432" # Bind to localhost only
- "8080:80/udp" # UDP protocol
Long Syntax
ports:
- target: 80 # Container port
published: "8080" # Host port
protocol: tcp # Protocol (tcp or udp)
host_ip: 127.0.0.1 # Bind to specific host IP
Binding to Localhost Only
By default, published ports are accessible from any IP address. For security, bind to localhost:
services:
db:
image: postgres:16
ports:
# ⚠️ Accessible from any machine on your network
- "5432:5432"
# ✅ Only accessible from localhost
- "127.0.0.1:5432:5432"
expose: — Internal Only
expose: makes a port accessible to other services in the Compose network, but NOT from the host:
services:
api:
build: .
expose:
- "3000" # Other services can reach api:3000
# No ports: — NOT accessible from host machine
web:
image: nginx
ports:
- "80:80" # Accessible from host
# Can reach api:3000 through Compose network
┌────────────────────── Compose Network ──────────────────┐
│ │
│ ┌───────────┐ ┌───────────┐ │
│ │ web │───►│ api │ │
│ │ :80 │ │ :3000 │ │
│ │(published)│ │ (exposed) │ │
│ └─────┬─────┘ └───────────┘ │
│ │ │
└────────┼─────────────────────────────────────────────────┘
│
Host :80
Note: In practice, expose: is mostly documentation. Services can reach each other’s ports within the Compose network regardless of expose:. But it’s good practice to include it for clarity.
ports vs expose
| Feature | ports: |
expose: |
|---|---|---|
| Host access | ✅ Yes | ❌ No |
| Inter-service access | ✅ Yes | ✅ Yes |
| Format | "host:container" |
"container" |
| Use case | External access | Internal documentation |
Port Conflicts
A common error — two services trying to use the same host port:
# ❌ This will fail — port 80 can only be bound once
services:
web1:
image: nginx
ports:
- "80:80"
web2:
image: nginx
ports:
- "80:80" # Error: port already allocated
Fix: use different host ports:
# ✅ Different host ports
services:
web1:
image: nginx
ports:
- "8081:80"
web2:
image: nginx
ports:
- "8082:80"
Summary
| Feature | Syntax | Accessible From |
|---|---|---|
| Published port | ports: - "8080:80" |
Host + other services |
| Localhost only | ports: - "127.0.0.1:8080:80" |
Host localhost + other services |
| Internal only | expose: - "3000" |
Other services only |
Quick Reference
| Item | Detail |
|---|---|
| ports: ‘8080:80’ | Map host 8080 to container 80 |
| ports: ‘127.0.0.1:5432:5432’ | Localhost only binding |
| expose: ‘3000’ | Document internal port |
| Port conflict | Two services can’t use the same host port |
| Long syntax | target, published, protocol, host_ip |
| Best practice | Bind databases to 127.0.0.1 only |