What this usually means
Docker Compose creates a default bridge network for services defined in the same compose file, letting them resolve each other by service name. When connectivity fails, it's usually because the target container is on a different network (explicit or implicit), the service name is misspelled, the port is bound to 127.0.0.1 inside the container, or the target container never started. Rarely, it's a Docker DNS issue or a firewall rule blocking inter-container traffic. The default DNS resolution works via embedded DNS at 127.0.0.11 inside each container.
The first ten minutes — establish facts before touching code.
- 1docker-compose ps -a — check if all services are up and not exited
- 2docker-compose logs <failing-service> | tail -50 — look for connection errors
- 3docker exec -it <container-id> sh — shell into the failing container
- 4cat /etc/hosts — verify the service hostname is not present (it shouldn't be; DNS does it)
- 5getent hosts <target-service> — test DNS resolution inside the container; should return an IP
- 6curl -v http://<target-service>:<port> — check if TCP connection works
- 7docker network ls && docker inspect <container> | jq '.[].NetworkSettings.Networks' — verify containers share a network
The specific files, logs, configs, and dashboards that usually own this bug.
- searchdocker-compose.yml — networks section and service-level network aliases
- search/etc/hosts inside the container — should NOT contain service names (DNS handles it)
- searchdocker-compose logs --tail=50 — real-time logs from all services
- searchdocker network inspect <network-name> — shows container IPs and endpoints
- searchApplication configuration files (e.g., .env, config.js, settings.py) — hostname/port values
- searchDocker daemon logs (/var/log/docker.log or journalctl -u docker) — rare DNS issues
- searchFirewall rules on the host (iptables -L -n) — check FORWARD chain and Docker rules
Practical causes, not theory. These are the things you will actually find.
- warningService name mismatch in code vs. defined in docker-compose.yml
- warningServices not on the same Docker network (explicitly defined networks vs default 'bridge')
- warningTarget service binds to 127.0.0.1 instead of 0.0.0.0 (common in dev servers like Flask, Django, Node)
- warningPort conflict: container port is not exposed to the network (ports section missing or wrong mapping)
- warningDNS caching inside the container (rare, but possible with custom DNS settings)
- warningRunning docker-compose up with --force-recreate but network isolation persists
- warningUsing links (deprecated) instead of networks, or misconfigured network aliases
Concrete fix directions. Pick the one that matches your root cause.
- buildEnsure all services are under the same networks: block in docker-compose.yml, or rely on default network
- buildChange application bind address to 0.0.0.0 or :: (example: Flask app.run(host='0.0.0.0'))
- buildExplicitly expose the container port with expose: or ports: in docker-compose.yml (ports: '8080:8080')
- buildUse service name as hostname (exactly as defined in compose file), not container name or IP
- buildRemove any deprecated links: and use networks: with aliases if needed
- buildRestart Docker daemon or run docker-compose down && docker-compose up to clear stale state
- buildAdd healthcheck to dependent services and use depends_on with condition: service_healthy
A fix you cannot prove is a guess. Close the loop.
- verifiedFrom inside container: curl http://<service-name>:<port> — expect HTTP 200 or expected response
- verifiedFrom inside container: ping -c 2 <service-name> — expect replies
- verifieddocker-compose exec <service-a> sh -c 'wget -qO- http://<service-b>:<port>/health' — check health endpoint
- verifieddocker network inspect <network> --format '{{range .Containers}}{{.Name}} {{end}}' — confirm both containers appear
- verifiedCheck application logs for successful connection after the fix
- verifiedRun a synthetic test (e.g., curl to an API endpoint that calls the other service) and confirm no errors
Things that make this bug worse or harder to find.
- warningRelying on container names or IP addresses from docker inspect — they can change on restart
- warningUsing localhost inside a container to reach another container — localhost refers to the same container
- warningSetting network_mode: 'host' — removes network isolation and service DNS resolution
- warningForgetting to rebuild the image after changing application bind address — old image still binds to 127.0.0.1
- warningAdding a custom dns: option that overrides the embedded DNS (127.0.0.11)
- warningAssuming depends_on waits for the service to be ready — it only waits for container start, not health
Frontend can't reach backend container in Docker Compose
Timeline
- 09:15Deploy new version of frontend service (React app) and backend (Express API)
- 09:20Users report 502 Bad Gateway errors on login page
- 09:25Check frontend logs: 'connect ECONNREFUSED 127.0.0.1:3001'
- 09:27docker-compose ps — all services 'Up' including backend (api:3001)
- 09:30docker exec -it frontend sh, then curl http://api:3001 — connection refused
- 09:35Check api logs: 'Server running on port 3001' — no incoming connections
- 09:40docker exec -it api sh, then netstat -tlnp — port 3001 listening on 127.0.0.1
- 09:45Check api app.js: app.listen(3001, '127.0.0.1') — binds to loopback only
- 09:50Change to app.listen(3001, '0.0.0.0'), rebuild and restart
- 09:55Verify: curl http://api:3001 from frontend returns JSON — issue resolved
We had a Node.js Express API that was working fine in development when accessed via the host's localhost. The code had app.listen(3001, '127.0.0.1') which is a common security practice to avoid exposing the server on all interfaces. But inside Docker, each container has its own loopback interface, so binding to 127.0.0.1 means the server only accepts connections from within the same container. Other containers trying to reach it via the service name 'api' would get connection refused because the network packets arrive on the container's eth0 interface, not lo.
When the frontend container tried to connect to 'api:3001', it resolved the IP via Docker DNS to the internal bridge network IP (e.g., 172.18.0.3). That IP is not 127.0.0.1, so the Express server refused it. The 'api' container logs showed 'Server running on port 3001' but no actual connections because the bind failed silently. I wasted time checking firewall rules and network configurations before thinking to inspect what address the server was actually listening on.
The fix was trivial: change 127.0.0.1 to 0.0.0.0 in the listen call. But we had to rebuild the image because the code change required a new build. After restarting the stack, the frontend could reach the API. I added a health endpoint and healthcheck to the compose file to catch this earlier next time. The lesson: always bind to 0.0.0.0 inside containers unless you have a specific reason not to, and use netstat or ss inside the container to verify listening addresses as part of deployment checks.
Root cause
Express server bound to 127.0.0.1 only, rejecting connections from other containers on the Docker network.
The fix
Changed app.listen(3001, '0.0.0.0') in the API source, rebuilt the image, restarted the stack.
The lesson
Always verify the listening address inside a container (netstat -tlnp) when inter-container connectivity fails. Default to 0.0.0.0 for containerized services.
Docker Compose creates a default bridge network (named <project>_default) for all services defined in the same docker-compose.yml file. Each container gets an IP on this network and DNS resolution via the embedded DNS server at 127.0.0.11. Service names defined in the compose file become hostnames that resolve to the container's IP. This is the key difference from using docker run, where containers are isolated unless you manually connect them.
The default network is sufficient for most cases. However, if you define custom networks in the compose file and do not assign a service to a network, it will only be on the default network. Services on different custom networks cannot communicate unless you explicitly connect them or use network aliases. Also, the default network does not support container discovery across different compose files unless you use external networks.
A frequent mistake is assuming 'localhost' inside a container refers to the host machine or other containers. It does not—localhost is the container's own loopback interface. To reach another container, you must use its service name (or hostname if defined) as defined in docker-compose.yml. Similarly, using container names (like docker-compose ps shows) may not work unless the container is on the same network and DNS is configured properly. The service name is the reliable hostname.
Another pitfall is hardcoding IP addresses from docker inspect. Containers can restart and get new IPs. Always use service names. Also, if you use depends_on, it only ensures the container is started, not that the service is ready. Use healthchecks with depends_on condition: service_healthy to wait for the service to be accepting connections.
Start with docker-compose ps to confirm all services are running. Then check logs with docker-compose logs <service>. If logs show connection errors, exec into the failing container: docker exec -it <container-id> sh. Inside, run getent hosts <target-service> to see if DNS resolves. If it resolves but curl fails, the target service might not be listening on the expected interface or port.
Inside the target container, run ss -tlnp or netstat -tlnp to see which addresses and ports the service is listening on. If it shows 127.0.0.1:port, you need to change the bind address to 0.0.0.0. If the port is missing entirely, the service may not have started correctly or is not exposing the port in the compose file. Also, verify the network with docker network inspect <network> to see both containers are connected.
When services need to communicate across multiple docker-compose projects (e.g., a shared database), you must define an external network in each compose file and connect the services to it. For example, create a network with docker network create shared-net, then in each compose file declare networks: shared-net: external: true and assign the network to the services. Service names from one project are not automatically resolvable in another; you may need to use network aliases or a DNS service like Consul.
Another advanced scenario: using network_mode: 'service:<name>' to share a container's network stack (e.g., for sidecar proxies). This removes network isolation and can cause port conflicts. Use it sparingly. For most cases, stick to the default network or custom bridge networks.
Binding to 0.0.0.0 inside a container is safe because the container's ports are only exposed to other containers on the same Docker network, not directly to the host's network unless you publish ports with ports: in compose. The default bridge network is isolated. However, if you publish ports (e.g., '3001:3001'), the service becomes accessible on the host's IP. If you want extra security inside the container, you can bind to 127.0.0.1 but then you must ensure that only processes within the same container need to connect. For inter-container communication, always use 0.0.0.0.
Also, avoid setting network_mode: 'host' unless you fully understand the implications: the container shares the host's network stack, losing network isolation and the embedded DNS. Service name resolution breaks, and you must use host ports directly. This is rarely needed and often causes more problems.
Frequently asked questions
Why can't I ping another service by its service name from inside a container?
Docker Compose's embedded DNS resolves service names to container IPs, but ping uses ICMP which may be blocked by Docker's default network rules. Use curl, wget, or netcat to test TCP connectivity instead. If ping fails but curl works, it's not a DNS issue. To test DNS, use getent hosts <service-name>.
My service is listening on 0.0.0.0 but I still get connection refused. What else could be wrong?
Check that the target container is actually running and the service is up. Use docker-compose ps. Also verify the port number: the connecting service might be using a different port than what the target is listening on. Inside the target container, run ss -tlnp to confirm the port and address. Also ensure both containers are on the same network with docker network inspect.
Do I need to expose ports in docker-compose.yml for inter-container communication?
No. The ports directive publishes ports to the host, but inter-container communication within the same Docker network does not require published ports. You only need to expose (expose: or the application listening on a port) the port inside the container. The service name resolves to the container IP, and the port is accessible directly. However, if the service is on a different network, you may need to publish or use an external network.
My containers can communicate after docker-compose up but stop working after a restart. Why?
This usually happens when you rely on container names or IPs that change on restart. Always use service names as hostnames. Also, ensure your application's configuration does not cache DNS lookups indefinitely. If you use a custom DNS server, it might not have the service names. Finally, check that depends_on is not causing a race condition; use healthchecks to ensure the target service is ready before the dependent service tries to connect.
What is the difference between links and networks in Docker Compose?
Links are a legacy feature that connect containers and inject environment variables with IPs and hostnames. Networks are the modern way: they provide DNS resolution and network isolation. Links are deprecated and do not support features like network aliases or external networks. Always use networks instead. If you see links in an old compose file, migrate to networks.