Docker Sidecar vs Host-Network Mock Routing

Your application runs in a container and needs to call a mock server. You point it at http://localhost:8080, it returns connection refused, and you are stuck — because inside a bridge-networked container localhost is the container’s own loopback, not the host and not the mock. There are two clean ways out: run the mock as a sidecar on a shared Compose network and address it by service name, or run it with network_mode: host and address it on localhost. This page compares both and gives complete, runnable Compose files for each.

Context: why localhost fails between containers

Docker’s default bridge networking gives every container its own network namespace. A container’s localhost (127.0.0.1) resolves to that container’s loopback interface, so an app container calling localhost:8080 is looking for a server inside itself — not the mock container next to it, and not the host. This is the single most common Docker mocking mistake.

Two topologies fix it, and they fix it differently:

  • Sidecar on a Compose network. The app and the mock join the same user-defined bridge network. Docker’s embedded DNS resolves the mock’s service name to its container IP, so the app calls http://wiremock:8080. Each container keeps its own isolated network namespace. This is the model used throughout dockerized mock environments.
  • Host networking. The mock runs with network_mode: host, sharing the host’s network namespace directly. It binds port 8080 on the host itself, so anything that can reach the host loopback — including an app also on host networking — reaches it at localhost:8080.

The diagram contrasts the two paths a request takes:

Sidecar vs host-network mock routing topologies Left: app and mock containers share a Compose bridge network; the app calls the mock by service name wiremock colon 8080 through Docker embedded DNS. Right: the mock runs with network_mode host and binds host port 8080, so an app also on host networking reaches it at localhost colon 8080. Sidecar (bridge network) Host networking bridge network: mock-net app own namespace embedded DNS wiremock :8080 app calls http://wiremock:8080 service name → container IP host network namespace host loopback 127.0.0.1 app host netns wiremock binds :8080 app calls http://localhost:8080 shared loopback, no DNS hop

Comparison: which topology to reach for

Dimension Sidecar (bridge + service DNS) Host networking (network_mode: host)
App addresses the mock as http://wiremock:8080 (service name) http://localhost:8080
Network isolation Full — own namespace per container None — shares the host namespace
Host port collisions Avoided; only published ports touch host Every bound port occupies the host directly
Cross-platform Identical on Linux, macOS, Windows Native on Linux; opt-in and partial on Docker Desktop
Production parity High — mirrors service discovery Low — production rarely uses host mode
Dynamic / many ports Must publish each port explicitly All ports available with no mapping
Best for Almost all mock routing Tools needing raw host access or wide port ranges

The short version: default to the sidecar. It is isolated, portable, collision-free, and it mirrors how services find each other in production. Choose host networking only when a tool genuinely needs the host namespace — for example a mock that opens a large, dynamic range of ephemeral ports that would be painful to publish one by one.

Solution

Option A — Sidecar on a Compose network

The app and mock share mock-net; the app reads the mock’s address from an environment variable set to the service name. Nothing binds the host except the ports you explicitly publish for your own browser access.

# docker-compose.sidecar.yml
services:
  app:
    build: .
    environment:
      # Service DNS — resolved by Docker's embedded resolver, not localhost.
      API_BASE_URL: http://wiremock:8080
    depends_on:
      wiremock:
        condition: service_healthy
    networks:
      - mock-net

  wiremock:
    image: wiremock/wiremock:3.13.2
    command: ["--port=8080", "--verbose"]
    volumes:
      - ./wiremock/mappings:/home/wiremock/mappings:ro
    # Publish only so YOU can curl it from the host; the app does not need this.
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"]
      interval: 5s
      timeout: 3s
      retries: 6
      start_period: 10s
    networks:
      - mock-net

networks:
  mock-net:
    driver: bridge

Inside the wiremock healthcheck, localhost correctly refers to the WireMock container’s own loopback — that is the one place localhost is right, because the probe runs inside that container. The app, in a different container, must use the service name.

Option B — Host networking

Both services join the host namespace. There is no networks: block and no ports: mapping — with host networking, published ports are ignored because the container binds the host directly.

# docker-compose.host.yml   (Linux, or Docker Desktop 4.34+ with host networking enabled)
services:
  app:
    build: .
    environment:
      # Host loopback is shared, so localhost reaches the mock.
      API_BASE_URL: http://localhost:8080
    network_mode: host

  wiremock:
    image: wiremock/wiremock:3.13.2
    command: ["--port=8080", "--verbose"]
    volumes:
      - ./wiremock/mappings:/home/wiremock/mappings:ro
    network_mode: host
    # NOTE: `ports:` is intentionally omitted — it is invalid/ignored with host mode.
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/__admin/health"]
      interval: 5s
      timeout: 3s
      retries: 6
      start_period: 10s

With this file, depends_on: condition: service_healthy still works, and the app reaches the mock at localhost:8080 because both share the host’s single loopback. The coordination of startup and teardown for either topology follows the mock lifecycle management start → wait-healthy → test → teardown sequence; for the deeper WireMock-in-Compose setup that Option A extends, see running WireMock in Docker Compose.

Verification

Prove which loopback each topology uses by curling from inside the app container. For the sidecar, localhost fails and the service name succeeds — that contrast is the whole point:

# --- Option A: sidecar ---
docker compose -f docker-compose.sidecar.yml up -d --wait

# Service DNS works from inside the app container:
docker compose -f docker-compose.sidecar.yml exec app \
  curl -sf http://wiremock:8080/__admin/health && echo "  sidecar: service DNS OK"

# localhost does NOT (it is the app's own loopback) — expect a non-zero exit:
docker compose -f docker-compose.sidecar.yml exec app \
  sh -c 'curl -sf http://localhost:8080/__admin/health || echo "  sidecar: localhost refused (expected)"'
# --- Option B: host networking (Linux / Docker Desktop 4.34+) ---
docker compose -f docker-compose.host.yml up -d --wait

# localhost works because the namespace is shared with the host:
docker compose -f docker-compose.host.yml exec app \
  curl -sf http://localhost:8080/__admin/health && echo "  host: localhost OK"

A passing run prints sidecar: service DNS OK, sidecar: localhost refused (expected), and host: localhost OK, confirming each topology routes exactly as designed.

Gotchas and edge cases

  • Host networking on Docker Desktop is not native Linux. On macOS and Windows, Docker runs inside a Linux VM, so historically network_mode: host bound the VM’s namespace, not your Mac/Windows host — the mock was unreachable from host-side tools. Docker Desktop 4.34+ added opt-in host networking, but it must be enabled in Settings → Resources → Network and still has caveats. If your team is mixed-OS, use the sidecar so the same Compose file behaves identically everywhere.

  • Host mode ignores ports: and collides silently. Publishing ports: under a host-networked service is invalid and either warns or is ignored — the container already owns the host port. Worse, if another process already holds 8080 on the host, the mock fails to bind with address already in use, whereas the sidecar would have quietly mapped a free host port. Check lsof -i :8080 before starting host-mode mocks. The port-collision fix (remap the host side) covered in dockerized mock environments only applies to bridge networking.

  • Docker’s embedded DNS only resolves service names on user-defined networks. The legacy default bridge does not provide name resolution, so http://wiremock:8080 fails there — you must declare an explicit networks: block (as in Option A) to get the embedded resolver. If service-name lookups return could not resolve host, confirm both services are attached to the same named network, not the implicit default bridge.


FAQ

Why can’t my app container reach the mock on localhost?

Inside a bridge-networked container, localhost is the container’s own loopback, not the host or a sibling container. Address the mock by its Compose service name instead (http://wiremock:8080), or switch both services to network_mode: host so the localhost they share is the host loopback the mock binds.

Does network_mode host work on Docker Desktop for Mac and Windows?

Historically no — host networking only bound inside the Linux VM, not the Mac or Windows host, so host-side tools could not reach the mock. Docker Desktop 4.34+ added opt-in host networking, but it must be enabled in Settings and still behaves differently from native Linux. For mixed-OS teams the Compose sidecar with service DNS is the portable default.

When should I prefer a sidecar over host networking?

Prefer a sidecar for almost all mock routing: it is isolated, portable across operating systems, avoids host port collisions, and mirrors production service discovery. Reach for host networking only when the tool needs many dynamic ports or raw host network access that explicit port publishing cannot express.


← Back to Dockerized Mock Environments