Local Network and Port Mapping
Configure deterministic local networking for containerized dev environments. Covers port binding, host-to-container routing, conflict resolution, and microservice DNS.
Two engineers binding the same host port, a service that cannot find its dependency by name, an IDE that forwards the wrong port — local networking friction is mundane but constant. This guide gives platform engineers and tech leads deterministic port-binding and DNS-routing configurations that hold across heterogeneous workstations. It is part of the broader containerized local environment patterns and pairs with multi-service orchestration with Compose for startup ordering.
Prerequisites
- Docker Compose v2 (
docker compose versionreports 2.x). ssornetstatfor host port inspection, andjqfor parsingdocker inspectoutput.- A
.envfile (gitignored) for per-developer port overrides, with a committed.env.example.
Host-to-Container Port Binding and Conflict Resolution
Parallel workflows collide when multiple engineers bind identical host ports, producing bind: address already in use. Drive every host-facing port through an .env variable with a default, and bind to 127.0.0.1 so nothing is exposed beyond the workstation.
# docker-compose.yml
services:
app:
image: myorg/app:latest
ports:
- "127.0.0.1:${APP_PORT:-3000}:3000"
db:
image: postgres:16-alpine
ports:
- "127.0.0.1:${DB_PORT:-5432}:5432"
Pre-flight audit — confirm the host port is free before bringing the stack up:
#!/usr/bin/env bash set -euo pipefail for p in "${APP_PORT:-3000}" "${DB_PORT:-5432}"; do if ss -tuln | grep -q ":${p} "; then echo "ERROR: port ${p} already in use" >&2 exit 1 fi done echo "ports free"Override locally by setting
APP_PORT/DB_PORTin.envwhen a teammate's stack already holds the default.Drift check — diff the resolved binding against the committed defaults:
#!/usr/bin/env bash set -euo pipefail docker compose ps --format '{{.Names}} {{.Ports}}'
When the bind still fails because a stale container or another Compose project owns the port, work through fixing "port is already allocated" errors in Compose.
Custom Bridge Networks and Service Discovery
A dedicated Compose network prevents host network pollution and guarantees predictable inter-service resolution. Replace hardcoded IPs in connection strings with service names or DNS aliases.
# docker-compose.yml
networks:
dev_net:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
gateway: 172.28.0.1
services:
api:
image: myorg/api:latest
networks:
- dev_net
db:
image: postgres:16-alpine
networks:
dev_net:
aliases:
- db.internal
Resolution test — confirm container-to-container name resolution:
#!/usr/bin/env bash set -euo pipefail docker compose exec api getent hosts db.internalRouting alignment — keep alias naming consistent with your DNS routing for microservices convention.
Drift check — inspect the resolver and flag missing aliases:
#!/usr/bin/env bash set -euo pipefail docker compose exec api cat /etc/resolv.conf
Devcontainer Network Integration and IDE Port Forwarding
IDE-level integration requires explicit attachment to the Compose bridge network. VS Code's Dev Containers extension forwards ports automatically, but a mismatched forwardPorts array causes routing surprises and extension timeouts. Keep these aligned with the devcontainer configuration standards.
// .devcontainer/devcontainer.json
{
"dockerComposeFile": "../docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"forwardPorts": [3000, 5432, 8080],
"portsAttributes": {
"3000": { "label": "Frontend", "onAutoForward": "silent" },
"5432": { "label": "Postgres", "onAutoForward": "notify" }
}
}
Connectivity test — open the Ports panel and confirm each forwarded port maps to an active listener.
Standardization — cross-check
forwardPortsagainst theports:declared indocker-compose.yml.Drift check:
#!/usr/bin/env bash set -euo pipefail docker compose config | grep -A2 'ports:'
Dynamic Port Allocation for Shared Environments
Static port assignments fail in shared environments. A deterministic seed script scans occupied host ports, exports a free range to .env.local, and brings the stack up with zero conflicts.
#!/usr/bin/env bash
# setup-local.sh
set -euo pipefail
find_free_port() {
local port="$1"
while ss -tuln | grep -q ":${port} "; do
port=$((port + 1))
done
echo "$port"
}
{
echo "DYNAMIC_DB_PORT=$(find_free_port 5432)"
echo "DYNAMIC_API_PORT=$(find_free_port 3000)"
} > .env.local
echo "Wrote dynamic ports to .env.local"
docker compose --env-file .env.local up -d
- Allocation logging — route service traffic through stable names via DNS routing for microservices so application configs never reference the volatile port.
- Variable injection —
docker compose --env-file .env.local configto verify resolved values. - Drift check — reject PRs that commit
.env.local; fail bootstrap on unresolved vars withdocker compose config --quiet.
macOS (Docker Desktop): Containers run inside a Linux VM, so host port binds add ~10–50ms under connection churn;
lsofmay needsudoto see every listener — preferss. Replacehost.docker.internalreferences with internal service names before comparing against production. WSL2:localhostforwards to container ports automatically, but IPv6 binds ([::]:3000) often fail — map to0.0.0.0or disable IPv6. Run all port scans inside the distro, not PowerShell, or you query the wrong stack. Apple Silicon (ARM64): Port mapping behaves like AMD64, but pull multi-arch images so emulation does not exacerbate proxy timeouts.
Rollback / recovery
If a networking change breaks resolution or leaves a port wedged, tear the stack down, prune the project network, and recreate from the committed configuration:
#!/usr/bin/env bash
set -euo pipefail
docker compose down --remove-orphans
docker network prune -f
git checkout HEAD -- docker-compose.yml
docker compose up -d --wait