Microservices that call each other by custom FQDN (api-gateway.internal) fail with Could not resolve host because Docker's embedded resolver only knows container names on the active network. This page configures a deterministic local resolver that mirrors Kubernetes service discovery; it extends local network and port mapping within the broader containerized local environment patterns.

Diagnostic

A frontend or API consumer reaching a downstream service fails at name resolution, not connection refusal:

#!/usr/bin/env bash
set -euo pipefail
docker compose exec frontend curl -sI http://api-gateway.internal:8080/health \
  || echo "DNS resolution failed"

Expected BAD output:

curl: (6) Could not resolve host: api-gateway.internal
DNS resolution failed

Confirm the network is running on the default embedded resolver with no overrides:

#!/usr/bin/env bash
set -euo pipefail
docker network inspect myapp_default --format '{{json .Options}}' | jq '.dns'
null

A null result confirms the network relies on the default 127.0.0.11 resolver, which does not handle custom TLDs.

Root Cause

Docker's embedded DNS at 127.0.0.11 resolves only container names and aliases defined within the active Compose network — it does not handle arbitrary FQDNs, custom TLDs, or external routing. Host /etc/hosts entries are isolated from container namespaces and never propagate. Worse, .local is intercepted by mDNS/Bonjour on macOS and Linux, and corporate forwarders frequently hijack .internal, so the same name resolves differently on every developer's machine.

Resolution

  1. Standardize on a reserved, non-routable suffix (.svc.cluster.local or a project-specific .docker.internal) across all Compose files to avoid mDNS/forwarder collisions.

  2. Add a CoreDNS resolver to the Compose network. Create a Corefile:

    .:53 {
      hosts {
        172.20.0.3 auth-service.internal
        172.20.0.4 payment-worker.internal
        172.20.0.5 api-gateway.internal
        fallthrough
      }
      forward . 127.0.0.11
      log
    }
  3. Declare the resolver as a service with a fixed address and point the network at it:

    # docker-compose.yml
    services:
      coredns:
        image: coredns/coredns:1.11.3
        command: ["-conf", "/etc/coredns/Corefile"]
        volumes:
          - ./Corefile:/etc/coredns/Corefile:ro
        networks:
          app_net:
            ipv4_address: 172.20.0.2
    networks:
      app_net:
        driver: bridge
        ipam:
          config:
            - subnet: 172.20.0.0/16
  4. Override DNS for consumers so they query CoreDNS first, then fall back to the embedded resolver:

    # docker-compose.yml
    services:
      api-gateway:
        dns:
          - 172.20.0.2
          - 127.0.0.11
  5. Validate propagation:

    #!/usr/bin/env bash
    set -euo pipefail
    docker compose exec api-gateway getent hosts auth-service.internal

For bridge isolation and ensuring UDP/53 is not blocked by iptables DROP rules, cross-check the bridge network configuration.

Expected Output

After applying the resolver, custom-TLD names resolve deterministically:

172.20.0.3      auth-service.internal

A loop across services confirms parity:

#!/usr/bin/env bash
set -euo pipefail
for svc in api-gateway auth-service payment-worker; do
  if ! docker compose exec -T "$svc" getent hosts "${svc}.internal" >/dev/null 2>&1; then
    echo "FAIL: ${svc}.internal did not resolve" >&2
    exit 1
  fi
done
echo "DNS parity verified across all microservices"

Prevention

  • Commit the Corefile and the dns: overrides so resolution is reproducible on every clone.
  • Add a pre-flight DNS check to your Makefile or bootstrap that fails fast when a service FQDN returns NXDOMAIN before integration tests run.
  • Validate the Compose schema in CI with docker compose config --quiet to catch malformed dns: directives before merge.

macOS (Docker Desktop): .local is claimed by Bonjour; never use it for service routing. The injected host.docker.internal can mask a broken internal resolver — test with explicit FQDNs. WSL2: The localhost resolver bypasses Docker's embedded DNS; use service names or the CoreDNS address inside the distro, and ensure the docker CLI runs natively in WSL2. Apple Silicon (ARM64): x86-only tooling running under Rosetta 2 can hit glibc resolver quirks; pin the CoreDNS image to an arm64 manifest.

Rollback

#!/usr/bin/env bash
set -euo pipefail
docker compose down
# Remove the coredns service and dns: blocks from docker-compose.yml, then:
docker compose up -d --wait