Recently, I ran into a Docker networking issue that looked simple at first.
Two independent Docker Compose projects were running on the same host:
- a frontend service
- a backend API
Both Compose files declared a network called:
networks:
app-net:
At first glance, the setup looked straightforward.
If both containers use app-net, they should be able to communicate with each other.
But they could not.
The reason is an important Docker Compose detail:
The network name written in
compose.yamlis not always the actual Docker network name.
The Example
Imagine two independent projects:
Docker Host
│
├── portal/
│ └── compose.yaml
│
└── backend/
└── compose.yaml
The frontend project contains:
services:
portal-web:
image: example/portal-web:latest
networks:
- app-net
networks:
app-net:
The backend project contains almost the same configuration:
services:
core-api:
image: example/core-api:latest
networks:
- app-net
networks:
app-net:
Looking only at the YAML files, it seems like both containers are connected to the same network:
portal-web ─── app-net ─── core-api
But Docker Compose normally creates resources within the scope of a Compose project.
That changes what actually happens.
What Docker Really Creates
Compose projects are usually named after their project directories unless a project name is explicitly configured.
In this example, the projects may be called:
portal
backend
When Compose creates the networks, it may prefix the network names with the project name.
So this:
networks:
app-net:
inside the portal project may become:
portal_app-net
while the same configuration inside the backend project may become:
backend_app-net
The real structure is therefore closer to this:
Docker Host
│
├── portal-web
│ │
│ └── portal_app-net
│
└── core-api
│
└── backend_app-net
They are not connected to the same Docker network.
The network key is identical in both Compose files, but the actual network objects are different.
Verify Instead of Guessing
The easiest way to confirm this is:
docker network ls
You may see something like:
NETWORK ID NAME
a12bc34de567 portal_app-net
b98dc76fe543 backend_app-net
That immediately explains why the containers cannot communicate directly.
You can also inspect a network:
docker network inspect portal_app-net
The output includes the containers attached to it.
If portal-web appears there but core-api does not, the two services are isolated from each other.
This is why checking the actual Docker network is often more useful than repeatedly reviewing the Compose YAML.
The Cleaner Solution: A Shared Network
If two independent Compose projects intentionally need to communicate, they can share a network managed outside either project.
First, create the network:
docker network create app-shared-net
Then configure the frontend project:
services:
portal-web:
image: example/portal-web:latest
networks:
- app-shared-net
networks:
app-shared-net:
external: true
Use the same network in the backend project:
services:
core-api:
image: example/core-api:latest
networks:
- app-shared-net
networks:
app-shared-net:
external: true
Now both containers attach to the same Docker network:
Docker Host
│
├── portal-web
│ │
│ ├──── app-shared-net
│ │
│ └─────────────┐
│ │
└── core-api ─────────┘
The important part is:
external: true
This tells Compose:
Do not create a project-specific network. Use an existing Docker network.
That makes the relationship between the two independent projects explicit.
Why Docker DNS Matters
Once both containers share a user-defined Docker network, Docker provides internal DNS-based name resolution.
For example, the frontend can usually access the backend using the service name:
http://core-api:8080
instead of using a container IP such as:
http://172.22.0.5:8080
That is important because container IP addresses can change when containers are recreated.
Service names are much better identifiers.
You can test DNS resolution directly:
docker exec portal-web-1 getent hosts core-api
A successful result may look like:
172.22.0.3 core-api
Then test the application itself:
docker exec portal-web-1 curl http://core-api:8080/health
If the health endpoint responds, you have confirmed:
Shared Network ✓
Docker DNS ✓
TCP Connection ✓
Application ✓
Port Publishing Is a Different Thing
Another common source of confusion is Docker port mapping.
For example:
ports:
- "8080:8080"
means the service is exposed through the Docker host:
Host:8080
│
▼
core-api:8080
It does not automatically mean another container can resolve core-api through Docker DNS.
Host port publishing and container-to-container networking are separate concepts.
For internal service communication, sharing a Docker network and using service names is usually cleaner.
A Simple Troubleshooting Workflow
When two Docker containers cannot communicate, these checks usually find the problem quickly.
First, confirm the containers are running:
docker ps
Then check the real networks:
docker network ls
Inspect the intended network:
docker network inspect app-shared-net
Make sure both containers appear in the network.
Then test DNS:
docker exec portal-web-1 getent hosts core-api
Finally, test the application port:
docker exec portal-web-1 curl http://core-api:8080
This sequence helps separate different types of failures:
Docker Network
↓
Docker DNS
↓
TCP Port
↓
Application
If DNS resolution fails, debugging the application itself is probably too early.
The Key Lesson
The main lesson from this issue is simple:
Two Docker Compose files can use the same network key without actually sharing the same Docker network.
This:
networks:
app-net:
may result in:
portal_app-net
backend_app-net
Those networks are isolated.
If independent Compose projects need direct communication, check the actual Docker network objects and deliberately attach both projects to the same shared network.
The commands worth remembering are:
docker network ls
docker network inspect <network>
docker exec <container> getent hosts <service>
Docker networking becomes much easier to troubleshoot once you stop looking only at compose.yaml and start checking what Docker actually created.
Top comments (2)
The trap is that the network key is a name inside a project, not an address, so two projects declaring
app-netare declaring two different things that happen to look identical in the YAML. My addition to your workflow:docker inspect <container> --format '{{json NetworkSettings.Networks}}'beatsdocker network inspectfor this specific bug, because it answers the question you actually have - which network object is this container attached to - instead of listing a network and hoping you spot the container in it.Worth noting the one legitimate shortcut too:
networks: app-net: external: truein both compose files makes the sharing explicit and fails loudly if the network doesn't exist, which is a better default than silently getting two prefixed networks and debugging DNS for an hour. Yourgetent hostsstep is the right first check for the same reason - it separates "not attached" from "attached but misconfigured" before the app gets blamed.Good point — especially using docker inspect to verify the container’s actual network attachment. That’s more direct than starting from docker network inspect.
And yes, external: true is a clean way to make cross-project network sharing explicit. Thanks for adding this.