Skip to content

Fix: Docker Compose Services Can't Connect to Each Other

FixDevs ·

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 [::]:6379

Or 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 fails

Or 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 network for all services in a docker-compose.yml file. Services communicate using their service name as the hostname, not localhost or the host IP. Common causes of connection failures:

  • Using localhost instead of the service name — inside a container, localhost refers to that container itself, not other containers. Use db not localhost:5432.
  • Port mapping confusionports: "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: host may not be on the same Docker network.
  • Service not ready when another tries to connectdepends_on only 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.

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-alpine

The 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 localhost in their app configuration because it works locally without Docker. When running in Docker Compose, localhost inside the app container refers to the app container’s own network interface — not the db container.

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 5433
services:
  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/mydb

The 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:16

Use 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:6379

condition: 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_default

If 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 5432

Fix 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:8080

host.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:8080

Fix 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 network

Both 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 REDIS

Inspect 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-resolvable

If 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 everything

For related Docker issues, see Fix: Docker Container Keeps Restarting and Fix: Docker Compose Env Not Loaded.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles