background-shape
Networking in Docker Compose, Bridges, Aliases, External Networks
July 18, 2022 · 5 min read · by Muhammad Amal programming

TL;DR — Compose creates a default bridge network per stack. Services reach each other by service name. For cross-stack communication, use external: true networks. Aliases for multi-name services. network_mode: host only 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:

  1. Full bind on all interfaces. Default; what most examples show.
  2. Bind to localhost only. More secure; nothing reachable from LAN.
  3. 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.