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.

Why Cross-Project Names Do Not Resolve Flow from a service in one project looking up a service name that exists only on another project's network. Why Cross-Project Names Do Not Resolve orders-api looks up inventory project DNS no such name inventory-api on its own network ENOTFOUND no shared network
Embedded DNS answers only for containers on networks the caller shares.

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

  1. 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
  1. 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.

  1. 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
  1. 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
A Shared Network Between Projects The shared shop-dev network in the centre connects the public services of three projects while their databases stay private. A Shared Network Between Projects shop-dev network orders api alias orders inventory api alias inventory payments api alias payments databases private per project
Only services other projects call join the shared network, each under a unique alias.

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

  1. Create the network in make bootstrap and check it in make 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).

  2. Reserve aliases in a shared document or a small registry file in a platform repository — one alias per service, never generic names like api or db on the shared network.

  3. Avoid host.docker.internal for 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.

host.docker.internal vs Shared Network Comparison of routing cross-project calls through the host against a shared external network. host.docker.internal vs Shared Network via host.docker.internal shared external network needs published ports no ports needed differs on Linux same everywhere port clashes break it names, not ports leaves Docker network stays on bridge
The shared network keeps traffic inside Docker and works the same on every platform.

Platform caveats

Linux: host.docker.internal is 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-dev or docker network create through 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.