Internal DNS #

One of Docker’s features often treated as trivial but absolutely crucial in container architecture is internal DNS. Thanks to it, containers can find each other just by service or container name — no need to know IP addresses, no hardcoding, no extra configuration.

Without internal DNS, building multi-container systems would be far more fragile. Every time a container restarts, its IP can change. Every time you scale, new IPs appear. Without stable service discovery, one small change can bring down the entire architecture.

Docker provides internal DNS that works automatically on user-defined networks. It’s embedded in the Docker Engine, super fast, and needs no manual configuration. This article covers internal DNS in depth: how it works, when it’s active, how it interacts with Docker Compose, and how to troubleshoot when name resolution fails.

What Is Docker Internal DNS? #

Docker internal DNS is a built-in DNS server running inside the Docker Engine. When a container makes a DNS query, it’s automatically directed to this server via the 127.0.0.11 resolver.

flowchart LR
    A[Container: app] -->|nslookup db| B[127.0.0.11]
    B -->|Check: db container on this network?| C{Docker DNS}
    C -- Yes, found --> D[Return: 172.18.0.3]
    C -- No --> E[Forward to external DNS]
    E --> F[Google / Cloudflare DNS]
    F --> E
    E --> A

Main characteristics:

  • Automatic resolver — containers don’t need manual /etc/resolv.conf configuration.
  • Resolves container namesdb, api, web can be used as hostnames.
  • Resolves service names — in Compose, db refers to the db service.
  • Round-robin — for multiple containers with the same name, returns all IPs.
  • External forwarding — queries for public domains are forwarded to external DNS.
The resolver is 127.0.0.11. It’s an internal IP only reachable from inside the container. When a container runs cat /etc/resolv.conf, you’ll usually see nameserver 127.0.0.11. Don’t be confused — 127.0.0.11 is not the host’s localhost, but Docker’s embedded DNS inside the container namespace.

The Internal DNS Address: 127.0.0.11 #

Every container created with a modern network (user-defined) will have 127.0.0.11 as its resolver. How to see it:

docker run --rm --network app-net alpine cat /etc/resolv.conf

Output:

nameserver 127.0.0.11
options ndots:0
  • nameserver 127.0.0.11 — all DNS queries are directed here.
  • options ndots:0 — the minimum number of dots before a query is treated as an FQDN.
ndots:0 matters: it means Docker DNS will try to resolve short names (like db) directly, without appending a domain suffix. That’s what makes short-name service discovery work. With a higher ndots, db might be treated as relative and get a domain suffix appended first.

When Is Internal DNS Active? #

Docker internal DNS only works optimally on user-defined networks. This is one of the biggest differences between the default bridge and user-defined bridges.

Network TypeInternal DNS?Container name resolution?
Default bridge (bridge)❌ None❌ No (must use IP or --link)
User-defined bridge✅ Yes✅ Yes
Host network❌ No❌ No (uses the host’s resolv.conf)
None network❌ No❌ No
Overlay (Swarm)✅ Yes✅ Yes (in the Swarm scope)
Custom network pluginsDepends on the pluginDepends on the plugin
The most common trap: Developers run containers on the default bridge, then wonder why ping db fails or curl http://api:8080 errors. This isn’t a bug — it’s by design. The default bridge has no internal DNS. The solution: docker network create my-net and run the containers on that network.

Experiment: Default vs User-Defined #

# Default bridge - DNS inactive
docker run -d --name db --network bridge postgres
docker run -it --rm --network bridge alpine sh
/ # nslookup db
# nslookup: can't resolve 'db'
# (fails)

# User-defined network - DNS active
docker network create app-net
docker run -d --name db --network app-net postgres
docker run -it --rm --network app-net alpine sh
/ # nslookup db
# Server:    127.0.0.11
# Address:   127.0.0.11:53
# Non-authoritative answer:
# Name: db
# Address: 172.18.0.2
# (works!)

The DNS Resolution Flow: Step by Step #

What happens when a container queries db:

sequenceDiagram
    participant App as Application (container)
    participant R as /etc/resolv.conf
    participant DNS as 127.0.0.11
    participant E as External DNS

    App->>R: gethostbyname("db")
    R->>DNS: UDP query 127.0.0.11:53
    DNS->>DNS: Check: "db" container on this network?
    alt Container found
        DNS-->>App: Return 172.18.0.3
    else Container not found
        DNS->>E: Forward to 8.8.8.8
        E-->>DNS: Response (or NXDOMAIN)
        DNS-->>App: Forward response
    end

The interesting part: as long as the container and service are on the same network, resolution succeeds with very low latency (sub-millisecond). No external DNS hop, no internet round-trip.

flowchart TB
    subgraph Internal["Local Resolution (instant)"]
        A[app nslookup db] --> B[127.0.0.11]
        B --> C[Return container IP]
    end

    subgraph External["External Resolution (forward)"]
        D[app nslookup google.com] --> E[127.0.0.11]
        E --> F[Forward to 8.8.8.8]
        F --> G[Return google IP]
    end

When does Docker DNS fail?

  • Containers on different networks (no connectivity).
  • Containers with the same name on different networks (ambiguous).
  • Typos in the service name.
  • aliases not set correctly in Compose.
  • links (legacy), which is deprecated — don’t use it.

Round-Robin for Multi-Container Setups #

When one service runs as multiple containers (e.g. api-1, api-2, api-3), Docker DNS can resolve all IPs at once for round-robin load balancing.

# Start 3 containers with the same name (alias)
docker network create --driver bridge scale-net

docker run -d --name api-1 --network scale-net --network-alias api my-api
docker run -d --name api-2 --network scale-net --network-alias api my-api
docker run -d --name api-3 --network scale-net --network-alias api my-api

# Query from another container
docker run -it --rm --network scale-net alpine sh
/ # nslookup api
# Name: api
# Address: 172.18.0.2
# Address: 172.18.0.3
# Address: 172.18.0.4
# Docker Compose - scale
services:
  api:
    image: my-api
    networks:
      - app-net
    deploy:
      replicas: 3
flowchart LR
    Q[Container: client] -->|gethostbyname api| D[127.0.0.11]
    D --> R1[172.18.0.2]
    D --> R2[172.18.0.3]
    D --> R3[172.18.0.4]
    Q --> R1
    Q --> R2
    Q --> R3

Round-robin here is DNS-level — the client receives a list of IPs and usually implements its own load balancing. This differs from load balancing at the proxy level (Nginx, HAProxy), which is more advanced.

Important: round-robin DNS isn’t perfect load balancing. Clients may cache results and not truly round-robin. For production, it’s better to use a reverse proxy (Traefik, Nginx) or an orchestrator (K8s, Swarm) with smarter load balancing.

Internal DNS in Docker Compose #

Docker Compose automatically enables internal DNS based on service names. Every service resolves automatically.

version: "3.9"
services:
  web:
    image: nginx
  api:
    image: my-api
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/mydb
      REDIS_URL: redis://cache:6379
  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD: pass
  cache:
    image: redis

api can access db via db:5432 and cache via cache:6379without extra configuration. Compose automatically:

  • Creates one default network for the project.
  • Connects all services.
  • Enables internal DNS.

Extra Aliases with aliases #

services:
  db:
    image: postgres
    networks:
      default:
        aliases:
          - database
          - postgres-db
          - primary-db

Now db can be called: db, database, postgres-db, or primary-db. Useful for:

  • Migration — old services use the name database, new services use db. Use aliases for compatibility.
  • Abstraction — application code uses db (generic), but the underlying service can be swapped.
  • Multiple environmentsdev-db in development, prod-db in production.

External DNS Resolution #

Docker internal DNS also forwards queries for external (public) domains to upstream DNS. You can set custom DNS servers for all containers.

services:
  app:
    image: my-app
    dns:
      - 1.1.1.1
      - 8.8.8.8
    dns_search:
      - example.com
      - internal.local

Or set it at the daemon level (all containers):

// /etc/docker/daemon.json
{
  "dns": ["1.1.1.1", "8.8.8.8"],
  "dns-search": ["example.com"]
}
sudo systemctl restart docker
Be careful: if you set a custom DNS in the daemon and forget to restart Docker, new containers will still use the old setting. Restart the daemon every time you change daemon.json.

Verifying Internal DNS #

The most effective ways to confirm internal DNS is active:

# 1. Check resolv.conf
docker exec <container> cat /etc/resolv.conf
# Must contain nameserver 127.0.0.11

# 2. Check hostname resolution
docker exec <container> nslookup <target-container>
# or
docker exec <container> getent hosts <target-container>

# 3. Test connectivity via hostname
docker exec <container> ping -c 1 <target-container>
docker exec <container> wget -qO- http://<target-service>:8080

# 4. View the container's networks
docker inspect <container> --format '{{json .NetworkSettings.Networks}}'

Troubleshooting Failed DNS #

1. “Could not resolve host” #

Common causes:

  • Container on the default bridge.
  • Containers on different networks.
  • Typo in the name.
# Debug: check the container's networks
docker inspect <container> | grep -A 5 NetworkSettings

# Make sure they're on the same network
docker network connect app-net <container>

2. Very Slow DNS #

Common causes:

  • Forwarding to a slow external DNS.
  • ndots settings causing repeated queries.
# Check query time
docker exec <container> dig +stats <hostname>
# Look at "Query time"

# Set a faster DNS
docker run --dns 1.1.1.1 --dns 8.8.8.8 my-app

3. Intermittent Resolution Failures #

Common causes:

  • Container restarts with a new IP.
  • Overlay networks (Swarm) with quorum issues.
  • DNS caching in the application (Java’s DNS cache is famously stubborn).
# Test resolution repeatedly
docker exec <container> sh -c 'for i in 1 2 3 4 5; do nslookup db; done'

DNS Caching in Applications #

Some applications and runtimes have internal DNS caches that keep query results too long. This can cause problems when containers restart with new IPs.

RuntimeDefault TTLNotes
Go30 seconds (default)Fast enough
Java30+ seconds (caches forever until JVM restart)⚠️ Common problem
Node.js0 (no cache by default)Good
PythonLibrary-dependentVaries
PHP-FPM60 seconds (PHP built-in)Adequate
Java’s DNS cache is a famous trap: the JVM stores DNS results forever (or until the TTL setting is changed). For containerized Java apps, set -Dsun.net.inetaddr.ttl=30 or use another DNS resolver library (dnsjava). Without this, a container restart can leave the app stuck on the old IP.

Internal DNS Best Practices #

1. Always Use a User-Defined Network #

docker network create app-net
docker run --network app-net my-service

Without a user-defined network, internal DNS is inactive.

2. Use Service Names (Compose), Not Container Names #

# ✓ Solution: stable service name
services:
  api:
    image: my-api
    environment:
      DATABASE_HOST: db  # service name, stable

# ✗ Anti-pattern: fragile container name
services:
  db:
    container_name: my-database  # can't be scaled
    image: postgres

3. Don’t Hardcode IPs #

# ✗ Anti-pattern
DATABASE_HOST=172.18.0.3  # can change

# ✓ Solution
DATABASE_HOST=db  # DNS resolves

4. Set Consistent DNS #

// /etc/docker/daemon.json
{
  "dns": ["1.1.1.1", "8.8.8.8"]
}

Consistent external DNS helps reproducibility.

5. Test DNS Resolution in CI/CD #

# A health check that verifies DNS
docker run --rm --network app-net my-app \
  sh -c "getent hosts db && getent hosts cache"

Internal DNS in Swarm and Kubernetes #

Docker Swarm has its own, more advanced DNS service — Swarm Mode DNS. Services deployed in Swarm auto-resolve and get load-balanced.

Kubernetes has CoreDNS, running as a pod in the cluster. Service names resolve via Service objects. More advanced than Docker DNS — health checks, weighted load balancing, and service mesh integration.

flowchart TB
    subgraph Docker["Docker / Compose"]
        D1[127.0.0.11]
    end
    subgraph Swarm["Docker Swarm"]
        S1[Swarm DNS<br/>tasks.service_name]
    end
    subgraph K8s["Kubernetes"]
        K1[CoreDNS<br/>service.namespace.svc.cluster.local]
    end
    Docker --> Swarm --> K8s

Same concept, different implementations. For development, Docker DNS is enough. For multi-host production, Swarm or K8s provide more features.


Internal DNS vs Service Discovery Tools #

AspectDocker DNSConsuletcdK8s CoreDNS
Built-in✅ (default)
Multi-host❌ (except Swarm)
Health checks❌ (needs a library)
Load balancingBasic round-robin
Setup complexityZeroHighHighMedium
Best forSingle-host ComposeTraditional multi-hostPre-era K8s multi-hostKubernetes
For most cases, Docker DNS is enough. It’s free, automatic, and zero-config. Only consider Consul/etcd/CoreDNS when you need health-check-based routing, multi-host, or service mesh integration.

Summary #

  • Docker internal DNS is a DNS server embedded in the Docker Engine, accessed via 127.0.0.11. It resolves container and service names on user-defined networks.
  • Automatic resolver — containers get 127.0.0.11 in /etc/resolv.conf without manual configuration.
  • Only active on user-defined networks. The default bridge has NO internal DNS — this is the most common trap.
  • Round-robin — multi-containers with the same alias return all IPs; clients implement load balancing.
  • External queries are forwarded to upstream DNS (default: from the host). Overridable per container or at the daemon level.
  • Docker Compose automatically enables service-name-based internal DNS. Every service is reachable by its service name.
  • Aliases in Compose let one service have many names (for compatibility, abstraction, multiple environments).
  • Anti-patterns: using the default bridge, hardcoding IPs, using container_name in Compose, using --link (deprecated).
  • Troubleshooting: check /etc/resolv.conf, nslookup, getent hosts, and make sure containers are on the same network.
  • Java’s DNS cache is a famous trap — set a low TTL for containerized Java apps.
  • Best practices: always user-defined networks, use service names (not container_name), don’t hardcode IPs, test DNS resolution in CI/CD.
  • For multi-host production, consider Swarm (built-in) or K8s (CoreDNS). Docker DNS is enough for single-host Compose.

← Previous: Port Mapping & Exposure   Next: Container-to-Container Communication →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact