Docker Compose Ports: Mapping and Exposing Explained (2026)
The ports key in Docker Compose decides which container ports are reachable from outside Docker. Getting it slightly wrong is the most common reason an app "works in the container" but you cannot open it in a browser. This guide covers the syntax and the gotchas.
Docker Compose ports syntax
services:
web:
image: nginx
ports:
- "8080:80"
The short form is [HOST:]CONTAINER[/PROTOCOL]. The example maps port 80 inside the container to 8080 on your host, so http://localhost:8080 reaches Nginx. The container side stays 80; you change only the host side to avoid clashes.
Every short form Compose accepts:
| Mapping | What it does |
|---|---|
"8080:80" | Host 8080 → container 80 |
"80" | Container 80 → an ephemeral random host port |
"127.0.0.1:8080:80" | Bind to one host IP instead of all interfaces |
"9090-9091:8080-8081" | Port range; host and container ranges must be the same length |
"53:53/udp" | UDP instead of the default TCP |
"[::1]:6001:6001" | IPv6 host address, in square brackets |
Rule of thumb: the number on the left is the one you type in your browser. The number on the right is the port your app actually listens on inside the container.
Two details the examples above hide:
HOST:CONTAINERmust be a quoted string. Unquoted,80:80is a valid YAML sexagesimal number and Compose will misread it. Quote every mapping.- Port mapping cannot be combined with
network_mode: host. The container already has the host's network, so Compose fails at runtime. Remove theportsentry when you switch a service to host networking.
ports vs expose
These are not the same thing, and mixing them up causes a lot of confusion.
| Key | What it does | Reachable from |
|---|---|---|
ports | Publishes a port to the host | Host machine and other containers |
expose | Documents a port for other containers | Other containers only |
services:
app:
build: .
expose:
- "3000" # only other services can reach app:3000
web:
image: nginx
ports:
- "80:80" # the public entry point
Use expose (or nothing at all — containers on the same Compose network can already reach each other) for internal services like a database. Use ports only for what the outside world needs.
Binding to a specific interface
By default "8080:80" listens on every interface (0.0.0.0), which means the port is reachable from your whole network. To keep a service on localhost only:
services:
db:
image: postgres:16
ports:
- "127.0.0.1:5432:5432"
The full long form is HOST_IP:HOST_PORT:CONTAINER_PORT. Binding to 127.0.0.1 is a simple way to stop a database being exposed to your LAN.
Warning: without a host IP, Docker publishes on
0.0.0.0and bypasses host firewall rules. On a server with a public IP,"8080:80"is reachable from the internet even if you never opened the port inufworfirewalld. Bind to127.0.0.1unless you actually want public access.
Port ranges and protocols
services:
app:
image: myapp
ports:
- "3000-3005:3000-3005" # a range
- "53:53/udp" # UDP instead of TCP
Append /udp when the service is not TCP (DNS, some game servers). The default is TCP.
Long form
For clarity, the long syntax spells out each field:
services:
web:
image: nginx
ports:
- name: web
target: 80
host_ip: 127.0.0.1
published: "8080"
protocol: tcp
app_protocol: http
mode: host
| Field | Meaning |
|---|---|
target | The container port (required) |
published | The host port, as a string; accepts a range like "8083-9000" |
host_ip | Host IP to bind. Omit to bind all interfaces |
protocol | tcp (default) or udp |
app_protocol | Layer-7 hint, e.g. http, https |
mode | Swarm only: ingress (default) or host |
name | Human-readable label for the mapping |
target and published are the container and host ports from the short form. The rest are options the short syntax cannot express. If you are not running Swarm, leave mode alone.
Why your port isn't reachable
A checklist when the mapping looks right but nothing loads:
- The app listens on
127.0.0.1inside the container. It must bind to0.0.0.0so Docker can forward traffic to it. This is the number-one cause. - Wrong side of the colon.
"80:8080"means the host is80and the container is8080— easy to flip. - Host port already in use. Another process owns it. Check with
sudo lsof -i :8080and pick a different host port. - You only set
expose. That does not publish to the host. Useports. - Port mapping combined with
network_mode: host. Compose rejects the service at runtime. Drop theportsentry. - Unquoted mapping.
- 80:80without quotes is parsed as the number 8080 in YAML. Always write- "80:80". - A firewall is blocking the host port on a remote server — though remember that publishing on
0.0.0.0skips host firewall rules, so a port you never opened can still be reachable.
For container-to-container traffic, which does not use ports at all, see Docker Compose networks. For host-to-container networking edge cases, see Docker host networking. To persist data for the same service, see Docker Compose volumes.
Let the platform handle ports
On Hostim.dev you declare the one HTTP port your app listens on, and it is published on HTTPS with a domain — no host-port juggling or firewall rules.
👉 Deploy an app — we handle the ports and HTTPSFrequently asked questions
What does HOST:CONTAINER mean in Docker Compose ports?
In a port mapping like '8080:80', the left number (8080) is the port on your host machine that you connect to, and the right number (80) is the port the app listens on inside the container. You typically change only the host side to avoid clashes with other services.
What is the difference between ports and expose in Docker Compose?
'ports' publishes a port to the host machine so it is reachable from outside Docker. 'expose' only documents a port for other containers on the same network and does not make it reachable from the host. Use 'ports' for public entry points and 'expose' (or nothing) for internal services.
Why is my Docker Compose port not working?
The most common cause is that the app inside the container binds to 127.0.0.1 instead of 0.0.0.0, so Docker cannot forward traffic to it. Other causes are flipping the host and container sides of the mapping, the host port already being in use, using 'expose' instead of 'ports', or a firewall blocking the port.
How do I bind a Docker Compose port to localhost only?
Add the host IP to the mapping, like '127.0.0.1:5432:5432'. The full form is HOST_IP:HOST_PORT:CONTAINER_PORT. Binding to 127.0.0.1 keeps the service reachable only from the host and not from the rest of your network. This matters because the default, with no host IP, publishes on 0.0.0.0 and bypasses host firewall rules.
What does a single port mean in Docker Compose ports?
A mapping with only the container port, like '80', publishes that container port on an ephemeral, randomly chosen host port. Find the assigned port with 'docker compose port <service> 80' or in 'docker compose ps'. Use the HOST:CONTAINER form when you need a fixed host port.
Can I use ports with network_mode: host in Docker Compose?
No. Port mapping and network_mode: host cannot be combined; Compose fails at runtime because the container already shares the host network and exposes its ports directly. Remove the ports entry when you switch a service to host networking. Likewise, 'ports' and 'expose' cannot be used to route around it.
Does Docker Compose need the colon mapping as a quoted string?
Yes. 'HOST:CONTAINER' should always be written as a quoted string, like "8080:80". Unquoted, YAML parses 80:80 as a sexagesimal number and the value becomes 8080, which silently produces the wrong mapping.