Networking in Docker Compose, Bridges, Aliases, External Networks
TL;DR — Compose creates a default bridge network per stack. Services reach each other by service name. For cross-stack communication, use
external: truenetworks. Aliases for multi-name services.network_mode: hostonly on Linux when you really need it.
After volumes, the other axis of inter-container concerns: networking. Compose handles most cases automatically; the cases it doesn’t are worth knowing.
The default network
Compose creates a single bridge network per stack:
services:
postgres: { image: postgres:14-alpine }
api: { build: . }
Both containers join <stack>_default. They reach each other by service name:
api → postgres:5432 # works
api → localhost:5432 # doesn't (localhost = api's own container)
This is the most-common confusion for newcomers. localhost inside a container = the container, not the host or other containers.
Ports mapped to the host (ports: ["5432:5432"]) are for external access — from your laptop, not from other containers.
Service aliases
Add additional DNS names:
services:
postgres:
image: postgres:14-alpine
networks:
default:
aliases:
- db
- primary-db
Now postgres, db, and primary-db all resolve to this container.
Useful when:
- Multiple environments expect different hostnames (config drift across teams)
- You’re transitioning from one name to another and need both during migration
Usually overkill. Most stacks just use service names.
Custom networks
For larger stacks, split into multiple networks:
services:
postgres:
networks: [backend]
redis:
networks: [backend]
api:
networks: [backend, frontend]
bff:
networks: [frontend]
networks:
backend:
frontend:
bff can reach api (both on frontend) but not postgres (not on backend). Provides isolation for security-conscious setups.
In practice for dev: rarely needed. One default network is fine for 90% of stacks.
External networks — cross-stack communication
The killer feature for development environments where multiple compose stacks share infrastructure:
# In your shared-infra/docker-compose.yml
networks:
shared:
name: dev-shared
services:
postgres:
networks: [shared]
redis:
networks: [shared]
# In your app stack
networks:
shared:
external: true
name: dev-shared
services:
api:
networks: [shared, default]
The api joins both its own stack’s default network AND the externally-created dev-shared network. Can reach Postgres in the other stack via service name.
Use case: shared Postgres / Redis used by multiple service stacks during local dev. Stand up infra once; develop multiple services against it.
To set up the shared network:
docker network create dev-shared
Then bring up infra and app stacks. They share Postgres without each running their own.
Host networking (Linux only)
services:
myservice:
network_mode: host
Container uses the host’s network stack directly. No port mapping needed; bind to a port on the container = bind to that port on the host.
When to use:
- High-performance use cases where the bridge network adds latency
- Services that need to discover other containers via mDNS / Bonjour
- Network testing tools
When NOT to use:
- Default. Bridge is fine and isolates.
- Mac/Windows: doesn’t fully work (Docker Desktop runs in a VM). Often silently weird.
Exposing ports
services:
api:
ports:
- "8080:8080" # host:container, all interfaces
- "127.0.0.1:5432:5432" # bind to localhost only
- "443" # random host port
Three patterns:
- Full bind on all interfaces. Default; what most examples show.
- Bind to localhost only. More secure; nothing reachable from LAN.
- Random port. Compose picks a free port. Useful in CI where conflicts are common.
For dev, full bind is fine. For security-sensitive setups (e.g., dev VM with multiple developers), localhost-only.
Inter-container access without ports
For services that only need to talk to each other (no host access), don’t expose ports:
services:
redis:
image: redis:7-alpine
# no `ports:` — only accessible from containers on the same network
api can still reach redis:6379. Your laptop cannot. Cleaner; less to forget.
DNS resolution gotchas
Container restart changes IP. Service name resolves via Docker’s internal DNS, so the IP behind the name updates automatically. Apps should use the service name, not cache IPs.
DNS round-robin for replicas. docker compose up -d --scale api=3 runs 3 api containers. api DNS resolves to all three; Docker’s DNS does round-robin. Your client gets one IP per lookup. If your client caches DNS, you get one of three until restart.
TLD: .docker.internal. From within a container, host.docker.internal resolves to the Docker host (Mac/Windows; Linux needs the host-gateway mapping). Useful for hitting a service running on the host directly.
A real-world layout
name: my-app
services:
postgres:
image: postgres:14-alpine
# no host port — internal only
networks: [internal]
healthcheck: { ... }
redis:
image: redis:7-alpine
networks: [internal]
api:
build: ./services/api
networks: [internal]
ports:
- "127.0.0.1:8080:8080" # localhost only for host access
depends_on:
postgres: { condition: service_healthy }
bff:
build: ./services/bff
networks: [internal, public]
ports:
- "127.0.0.1:3000:3000"
depends_on: [api]
networks:
internal:
internal: true # no external connectivity
public:
Two networks: internal is isolated (no internet access), public allows outbound (for BFF to hit external APIs). API can only reach external services via BFF. Tighter security model for prod-like dev.
Common Pitfalls
Using localhost to reach other containers. Doesn’t work. Use service name.
Forgetting that ports are for host access only. “I added ports but my other container still can’t reach it” — they always could, via service name.
External network name mismatch. name: on the network definition must match what docker network create created. Typo silently joins a different network.
Host networking on Mac. Sometimes works, sometimes doesn’t. Avoid.
Same default network across stacks. Two projects named my-app and myapp create different networks. Naming convention matters.
No internal-only flag for sensitive stacks. Internet-reachable services for things that should never reach internet (databases). Mark internal: true.
Wrapping Up
Default network + service names cover 90% of cases. External networks for cross-stack infra. Custom networks for isolation. Wednesday: build vs image in Compose — building vs pulling.