Why Your Containers Can't Talk to Each Other

by Eric Hanson, Backend Developer at Clean Systems Consulting

The connection error you can't reproduce on your machine

Your Docker Compose stack has an app service and a database service. Locally, it works fine. In CI, the app consistently fails to connect to the database on the first attempt. Or: you have two separate Docker Compose projects and you've connected them to the same network, but service A still can't reach service B. Or: your service container connects to the database by IP address and it works until you restart the database container, which gets a new IP.

These are the four categories of inter-container communication failures:

  1. Wrong hostname
  2. Wrong or mismatched network
  3. Service binding to the wrong interface
  4. IP address hardcoding

Each has a specific diagnosis path.

Category 1: wrong hostname

Inside a Docker network, containers are reachable by their container name or (in Compose) their service name. If your application's connection string references localhost, 127.0.0.1, or the host machine's IP, it won't reach another container.

Diagnosis:

# From inside the failing container, test DNS resolution
docker exec -it your-app-container nslookup postgres
docker exec -it your-app-container ping -c 3 postgres

If nslookup postgres returns the database container's IP, DNS works. If it fails with "Name or service not known," the containers aren't on the same network, or the service name doesn't match.

Fix: update the connection URL in your application config to use the service name:

# Wrong
DATABASE_URL=jdbc:postgresql://localhost:5432/mydb
DATABASE_URL=jdbc:postgresql://127.0.0.1:5432/mydb

# Right — 'postgres' is the Compose service name or container name
DATABASE_URL=jdbc:postgresql://postgres:5432/mydb

Category 2: containers on different networks

Docker containers on different networks cannot communicate by default. This is a common issue when:

  • You have multiple Compose projects and want services from one to reach services from another
  • You started containers with docker run without specifying a network, so they landed on the default bridge network

Diagnosis:

# List all networks
docker network ls

# Inspect which containers are on which network
docker network inspect your-network-name

# Check what networks a specific container is on
docker inspect your-container-name | jq '.[0].NetworkSettings.Networks | keys'

Fix for cross-project Compose communication: create an external network and attach both projects to it.

docker network create shared-network

In each docker-compose.yml:

networks:
  shared-network:
    external: true

services:
  service-a:
    networks:
      - shared-network
      - default   # still on the default project network

With external: true, Compose connects the service to the pre-existing shared-network without trying to create or manage it. Services from both projects on shared-network can reach each other by container name.

Note: in cross-project scenarios, the DNS name is the container name (which Compose generates as {project-name}-{service-name}-1), not just the service name. This is a common gotcha.

# Find the actual container name
docker ps --format "table {{.Names}}"
# Example: myproject-app-1, myproject-db-1

Category 3: service binding to the wrong interface

A service inside a container listens on a network interface. If it listens on 127.0.0.1 (loopback only), it's not reachable from other containers even on the same Docker network. The service must listen on 0.0.0.0 to be reachable from outside the container.

This is a common issue with:

  • Go HTTP servers that default to localhost:port instead of :port
  • Applications configured with explicit bind addresses for security reasons
  • Some databases configured to listen on localhost only

Diagnosis from another container:

# Telnet/nc to test connectivity (if nc is available)
docker exec -it app-container nc -zv database-service 5432

# If you can get a shell in the database container:
docker exec -it db-container ss -tlnp | grep 5432
# Look for '0.0.0.0:5432' vs '127.0.0.1:5432'

If the service shows 127.0.0.1:5432 rather than 0.0.0.0:5432, it's only reachable from within that container.

Fix: configure the service to listen on all interfaces. For PostgreSQL:

services:
  db:
    command: postgres -c listen_addresses='*'

For your own Go/Java/Python service: change the bind address from localhost to 0.0.0.0 or just use the port without a hostname:

// Go — wrong
http.ListenAndServe("localhost:8080", handler)

// Go — right: listens on all interfaces
http.ListenAndServe(":8080", handler)
http.ListenAndServe("0.0.0.0:8080", handler)

Category 4: hardcoded IP addresses

Container IP addresses change when containers are recreated. If your application connects to another service using a hardcoded IP (from docker inspect), it works until anything is restarted.

# This IP is temporary — don't hardcode it
docker inspect db-container | grep IPAddress
# "IPAddress": "172.18.0.2"

Fix: always use service names or container names, never IPs. Docker's internal DNS handles resolution. If you need the IP programmatically for some reason, re-resolve it at runtime rather than storing it.

Systematic diagnosis workflow

When inter-container communication fails, work through this sequence:

# 1. Confirm both containers are running
docker ps | grep -E "container-a|container-b"

# 2. Confirm they're on the same network
docker network inspect $(docker network ls -q) | grep -A5 '"Name"'

# 3. Test DNS from inside the failing container
docker exec -it failing-container nslookup target-service

# 4. Test TCP connectivity
docker exec -it failing-container timeout 5 bash -c 'echo > /dev/tcp/target-service/8080' \
  && echo "connected" || echo "failed"

# 5. Check the target service's bind address
docker exec -it target-container ss -tlnp

If step 3 fails: networks are mismatched or service names don't match. If step 3 succeeds but step 4 fails: the service is bound to loopback only. If both succeed: the problem is in the application (auth, protocol mismatch, TLS).

The fast path for Compose setups

In Compose, networking is supposed to be automatic. If it's not working:

  1. Are both services in the same docker-compose.yml? Same network by default → service names resolve.
  2. Are services in different Compose files/projects? Different networks by default → need an external network.
  3. Is the ports: mapping present but the application still can't connect internally? ports: is for host access, not required for container-to-container — remove it from the diagnosis path.
  4. Did you use network_mode: host on one service? That service is on the host network, not the Compose network.

docker compose config shows the fully resolved configuration including network assignments — check it when the Compose setup doesn't behave as expected.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Java Thread Management — Why ExecutorService Exists and How to Use It Well

Creating threads directly is expensive, uncontrolled, and hard to shut down cleanly. ExecutorService solves all three problems — but its default configurations have tradeoffs that matter in production.

Read more

Questions to Ask Before Starting a Backend Project

“We just need an API… should be quick, right?” That sentence has started more fragile backend systems than anyone admits.

Read more

Technical Debt Is Not Always Bad. Unmanaged Technical Debt Is.

Technical debt is a deliberate tool that enables faster delivery in the short term at the cost of slower delivery later. Like financial debt, the problem is not taking it on — it is losing track of what you owe.

Read more

Handling Clients Who Think You’re a 24/7 Developer

It starts with a “quick message” at night. Then suddenly, you’re expected to reply at 2 AM like it’s normal.

Read more