Fix: Docker Compose Services Can't Connect to Each Other
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix Docker Compose networking issues — services can't reach each other by hostname, port mapping confusion, network aliases, depends_on timing, and host vs container port differences.
The Error
One Docker Compose service can’t connect to another:
Error: connect ECONNREFUSED 127.0.0.1:5432
Error: getaddrinfo ENOTFOUND postgres
connect: connection refused [::]:6379Or the connection works for external access but not between services:
# You can reach http://localhost:3000 from your browser
# But from inside the app container, http://localhost:8080 failsOr after adding a service to docker-compose.yml, it can’t be reached by the other services.
Why This Happens
Docker Compose creates a private bridge network for every project. When you run docker compose up, the CLI creates a network named <project>_default (or a custom name if you set one) and attaches every service to it unless you override that with explicit networks: blocks. Each service gets a DNS name equal to its service key in the YAML, plus an alias for any extra hostnames you configure. Containers reach each other by service name on the container’s listening port, never on the host port from a ports: mapping.
The reason localhost does not work between containers is that each container has its own network namespace. Inside the container, localhost resolves to the container’s own loopback interface — not the host machine, and not the other containers in the same Compose project. Two containers in the same network are peers on a virtual L2 segment; they reach each other by IP or by name, but never through the host loopback. This mental model is what every fix below depends on.
Connectivity failures come from a small set of causes:
- Using
localhostinstead of the service name — inside a container,localhostrefers to that container itself, not other containers. Usedbnotlocalhost:5432. - Port mapping confusion —
ports: "5432:5432"exposes the container port to the host. Between containers on the same Compose network, use the container port directly without mapping. - Service not on the same network — services defined in different Compose files or using
network_mode: hostmay not be on the same Docker network. - Service not ready when another tries to connect —
depends_ononly waits for the container to start, not for the application inside to be ready. - Custom networks not shared — if you define custom networks and not all services are on the same one, they can’t reach each other.
- DNS resolution failure — Docker’s internal DNS resolves service names, but misconfigured networking can break this.
Version History That Changes the Failure Mode
The tooling around docker-compose has changed significantly, and old tutorials lead you astray. The original docker-compose v1 was a Python project installed separately from Docker. It read docker-compose.yml, parsed it against a versioned schema (version: "2", version: "3"), and orchestrated containers through the Docker API. Docker formally deprecated v1 in July 2023 and stopped publishing new releases. If you see docker-compose (with a hyphen) in your docs and the command behaves oddly, you may be running the legacy Python tool.
Docker Compose v2, which ships as a Go-based CLI plugin invoked via docker compose (space, not hyphen), is the current version and the only one that receives updates. The on-disk YAML format is the same, but the project ignores the version: field at the top of the file. The format is now governed by the open Compose Spec, which is implemented by Compose v2, Kubernetes-flavored tools like kompose, and various third-party orchestrators. If a network setting works in v2 but produces a “Unsupported config option” error in someone else’s environment, they may still be on v1.
Compose v2.20 (mid-2023) and later changed how depends_on, extends, and YAML anchors interact with network inheritance. In v2.20+, services that extend another file’s definition inherit networks more conservatively to avoid silent leakage of internal services into the default network. If you have extends: blocks that worked on v2.18 and broke on v2.21, double-check the resulting networks: list with docker compose config. The default bridge network (the one created when you run docker run without --network) still does not provide automatic DNS — service-name resolution only works on user-defined bridge networks, which Compose creates for you.
Fix 1: Use the Service Name as the Hostname
The service name in docker-compose.yml is the hostname other services use to connect:
services:
app:
image: myapp
environment:
# WRONG — localhost is the app container itself
DATABASE_URL: postgres://user:pass@localhost:5432/mydb
# CORRECT — use the service name
DATABASE_URL: postgres://user:pass@db:5432/mydb
# CORRECT — use the service name for Redis too
REDIS_URL: redis://cache:6379
db:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
cache:
image: redis:7-alpineThe hostname db resolves to the db container’s IP address on Docker’s internal network. The hostname cache resolves to the cache container.
Common Mistake: Developers hardcode
localhostin their app configuration because it works locally without Docker. When running in Docker Compose,localhostinside theappcontainer refers to theappcontainer’s own network interface — not thedbcontainer.
Fix 2: Understand Port Mapping vs Container Ports
ports in Compose maps container ports to host ports — for access from outside Docker (your browser, curl). Between services on the same Compose network, use the container port directly:
services:
db:
image: postgres:16
ports:
- "5433:5432" # Maps host:5433 → container:5432
# From your HOST machine: psql -h localhost -p 5433
# From OTHER CONTAINERS: postgres://db:5432 ← container port, not 5433services:
app:
environment:
# WRONG — 5433 is the host port, not the container port
DATABASE_URL: postgres://user:pass@db:5433/mydb
# CORRECT — use the container port (5432 is what PostgreSQL listens on inside the container)
DATABASE_URL: postgres://user:pass@db:5432/mydbThe port mapping only matters for host-to-container access. Container-to-container access always uses the container’s internal port.
Fix 3: Wait for Service Readiness
depends_on ensures the container starts before dependent services, but it doesn’t wait for the application inside to be ready:
# This starts db before app, but app might try to connect before Postgres is ready
services:
app:
depends_on:
- db # Waits for container start, not for Postgres to accept connections
db:
image: postgres:16Use health checks for proper readiness waiting:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s # Give Postgres time to initialize before health checks start
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 3
app:
image: myapp
depends_on:
db:
condition: service_healthy # Waits until db health check passes
cache:
condition: service_healthy
environment:
DATABASE_URL: postgres://postgres:secret@db:5432/mydb
REDIS_URL: redis://cache:6379condition: service_healthy only works if the dependency defines a healthcheck. Without it, Compose defaults to condition: service_started (container started, not app ready).
Fix 4: Verify the Network Configuration
By default, Compose creates one network for all services in the file and puts all services on it. Check the network setup:
# List networks
docker network ls
# Inspect which containers are on a network
docker network inspect <project_name>_default
# Example: if your project dir is "myapp", the network is "myapp_default"
docker network inspect myapp_defaultIf you define custom networks, include all relevant services:
services:
app:
networks:
- frontend
- backend # app is on both networks
db:
networks:
- backend # db is only on backend
nginx:
networks:
- frontend # nginx is only on frontend
networks:
frontend:
backend:If app is on frontend only and db is on backend only, they can’t reach each other. Both must share a network.
Confirm services can reach each other by running a test command:
# From inside the app container, ping the db container
docker compose exec app ping db
# Or test the actual port
docker compose exec app curl -s http://api:3000/health
docker compose exec app nc -zv db 5432Fix 5: Fix host.docker.internal for Host Machine Access
Sometimes you need a container to reach a service running on your host machine (not in Docker). Don’t use localhost — use host.docker.internal:
services:
app:
environment:
# Reach a service running on the host machine
EXTERNAL_API_URL: http://host.docker.internal:8080host.docker.internal is available on Docker Desktop (Mac and Windows). On Linux, add it manually:
services:
app:
extra_hosts:
- "host.docker.internal:host-gateway" # Linux only
environment:
EXTERNAL_API_URL: http://host.docker.internal:8080Fix 6: Connect to an External Docker Network
When containers are started by different Compose files (e.g., a shared infrastructure Compose and a per-project Compose), they’re on different networks by default and can’t reach each other.
Solution — use an external network:
# Create a shared network
docker network create shared_network# infrastructure/docker-compose.yml
services:
db:
image: postgres:16
networks:
- shared_network
networks:
shared_network:
external: true # Use the pre-created network# myapp/docker-compose.yml
services:
app:
image: myapp
environment:
DATABASE_URL: postgres://user:pass@db:5432/mydb
networks:
- shared_network
networks:
shared_network:
external: true # Same pre-created networkBoth app and db are on shared_network, so app can resolve db as a hostname.
Fix 7: Debug DNS Resolution Inside a Container
If you’re unsure whether DNS resolution is working, run commands inside the container:
# Test DNS resolution
docker compose exec app nslookup db
docker compose exec app getent hosts db
# Test connectivity
docker compose exec app curl -v http://db:5432
docker compose exec app ping -c 3 db
# Check environment variables (confirm DATABASE_URL uses the right hostname)
docker compose exec app env | grep DATABASE
docker compose exec app env | grep REDISInspect the container’s network configuration:
docker inspect <container_name> | grep -A 20 '"Networks"'This shows the container’s IP address and which networks it’s connected to.
Still Not Working?
Check for network mode conflicts. If a service uses network_mode: host, it shares the host’s network stack and is not on the Compose network. It can’t be reached by service name from other containers:
# This service is NOT on the Compose network
service_a:
network_mode: host # Bypasses Docker networking
# service_b cannot connect to service_a using hostname
service_b:
environment:
URL: http://service_a:3000 # Fails — service_a is not DNS-resolvableIf you need network_mode: host for one service, the other services must use the host’s IP (host.docker.internal on Mac/Windows, or the host’s actual IP on Linux).
Check for conflicting port bindings. If two services both try to bind to the same host port, the second one fails to start:
docker compose logs service_b | grep "address already in use"Recreate the network. Sometimes Docker networks get into a bad state. Stop everything, remove the network, and start fresh:
docker compose down -v # Stop and remove containers and volumes
docker network prune # Remove unused networks
docker compose up -d # Recreate everythingCheck for stale DNS inside the container. Some application runtimes cache DNS lookups longer than the container exists. Java’s networkaddress.cache.ttl defaults to 30 seconds in modern JREs but to indefinite in older ones. Node.js caches lookups inside the same process. If a service started before its dependency, you may get a cached NXDOMAIN. Restart the dependent container after the dependency reaches a healthy state, or set dns_search: and a short application-level TTL.
Verify the Compose project name. Networks are scoped per project, and the project name defaults to the directory name unless you set COMPOSE_PROJECT_NAME or use the -p flag. Running docker compose up from /srv/app creates app_default, while running it from /home/dev/app creates a separate app_default under a different project. Containers from the two projects cannot resolve each other unless they share an external network. Use docker compose ls to see exactly which projects exist and what their networks are named.
Look at iptables on Linux hosts. Docker programs the host firewall via iptables (or nftables on newer systems). If you have a custom firewall manager — ufw, firewalld, or a Kubernetes CNI plugin — it can clobber Docker’s rules and silently drop inter-container traffic on the bridge. Restart the Docker daemon after firewall changes and confirm the DOCKER-USER chain still contains your expected accept rules.
For related Docker issues, see Fix: Docker Container Keeps Restarting, Fix: Docker Compose Env Not Loaded, Fix: Docker Compose depends_on Not Working, and Fix: Docker Compose Healthcheck Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Docker Secrets Not Working — BuildKit --secret Not Mounting, Compose Secrets Undefined, or Secret Leaking into Image
How to fix Docker secrets — BuildKit secret mounts in Dockerfile, docker-compose secrets config, runtime vs build-time secrets, environment variable alternatives, and verifying secrets don't leak into image layers.
Fix: Docker Compose Healthcheck Not Working — depends_on Not Waiting or Always Unhealthy
How to fix Docker Compose healthcheck issues — depends_on condition service_healthy, healthcheck command syntax, start_period, custom health scripts, and debugging unhealthy containers.
Fix: docker-compose.override.yml Not Working — Override File Ignored or Not Merged
How to fix docker-compose.override.yml not being applied — file naming, merge behavior, explicit file flags, environment-specific configs, and common override pitfalls.
Fix: Docker Build ARG Not Available — ENV Variables Missing at Runtime
How to fix Docker ARG and ENV variable issues — build-time vs runtime scope, ARG before FROM, multi-stage build variable passing, secret handling, and .env file patterns.