Configuring Local DNS for Microservice Routing
Fix non-deterministic DNS in local Docker Compose. Configure microservice routing that mirrors Kubernetes service discovery without extra external dependencies.
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
Standardize on a reserved, non-routable suffix (
.svc.cluster.localor a project-specific.docker.internal) across all Compose files to avoid mDNS/forwarder collisions.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 }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/16Override 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.11Validate 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
Corefileand thedns:overrides so resolution is reproducible on every clone. - Add a pre-flight DNS check to your
Makefileor bootstrap that fails fast when a service FQDN returnsNXDOMAINbefore integration tests run. - Validate the Compose schema in CI with
docker compose config --quietto catch malformeddns:directives before merge.
macOS (Docker Desktop):
.localis claimed by Bonjour; never use it for service routing. The injectedhost.docker.internalcan mask a broken internal resolver — test with explicit FQDNs. WSL2: Thelocalhostresolver bypasses Docker's embedded DNS; use service names or the CoreDNS address inside the distro, and ensure thedockerCLI runs natively in WSL2. Apple Silicon (ARM64): x86-only tooling running under Rosetta 2 can hit glibc resolver quirks; pin the CoreDNS image to anarm64manifest.
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