Docker Compose — Ports and Expose

2 minute read

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