Docker Compose Networking: How Service Name DNS Resolution Works
Docker Compose creates a default bridge network where each service's name is registered as a DNS hostname. Containers reach each other by service name, not by IP or localhost. Understanding how Docker's embedded DNS works — and when it doesn't — prevents the most common Docker Compose connectivity bugs.
How Docker Compose DNS works
When Docker Compose starts a project, it creates a default network named <project-name>_default. All services defined in docker-compose.yml are automatically attached to this network. Docker's embedded DNS server registers each service's name as a resolvable hostname on the network.
services:
app:
image: my-app
environment:
DB_HOST: postgres # resolves to the postgres container's IP
REDIS_URL: redis://redis:6379
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
redis:
image: redis:7
When app connects to DB_HOST=postgres, Docker's DNS resolves postgres to the current IP of the postgres container. If postgres restarts and gets a new IP, the DNS record updates automatically — no hardcoded IPs needed.
localhost inside a container never reaches another container — each has its own network namespace
GotchaDocker NetworkingContainer network isolation means each container has a separate loopback interface. localhost (127.0.0.1) inside container A is container A's loopback only — it doesn't reach container B or the host machine. Cross-container connectivity requires using the service name as the hostname.
Prerequisites
- Linux network namespaces
- DNS resolution
- Docker bridge networking
Key Points
- localhost in a container refers to the container itself — not the host, not other containers.
- Service names are DNS hostnames on the Compose-created network, resolving to container IPs.
- Docker's DNS updates dynamically when containers restart and get new IPs.
- The default bridge network (docker0) does NOT support DNS by service name — only user-defined networks do.
Service name resolution in practice
services:
app:
image: my-app
environment:
# These all work — service names resolve to container IPs
DB_HOST: postgres
CACHE_HOST: redis
QUEUE_HOST: rabbitmq
# This does NOT work — localhost is container A's own loopback
# DB_HOST: localhost # WRONG
Connecting from inside a container to verify DNS works:
# Shell into the app container
docker exec -it app-container bash
# Check DNS resolution
nslookup postgres
# Server: 127.0.0.11 (Docker's embedded DNS)
# Address: 127.0.0.11#53
# Name: postgres
# Address: 172.18.0.3
# Test connectivity
curl -v http://postgres:5432
nc -zv redis 6379
127.0.0.11 is Docker's embedded DNS resolver — it's present in every container on a user-defined network.
Custom networks and multi-network setups
Compose creates one default network. You can define additional networks for service isolation:
services:
web:
image: nginx
networks:
- frontend
api:
image: my-api
networks:
- frontend
- backend # api can reach both web and db
db:
image: postgres:16
networks:
- backend # db is NOT reachable from web
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # no external internet access from this network
web and db are on separate networks and cannot reach each other directly. api bridges them. internal: true on backend prevents containers on that network from reaching the internet — good for database networks that shouldn't have outbound access.
📝Extra hosts and custom DNS resolution
For cases where you need to override DNS resolution or add entries not available through Docker's DNS:
services:
app:
image: my-app
extra_hosts:
- "legacy-api:192.168.1.100" # adds to /etc/hosts
- "host.docker.internal:host-gateway" # reach the Docker host machine
host.docker.internal is a special hostname that resolves to the Docker host's IP from inside a container. On Linux, it requires host-gateway as the IP value. On macOS and Windows (Docker Desktop), it resolves automatically without extra configuration.
For accessing services running on the host machine during development:
services:
app:
image: my-app
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
EXTERNAL_API: http://host.docker.internal:3000
This is useful when you have a service running locally (e.g., a hot-reload dev server) that the containerized app needs to reach.
A Docker Compose app service tries to connect to a database using DB_HOST=localhost and gets 'connection refused'. The postgres service is running and healthy. What is wrong?
easyBoth services are defined in the same docker-compose.yml. The postgres service starts before app (depends_on). The app container is running.
AThe postgres container needs to expose port 5432 in the docker-compose.yml
Incorrect.The ports directive exposes a container's port to the host machine. Inter-container communication on the same Docker network doesn't require ports to be exposed — it works on the container's internal port directly.Blocalhost inside the app container refers to the app container's own loopback interface, not the postgres container — use DB_HOST=postgres (the service name) instead
Correct!Each container has its own network namespace and loopback. localhost (127.0.0.1) inside the app container is the app container's own loopback — no postgres listener is bound there. Docker Compose registers each service name as a DNS hostname on the shared network. DB_HOST=postgres resolves to the postgres container's IP address on the Compose network.Cdepends_on doesn't guarantee postgres is ready to accept connections when app starts
Incorrect.This is also true — depends_on waits for the container to start, not for postgres to be ready to accept connections. But it's a secondary issue; the primary problem is localhost not reaching the postgres container at all.DThe app service needs to be on the same network as postgres using an explicit networks: configuration
Incorrect.When no explicit networks are defined, Docker Compose adds all services to the default project network automatically. Both services are already on the same network.
Hint:What does localhost resolve to inside a container?