KhueApps
Home/DevOps/Fix 'exported ports must be unique' in Docker Compose

Fix 'exported ports must be unique' in Docker Compose

Last updated: October 07, 2025

Overview

Docker Compose throws “exported ports must be unique” when two or more port publishes in the same Compose project collide. The conflict is usually the same published (host) port used multiple times with the same protocol and host IP.

Key points:

  • The uniqueness check applies to the merged Compose config (after profiles, overrides, and variable expansion).
  • Duplicates can exist across services or within a single service’s ports list.
  • Runtime conflicts can also occur with other containers or host processes even if Compose validation passes in separate projects.

Quickstart: fix duplicates fast

  1. Render the merged config

    • This shows the actual ports after applying all overrides and variables.
    docker compose config > merged.yml
    
  2. List published ports and spot duplicates

    • Look for repeated published ports (and protocol) across services.
    # Quick scan for published ports in the merged file
    grep -n "published:" merged.yml | sed 's/.*published: \(.*\)/\1/' | sort | uniq -c
    
  3. Apply one of these fixes

    • Make host ports unique: change published ports so each is different.
    • Remove unnecessary publishes: most internal services don’t need ports at all.
    • Use ephemeral publishes: specify only the target (container) port so Docker picks a free host port.
    • Split TCP/UDP explicitly if you truly need both.
  4. Recreate the stack

    docker compose up -d --remove-orphans --force-recreate
    docker compose ps
    

Broken example (for reference)

This fails because both services publish host port 8080/tcp.

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"   # publishes 8080/tcp
  admin:
    image: nginx:alpine
    ports:
      - "8080:80"   # duplicate publish of 8080/tcp → error

Minimal working example

Two services, each with a distinct published port:

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"     # host 8080 → container 80 (tcp)

  admin:
    image: nginx:alpine
    ports:
      - "8081:80"     # host 8081 → container 80 (tcp)

Alternative: publish only what’s public, keep internal services private:

services:
  api:
    image: nginx:alpine
    # Only the gateway is published externally

  gateway:
    image: nginx:alpine
    depends_on: [api]
    ports:
      - "8080:80"
    # api is reachable internally at http://api:80 without publishing api

Ephemeral host ports (Docker picks an available host port):

services:
  svc1:
    image: nginx:alpine
    ports:
      - "80"      # publish container 80 → random host port
  svc2:
    image: nginx:alpine
    ports:
      - "80"      # allowed; different random host port

Check assigned ports with:

docker compose ps

Common scenarios and fixes

SymptomCauseFix
Two services use 8080:80Same published portChange one to 8081:80 or remove one publish
Duplicate entries inside one serviceCopy/paste or YAML anchor expanded twiceRemove duplicates or consolidate to long syntax
Profiles/overrides reintroduce a portMerge from multiple filesInspect docker compose config and adjust specific file
Need both TCP and UDP on same portProtocol not specifiedUse two entries: 80:80/tcp and 80:80/udp
Only one service should be publicUnnecessary publishesReplace ports with expose or remove entirely

Long syntax tips (clearer and less error-prone)

Prefer long syntax for explicitness:

services:
  web:
    image: nginx:alpine
    ports:
      - target: 80       # container port
        published: 8080  # host port
        protocol: tcp
        mode: ingress
  • Uniqueness is evaluated on (host_ip, published, protocol). If you publish the same port on different host IPs, specify host_ip explicitly in long syntax.

Example binding to loopback only:

services:
  admin:
    image: nginx:alpine
    ports:
      - target: 80
        published: 8080
        protocol: tcp
        host_ip: 127.0.0.1

Pitfalls to avoid

  • Overlaps from multiple files: -f docker-compose.yml -f docker-compose.prod.yml can re-add ports. Always inspect the merged output.
  • Variable collisions: ${PORT:-8080} used in multiple services resolves to the same published port.
  • YAML anchors/aliases: reused anchors can duplicate a port unintentionally.
  • Subtle duplicates: 80:80 and 80:80/tcp are the same for TCP; specifying protocol doesn’t make them unique unless protocols differ.
  • network_mode: host: avoid combining with ports (it’s ignored). Host networking means container ports are on the host directly.
  • Different projects on the same host: validation passes per project, but runtime may still fail if the port is already taken by another container or process.

Performance notes

  • Publish only what you need. Each published port adds NAT/iptables rules; fewer publishes reduce overhead and startup time on busy hosts.
  • Use a single public entry point (reverse proxy or gateway) rather than publishing many service ports.
  • Internal traffic over the default bridge network is fast and avoids NAT; keep backend services private when possible.
  • Ephemeral host ports are fine for development; for production, prefer fixed ports or a stable frontend.

Step-by-step remediation checklist

  1. Run docker compose config and search for published: lines.
  2. Ensure each (host_ip, published, protocol) tuple appears at most once.
  3. Remove ports from services that don’t need external access; use expose if you want documentation-only exposure.
  4. Convert to long syntax for clarity and to set host_ip or protocol explicitly when needed.
  5. Recreate the stack and verify with docker compose ps and docker ps.

Tiny FAQ

  • Why does Compose error before starting containers?

    • It validates the merged spec and blocks known conflicts (duplicate publishes) early.
  • Can I publish the same port for TCP and UDP?

    • Yes. Use two entries: one /tcp, one /udp.
  • Do I need ports for service-to-service calls?

    • No. Containers on the same Compose network can talk via service name and container port without publishing.
  • How do I see which process holds a port outside Compose?

    • On Linux/macOS: lsof -i :PORT or ss -tulpen | grep :PORT. On Windows: netstat -ano | findstr :PORT.

Series: Docker

DevOps