Cloud Explainers

How to Fix Docker Compose Containers That Can't Talk to Each Other

Docker9 min videoPublished Nov 9, 2026

Your app container can't reach your database container in Docker Compose: connection refused, or name not known. In our own lab (Docker 29 and Compose 2.40 on Ubuntu 24.04, a Python app and PostgreSQL 18) we break and fix the four usual causes: localhost inside a container, the published port instead of the container port, two Compose projects on separate networks, and a database that isn't ready yet, which is why it works on the second try.

How to Fix Docker Compose Containers That Can't Talk to Each Other, the full picture: diagram of Ana's laptop, web, db, report, shop project, reports project
The whole picture, from the video's recap.

Running, but not talking

Your app container can't reach your database container in Docker Compose: connection refused. Compose started both containers. The database is still running, the app quit, and yet you can reach the database from your own laptop. Ana's tea shop has exactly that.

laptop: Ana's terminal
docker compose logs --no-log-prefix webweb: connecting to the database at localhost:5432
web: connection failed: connection to server at "127.0.0.1", port 5432 failed: Connection refused
laptop: Ana's terminal
docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'SERVICE   STATUS
db        Up 1 second
web       Exited (1) Less than a second ago
laptop: Ana's terminal
psql -h localhost -p 5433 -U postgres -c 'SELECT count(*) FROM teas' count
-------
     5
(1 row)

Her compose file has two services: db, running PostgreSQL on its usual port, 5432, and web, her shop's app. Web is told to find the database at localhost.

Nothing is broken in Docker here. There are four usual reasons containers can't reach each other, and one of them is strange: it fails the first time you start, then works when you run the very same command again. By the end you'll know all four, and why the second try works.

You're far from alone. On Stack Overflow, the question about containers in two Compose projects that can't reach each other has over six hundred thousand views and 23 answers. That's one of the four, and its fix is three lines.

Communication between multiple docker-compose projects: figure of 669,727 views on Stack Overflow, source: api.stackexchange.com, question 38088279, 2026-11-09

The usual fix: the service name

If your app's settings say localhost for the database, start here. grep finds the line: line 14, localhost. Replace it with the database's service name, db. sed swaps the text right in the file.

laptop: Ana's terminal
grep -n DB_HOST compose.yaml14:      DB_HOST: localhost
laptop: Ana's terminal
sed -i 's/DB_HOST: localhost/DB_HOST: db/' compose.yaml
laptop: Ana's terminal
grep -n DB_HOST compose.yaml14:      DB_HOST: db

Then docker compose up -d again. Compose sees that web's settings changed, so it recreates only web, while the database keeps running. Web's log: connected. And the shop answers with its five teas.

laptop: Ana's terminal
docker compose up -d Container shop-db-1  Running
 Container shop-web-1  Recreate
 Container shop-web-1  Recreated
 Container shop-web-1  Starting
 Container shop-web-1  Started
laptop: Ana's terminal
docker compose logs --no-log-prefix webweb: connecting to the database at db:5432
web: connected, serving the shop on port 8000
laptop: Ana's terminal
curl localhost:8000chai: $5.75
earl grey: $7.00
green tea: $6.50
jasmine: $6.90
oolong: $8.25

So the usual fix is the service name. If yours already says db and still fails, one of the other three is waiting for you, and each one fails in its own way.

Four reasons, one by one: cards of 1. localhost, 2. The port, 3. The network, 4. Not ready yet

Why localhost fails

Why did localhost fail, when it works on Ana's laptop? Inside a container, localhost means that container itself. Docker gives each container its own little network setup: its own address, and its own localhost. So when web asked localhost for port 5432, it asked itself, and nothing in web listens there.

Each container has its own localhost: diagram of Ana's laptop, web, db, web's localhost, Compose network

So how does the name db work? Compose made one network for this project, shop_default, with web and db on it, each at its own address. Every container there gets a name server at 127.0.0.11, run by Docker. Ask it for db, and it answers with db's address. Docker's docs put it simply: each service is discoverable by its service name.

laptop: Ana's terminal
docker network inspect shop_default --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{println}}{{end}}'shop-web-1 172.19.0.3/16
shop-db-1 172.19.0.2/16
laptop: Ana's terminal
docker compose exec web grep nameserver /etc/resolv.confnameserver 127.0.0.11
laptop: Ana's terminal
docker compose exec web getent hosts db172.19.0.2      db

Here's the working request, step by step. Web asks Docker's name server: who is db? The answer comes back: its address. Web connects there, on port 5432, and the database answers. The service name is the one address that works from every container on the network.

How web finds db: diagram of web, Name server, db, who is db?, 172.19.0.2, connect, port 5432, connected, The service name works from every container

The port that only works outside

The second reason looks like a fix, and fails anyway. Ana's db has a ports line, 5433 to 5432. It opens a door on the laptop, port 5433, that leads to port 5432 inside db. That door is how she reached the database from her laptop at the start. Web doesn't go through the laptop at all: it reaches db directly, on the network.

The ports line opens a door on the laptop: diagram of Ana's laptop, db, web, shop_default

So a teammate looks at that line and thinks: the database is on 5433, and changes web's port setting to match. Guess first: does web connect now? No. Connection refused, at db's own address, port 5433.

laptop: Ana's terminal
docker compose ps --format 'table {{.Service}}\t{{.Ports}}'SERVICE   PORTS
db        0.0.0.0:5433->5432/tcp, [::]:5433->5432/tcp
web       0.0.0.0:8000->8000/tcp, [::]:8000->8000/tcp
laptop: Ana's terminal
sed -i 's/DB_PORT: "5432"/DB_PORT: "5433"/' compose.yaml
laptop: Ana's terminal
grep -n DB_ compose.yaml14:      DB_HOST: db
15:      DB_PORT: "5433"
16:      DB_PASSWORD: kettle-lab
laptop: Ana's terminal
docker compose logs --no-log-prefix webweb: connecting to the database at db:5433
web: connection failed: connection to server at "172.19.0.2", port 5433 failed: Connection refused

Read the ports line as two halves. The left number is a door on the laptop, for you and your tools. The right number is the port inside the container. Docker's docs say it directly: service-to-service traffic uses the container port. So Ana sets web back to 5432, and the shop works again.

The ports line, in two halves: diagram of "5433:5432", 5433 = the laptop's port, 5432 = the container's port, Between containers: db, port 5432. Ana sets web back to it.

Two projects, two networks

The third reason is the Stack Overflow one. Ana's nightly report lives in its own folder, with its own compose file, and it looks for the database by name: db. She runs it: Name or service not known. And the networks show why: Compose names each project's network after its folder, so the report got its own, reports_default.

laptop: Ana's terminal
cat compose.yamlservices:
  report:
    image: teashop:1
    command: python report.py
    environment:
      DB_HOST: db
      DB_PASSWORD: kettle-lab
laptop: Ana's terminal
docker compose run --rm report Network reports_default  Creating
 Network reports_default  Created
report: [Errno -2] Name or service not known
laptop: Ana's terminal
docker network ls --filter name=_defaultNETWORK ID     NAME              DRIVER    SCOPE
41ce94915d2c   reports_default   bridge    local
dfac6c384b7c   shop_default      bridge    local

On the map: web and db share the shop's network. The report sits alone on its own. Its question, who is db, goes to a name server that only knows the containers on that network, and has never heard of db.

The fix: put the report on the shop's network. Three lines at the bottom of its compose file: its default network is named shop_default, and external true means it already exists, so Compose uses it instead of making a new one. Run it again: five teas, 34 dollars 40 for one of each.

laptop: Ana's terminal
cat compose.yamlservices:
  report:
    image: teashop:1
    command: python report.py
    environment:
      DB_HOST: db
      DB_PASSWORD: kettle-lab
networks:
  default:
    name: shop_default
    external: true
laptop: Ana's terminal
docker compose run --rm reportreport: 5 teas, $34.40 for one of each

Now the report's container joins shop_default. Same name server as web and db, so who is db gets an answer, and it connects on port 5432. That's the fix the six hundred thousand visitors were looking for.

Works on the second try

And now the strange one: it fails only the first time. Ana deletes everything with down -v, where -v also deletes the database's data, like a new teammate's laptop, or a fresh test server. Same compose file that just worked. Guess first: does web come up? No. Connection refused, at db's right address and port.

laptop: Ana's terminal
docker compose down -v Container shop-web-1  Stopping
 Container shop-web-1  Stopped
 Container shop-web-1  Removing
 Container shop-web-1  Removed
 Container shop-db-1  Stopping
 Container shop-db-1  Stopped
 Container shop-db-1  Removing
 Container shop-db-1  Removed
 Volume shop_data  Removing
 Network shop_default  Removing
 Volume shop_data  Removed
 Network shop_default  Removed
laptop: Ana's terminal
docker compose up -d Network shop_default  Creating
 Network shop_default  Created
 Volume shop_data  Creating
 Volume shop_data  Created
 Container shop-db-1  Creating
 Container shop-db-1  Created
 Container shop-web-1  Creating
 Container shop-web-1  Created
 Container shop-db-1  Starting
 Container shop-db-1  Started
 Container shop-web-1  Starting
 Container shop-web-1  Started
laptop: Ana's terminal
docker compose logs --no-log-prefix webweb: connecting to the database at db:5432
web: connection failed: connection to server at "172.19.0.2", port 5432 failed: Connection refused

Same address, same port, so what changed? Only the timing. Compose started db, then started web right away. Web connected while the database was still setting up: refused, and web quit. Docker's docs say it plainly: Compose waits until a container is running, not until it's ready.

A first start, without a healthcheck: diagram of Compose, db, web, start, connect, port 5432, Running is not ready

The database's own log shows the gap. Ready, setup complete, and ready again, a third of a second later.

laptop: Ana's terminal
docker compose logs --no-log-prefix db | grep -E 'init process complete|ready to accept'2026-11-09 12:23:27.257 UTC [46] LOG:  database system is ready to accept connections
PostgreSQL init process complete; ready for start up.
2026-11-09 12:23:27.603 UTC [1] LOG:  database system is ready to accept connections

Why ready twice? On a first start, with no data yet, PostgreSQL creates its database using a setup server, which takes no network connections at all. It loads Ana's tea list, stops, and only then starts the real server, the one that listens on port 5432. Web knocked before the real server was up, and found nobody listening.

PostgreSQL's first start, in order: diagram of Setup server, teas.sql, Real server

Now Ana just runs up again, and changes nothing. Web's log has both tries: refused, then connected. That's the second-try mystery. Nothing was fixed. By the second try, the database was simply ready.

laptop: Ana's terminal
docker compose up -d Container shop-db-1  Running
 Container shop-web-1  Starting
 Container shop-web-1  Started
laptop: Ana's terminal
docker compose logs --no-log-prefix webweb: connecting to the database at db:5432
web: connection failed: connection to server at "172.19.0.2", port 5432 failed: Connection refused
web: connecting to the database at db:5432
web: connected, serving the shop on port 8000

The fix has two parts. First, a healthcheck on db: every second, Docker runs pg_isready, PostgreSQL's own is-it-ready tool, up to 30 times. Second, in web, depends_on db, with condition service_healthy: don't start web until that check passes.

laptop: Ana's terminal
grep -A 4 healthcheck compose.yaml    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 1s
      retries: 30
  web:
laptop: Ana's terminal
grep -A 2 depends_on compose.yaml    depends_on:
      db:
        condition: service_healthy

With both parts in place, the first start goes like this. Compose starts db, and Docker runs the check until db reports healthy. Only then does Compose start web, and its first connection works.

A first start, with a healthcheck: diagram of Compose, db, web, start, healthy, connect, port 5432, Compose waits for healthy

Fresh start again, data deleted. This time Compose waits for db, healthy, and only then starts web. Connected on the first try.

laptop: Ana's terminal
docker compose up -d Network shop_default  Creating
 Network shop_default  Created
 Volume shop_data  Creating
 Volume shop_data  Created
 Container shop-db-1  Creating
 Container shop-db-1  Created
 Container shop-web-1  Creating
 Container shop-web-1  Created
 Container shop-db-1  Starting
 Container shop-db-1  Started
 Container shop-db-1  Waiting
 Container shop-db-1  Healthy
 Container shop-web-1  Starting
 Container shop-web-1  Started
laptop: Ana's terminal
docker compose logs --no-log-prefix webweb: connecting to the database at db:5432
web: connected, serving the shop on port 8000

Real projects lean on the same idea. Sentry, the error-tracking company, ships its self-hosted version as one Docker Compose project, set up by an install script, and its docs start it with docker compose up --wait, which waits until services with a healthcheck report healthy.

Sentry self-hosted: one Compose project: cards of Runs on, Set up by, Started with, --wait

Check these four

So when containers can't talk, check these four. The name: the service name, never localhost. The port: the container's port, not the published one. The network: another project joins with external. And readiness: a healthcheck, plus service_healthy. Every command and the lab files are linked in the description, so you can break and fix this shop yourself.

When containers can't talk, check these four: cards of Name, Port, Network, Ready

Recap

So: inside a container, localhost is that container. Containers find each other by service name, on a shared network, at the container's port, once the other one is ready.

If this got your containers talking again, subscribing helps other beginners find it too.

Every command here ran for real, in our own lab. Thanks for watching Cloud Explainers.

Sources

Demos: one Ubuntu laptop container on our own lab server, running Docker and Compose inside it; the tea shop is made up. Watch the full video on YouTube.

More videos

Linux9 min

Linux Makefiles Will Finally Make Sense After This

Auth (OAuth, JWT)9 min

SAML vs OAuth vs OpenID Connect: What's the Difference?

Python7 min

How to Fix Python cron Jobs That Never Run

Linux22 min

Linux Bash Scripting: Full Tutorial for Beginners

Networking10 min

How PXE Network Boot Works

Active Directory9 min

How Active Directory Works

Linux10 min

How to Fix a Linux systemd Service That Won't Start

Linux9 min

Linux Logs: syslog vs journald

Auth (OAuth, JWT)9 min

JWT vs Session Cookies: Which One Should You Use?

Docker10 min

Docker vs Docker Compose: Which One Should You Use?

Django (Python web framework)22 min

Django Full Tutorial for Beginners

Linux9 min

How Linux systemd Works

Networking8 min

How to Fix 'Windows Cannot Access' Network Share Errors (SMB)

APIs10 min

REST vs gRPC: Which One Should You Use?

Certificates & PKI (ADCS)9 min

How to Fix SSL Certificate Chain Errors (Missing Intermediate)

Python9 min

Every Python Concept for Beginners in 10 Minutes

Databases & SQL9 min

Every SQL Join Type in 10 Minutes

OpenRouter (one API for many AI models)9 min

Why OpenRouter Free Models Hit Their Limit So Fast

Gemini API & Google AI Studio9 min

Why Your Gemini API Free Tier Runs Out So Fast

OpenRouter (one API for many AI models)10 min

OpenRouter Will Finally Make Sense After This

Caching & Redis10 min

How Redis Caching Works

Gemini API & Google AI Studio10 min

Gemini API vs Vertex AI: Which One Should You Use?

Gemini API & Google AI Studio8 min

How to Fix a Leaked Gemini API Key

Go (Golang)9 min

Why Golang Uses Goroutines Instead of Threads

Gemini API & Google AI Studio9 min

How to Fix Gemini API Error 429: Resource Exhausted

Claude & the Claude Developer Platform10 min

Claude Token Pricing Will Finally Make Sense After This

Python11 min

Why Python Web Scraping Returns Nothing (and the Fix)

Python10 min

Python uv vs pip: Which One Should You Use?

Python22 min

Python Automation: Full Tutorial for Beginners

Auth (OAuth, JWT)9 min

How OAuth Login Works

Gemini API & Google AI Studio10 min

Google AI Studio Will Finally Make Sense After This

Docker10 min

What Happens When You Run a Docker Container

Claude & the Claude Developer Platform8 min

How to Fix Claude Code Billing Your API Key Instead of Your Plan

Auth (OAuth, JWT)9 min

Why Passkeys Can't Be Phished Like Passwords

PowerShell10 min

PowerShell vs Bash: Which One Should You Learn?

Linux10 min

The Linux Roadmap for Beginners

Django (Python web framework)9 min

Django vs FastAPI: Which One Should You Use?

Databases & SQL10 min

Why SQLite Is the Most Used Database in the World

Databases & SQL11 min

How a NoSQL Database Like MongoDB Works

Python11 min

How to Fix Python pip error: externally-managed-environment

Databases & SQL9 min

How to Fix MySQL Too Many Connections Database Errors

Go (Golang)9 min

Golang Concurrency Will Finally Make Sense After This

AWS10 min

How Amazon S3 Works

Kubernetes10 min

How Kubernetes Works

Git10 min

How Git Works

Docker10 min

How to Fix Docker Port Already Allocated Errors

PostgreSQL10 min

How PostgreSQL Works

Git8 min

How to Fix Git Merge Conflicts

Linux9 min

How to Fix No Space Left on Device in Linux

Networking10 min

How the Internet Works

Databases & SQL9 min

SQL vs NoSQL: When Each One Wins

Auth (OAuth, JWT)8 min

How to Fix JWT Token Expired Errors (401 Unauthorized)

Claude & the Claude Developer Platform9 min

Claude API vs Claude Pro: Why Your Subscription Doesn't Include It

AI agent skills9 min

Why AI Agent Skills Beat Giant Prompts

Linux10 min

Linux Cron Jobs Will Finally Make Sense After This

Cybersecurity10 min

Security Logs Will Finally Make Sense After This

Cybersecurity8 min

How to Fix a Security Leak: An API Key Pushed to GitHub

DNS9 min

DNS Will Finally Make Sense After This

Cybersecurity9 min

Never Ignore a Security Update

Auth (OAuth, JWT)9 min

Two-Factor Authentication Will Finally Make Sense

Networking8 min

Never Expose a Database to the Internet

Cybersecurity10 min

SSH Keys vs Passwords: Which Is Safer for Security?

Linux10 min

The 5 Levels of Linux Boot: From Power On to Login

Networking12 min

The 5 Levels of Network Firewalls

Cybersecurity10 min

Password Security: Why Hackers Crack Weak Passwords

Linux9 min

Why Your Linux Server Runs Out of Memory

Networking10 min

HTTP vs HTTPS: What Actually Changes on Your Network

Linux12 min

Every Linux File Permission You Need to Know

Linux10 min

Linux Processes vs Threads: When Each One Wins

Networking11 min

The OSI Model Will Finally Make Sense After This

Git11 min

The 7 Levels of Git

Networking10 min

Every Network Troubleshooting Command You Need

Cybersecurity10 min

Hashing vs Encryption in Cybersecurity: When Each One Wins

Networking11 min

Why the Internet Ran Out of IP Addresses

Linux11 min

Linux Package Managers: apt vs Snap vs Flatpak

Python9 min

Why Python Is Slow (and When It Doesn't Matter)

Networking11 min

TCP vs UDP: When Each One Wins

Linux8 min

Stop Running Linux as Root (Do This Instead)

Linux11 min

Linux Users and Groups Will Finally Make Sense

Cybersecurity9 min

Why Phishing Beats Good Security (and How to Spot It)

Docker12 min

The 7 Levels of Docker

AI agents & MCP12 min

AI Agents Will Finally Make Sense After This

Git8 min

Stop Using git push --force (Use This Instead)

AI agent skills10 min

Agent Skills vs MCP: When Each One Wins

Networking12 min

Networking Will Finally Make Sense After This

Linux12 min

Every Linux Command You Actually Need

Cybersecurity12 min

The 7 Levels of Cybersecurity

System design12 min

Every System Design Concept You Need, in One Diagram

PostgreSQL10 min

Why PostgreSQL Ignores Your Index