Connecting Containers Across Compose Projects
The orders service runs from its own repository's Compose file and calls http://inventory:8080, which runs from another repository — and fails with getaddrinfo ENOTFOUND inventory. The workaround in the README says to call http://host.docker.internal:8081 instead, which works on macOS, fails on Linux, and routes container-to-container traffic out through the host. Each Compose project gets its own isolated default network, so services in different projects cannot resolve each other. This page connects them properly through a shared external network, as part of local network and port mapping.
This is the normal situation for teams with one repository per service: each repository has a Compose file for its own service and dependencies, and local development needs a few of them running together.
Diagnostic
Check which networks each project's containers are on and whether a name resolves across them:
#!/usr/bin/env bash
set -euo pipefail
for c in $(docker ps --format '{{.Names}}' | grep -E 'orders|inventory'); do
printf '%-22s %s\n' "$c" "$(docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$c")"
done
docker exec orders-api-1 getent hosts inventory || echo "inventory does not resolve from orders"
docker network ls --filter name=shop-dev
Expected bad output:
orders-api-1 orders_default
orders-db-1 orders_default
inventory-api-1 inventory_default
inventory does not resolve from orders
NETWORK ID NAME DRIVER SCOPE
Each project lives on its own <project>_default network, and no shared network exists, so Docker's embedded DNS in orders_default knows nothing about inventory.
Root cause
Compose creates a default bridge network per project, named after the project, and attaches every service in that project to it. Docker's embedded DNS server resolves service names, container names and aliases only for containers on networks the querying container is attached to. Two projects therefore form two isolated islands. Routing through host.docker.internal and published ports works around the isolation by leaving the Docker network entirely, which is slow, depends on each service publishing a port, differs between Docker Desktop and Linux, and breaks as soon as two projects publish the same port. The clean solution is to create one network that both projects join explicitly, marked external so neither project tries to own or delete it.
A second, quieter problem appears once projects share a network: service names collide. Both repositories probably have a service called db or api, and on a shared network a lookup for db may return either one. Docker's DNS returns every container that answers to a name on a shared network, and clients usually take the first address, so the orders service can end up writing to the inventory database without any error at all. That failure is far worse than ENOTFOUND, which is why the resolution keeps databases off the shared network entirely and gives shared services explicit, unique aliases rather than relying on their service names.
The external network also changes the startup contract between projects. Compose's depends_on only works within one project, so the orders project cannot wait for the inventory API to be healthy. Clients that call across projects need retries with backoff on startup, or a readiness check in the orders service's entrypoint that polls http://inventory:8080/health before accepting traffic.
Resolution
- Create the shared network once — in the bootstrap script, since it must exist before either project starts:
#!/usr/bin/env bash
set -euo pipefail
docker network inspect shop-dev >/dev/null 2>&1 || docker network create shop-dev
docker network ls --filter name=shop-dev
- Attach only the services that must be reachable across projects, with unambiguous aliases, and keep private dependencies on the project's default network:
services:
api:
build: .
networks:
default: {}
shop-dev:
aliases:
- inventory
db:
image: postgres:16.4
networks:
shop-dev:
external: true
Save this in the inventory repository's compose.yaml. The inventory database stays private on inventory_default; only the API joins shop-dev, under the alias inventory.
- Do the same in the consuming project and call the other service by its alias:
services:
api:
build: .
environment:
INVENTORY_URL: http://inventory:8080
networks:
default: {}
shop-dev: {}
networks:
shop-dev:
external: true
- Start both projects in any order and test resolution from inside:
#!/usr/bin/env bash
set -euo pipefail
(cd ../inventory && docker compose up -d --wait)
docker compose up -d --wait
docker compose exec -T api getent hosts inventory
docker compose exec -T api wget -qO- http://inventory:8080/health
Expected output
$ docker compose exec -T api getent hosts inventory
172.24.0.3 inventory
$ docker compose exec -T api wget -qO- http://inventory:8080/health
{"status":"ok"}
$ docker network inspect shop-dev --format '{{range .Containers}}{{.Name}} {{end}}'
inventory-api-1 orders-api-1
The orders API resolves and reaches the inventory API by name over the shared network, and the only containers on that network are the two APIs — the databases remain private to their projects.
Because the shared network is external, docker compose down in either repository detaches that project's containers but leaves the network in place for the other. Restarting one project, rebuilding it or switching its branch no longer breaks the other's connectivity, which is the day-to-day improvement developers notice first.
Prevention
Create the network in
make bootstrapand check it inmake doctor, so a missing network produces a clear message rather than a Compose error (network shop-dev declared as external, but could not be found).Reserve aliases in a shared document or a small registry file in a platform repository — one alias per service, never generic names like
apiordbon the shared network.Avoid
host.docker.internalfor container-to-container traffic. Keep it for reaching processes that genuinely run on the host, as described in reaching host services from inside a container.
Platform caveats
Linux:
host.docker.internalis not defined by default on Linux Docker Engine, which is one reason the host-routing workaround breaks for Linux developers. The shared network approach needs nothing platform-specific.
macOS and WSL2 (Docker Desktop): shared networks behave identically; all projects must use the same Docker context, or they are on different daemons and cannot share a network at all.
Podman: external networks work with Docker Compose against the Podman socket; create the network with
podman network create shop-devordocker network createthrough the socket.
Apple Silicon (ARM64): no difference; networking is independent of image architecture.
Rollback
Remove the shared network from both Compose files and delete it once no container uses it:
#!/usr/bin/env bash
set -euo pipefail
git restore compose.yaml
docker compose up -d --force-recreate
docker network rm shop-dev || echo "network still in use by another project"
Frequently Asked Questions
Why can't two Compose projects see each other's services?
Each project has its own default network, and Docker's DNS only resolves names for containers on shared networks. Create an external network and attach the services that must talk to each other.
What does external: true do?
It tells Compose the network is managed outside the project, so Compose uses it but never creates or deletes it. That prevents one project's docker compose down from removing a network another project needs.
How do I avoid name clashes like two services called db?
Keep generic services such as databases on the project's private default network, and give services on the shared network unique aliases such as inventory or payments.
Could we use include instead?
If the projects live in one repository, yes — include combines them into one project with one network. Across separate repositories, a shared external network is simpler than including files by relative path from another checkout.