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.
Basic port mapping
services:
web:
image: nginx
ports:
- "8080:80"
The format is HOST:CONTAINER. 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.
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.
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.
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:
- target: 80
published: 8080
protocol: tcp
mode: host
target is the container port, published is the host port. Equivalent to "8080:80" but easier to read in big files.
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. - A firewall is blocking the host port on a remote server.
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.