Summary
Containers run services on their default internal ports. Docker’s port-mapping flag (-p host:container) exposes each container to a unique port on the host, preventing conflicts when running multiple instances of the same service on one machine.
Problem
Running two PostgreSQL instances on the same host fails immediately — both try to bind to port 5432. The same applies to any duplicated service (Redis on 6379, MySQL on 3306, etc.).
- How do you run multiple instances of the same service on one host without reconfiguring the service itself?
- How do you keep each container using its default internal port so tooling and documentation remain standard?
- How do you ensure port assignments are predictable and documented?
Solution
Map each container’s internal port to a unique host port. The service inside the container stays on its default port; only the host-facing port changes.
# docker-compose.yml
services:
project_a_db:
image: postgres:16
ports:
- "5432:5432" # host:container
volumes:
- project_a_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: project_a
POSTGRES_USER: admin
POSTGRES_PASSWORD: ${PROJECT_A_DB_PASSWORD}
project_b_db:
image: postgres:15
ports:
- "5433:5432" # different host port, same internal port
volumes:
- project_b_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: project_b
POSTGRES_USER: admin
POSTGRES_PASSWORD: ${PROJECT_B_DB_PASSWORD}
volumes:
project_a_data:
project_b_data:
Each container listens on 5432 internally — the standard PostgreSQL port. Docker maps that to a distinct host port. Clients connect to the host port:
# Connect to project A
psql -h localhost -p 5432 -U admin -d project_a
# Connect to project B
psql -h localhost -p 5433 -U admin -d project_b
Document your port assignments to avoid collisions as the fleet grows:
| Project | Host Port | Internal Port | Image |
|---|---|---|---|
| Project A | 5432 | 5432 | postgres:16 |
| Project B | 5433 | 5432 | postgres:15 |
| Project C | 5434 | 5432 | postgres:16 |
As the fleet grows, a manually-maintained table drifts. A pre-flight check script catches collisions before docker compose up fails halfway through starting a stack:
#!/usr/bin/env bash
# check-port-conflicts.sh — scan a compose file's declared host ports
# against ports already bound on this machine.
set -euo pipefail
compose_file="${1:-docker-compose.yml}"
# Extract host-side ports from "host:container" mappings
host_ports=$(grep -oP '"\K[0-9]+(?=:[0-9]+")' "$compose_file" | sort -u)
conflicts=0
for port in $host_ports; do
if lsof -iTCP:"$port" -sTCP:LISTEN -t >/dev/null 2>&1; then
echo "CONFLICT: host port $port is already in use"
conflicts=$((conflicts + 1))
fi
done
if [ "$conflicts" -gt 0 ]; then
echo "Found $conflicts port conflict(s) — resolve before starting the stack."
exit 1
fi
echo "No port conflicts detected across $(echo "$host_ports" | wc -l) mapped port(s)."
Run it as a pre-flight gate: ./check-port-conflicts.sh docker-compose.yml && docker compose up -d. This turns a runtime failure (a container that silently fails to bind) into a fast, explicit pre-flight error.
Live Playground
Experiment with the pattern below. The before version assigns every service the same hardcoded host port and only discovers the collision when the second container tries to bind. The after version tracks assigned host ports and allocates the next free one, catching the conflict before it ever reaches Docker.
When to Use
- Running multiple instances of the same service on a single host
- Development or testing environments where each project needs its own database
- Quick isolation without full orchestration overhead
Avoid when:
- You only need one instance of the service — just use the default port
- Running in an orchestrated environment (Kubernetes, ECS) where the scheduler manages port allocation
- The service supports virtual hosting or multiplexing (e.g., a single PostgreSQL instance with multiple logical databases may suffice)
Trade-offs
| Benefit | Cost |
|---|---|
| Containers stay on service defaults — no internal reconfiguration | Host port assignments must be tracked manually |
| Simple, well-understood Docker primitive | Port sprawl as the number of services grows |
| Each container is independently startable and stoppable | Named volumes require cleanup discipline on teardown |
Related Patterns
- Multi-Database Orchestration — builds on port mapping to manage a full fleet of database containers with health checks, resource limits, and production readiness
- Infrastructure as Code — port assignments should be declared in IaC configuration rather than managed ad-hoc, so networking is reproducible and reviewable
- Hexagonal Architecture — container port mapping is a deployment concern that stays outside the application core; the database is accessed through a port abstraction regardless of where it is bound