How does Docker Compose's default networking model resolve service names to containers internally, and what breaks when two Compose projects on the same host define services with overlapping names or ports?
Quick Answer
Compose creates a dedicated user-defined bridge network per project (named after the project directory by default) and runs an embedded DNS resolver on that network so service names resolve to container IPs automatically — that's why checkout-worker can just connect to hostname postgres-db without hardcoding an IP. Two separate Compose projects get two separate networks and DNS namespaces, so name collisions across projects are harmless, but published host ports are a shared, global resource and will collide if two projects try to bind the same host port.
Detailed Answer
Picture an apartment building where every unit gets its own internal intercom system. Inside unit 4B, you can page 'kitchen' or 'front desk' and it just works, because that intercom network only exists within 4B's walls — it has no idea unit 6A also has a room called 'kitchen.' But the building's single street address and its shared elevator are different: only one entity can claim '123 Main St, Unit 1' at a time. Docker Compose networking works exactly like this. Each Compose project is its own private apartment (network), with its own internal name resolution, but published ports are the shared street address — a single, host-wide resource that any project can only claim once.
Compose was designed around per-project networks specifically so that stacks don't need central coordination to avoid name clashes. If every project shared one flat Docker network, then any two teams both naming a service 'redis' or 'api' would break each other's DNS resolution the moment both stacks ran on the same machine — a serious problem for shared CI runners or dev laptops running multiple projects at once. By scoping each project to its own bridge network (named <project>_default unless you override it), Compose lets 'db' mean something different in every project without any conflict.
Internally, when you run docker compose up, Compose first creates (or reuses) a user-defined bridge network for the project. Docker's embedded DNS server, which listens inside that bridge network at 127.0.0.11, registers each container's name and any network aliases as A records the moment the container starts. When checkout-worker's code does a DNS lookup for postgres-db, the container's resolv.conf points it at that embedded DNS server, which resolves the name to postgres-db's current container IP on that same bridge network — and re-resolves it if the container restarts and gets a new IP, which is why you should never hardcode IPs and always connect by service name. This whole mechanism is invisible to anything outside that specific bridge network; a container in a different Compose project's network cannot resolve or route to it at all unless you explicitly join both to an external shared network.
At scale, the failure mode teams actually hit isn't DNS collision (that's already solved by project isolation) — it's port collision. If project A's docker-compose.yml publishes 5432:5432 for its database and project B does the same, whichever starts second gets a hard 'port is already allocated' error, because host port bindings are a single shared namespace across the whole Docker daemon, not scoped per project the way the internal network is. Teams running multiple stacks on one CI host typically fix this by binding to ephemeral or offset host ports (binding host port to 0 lets Docker pick a free one) and only exposing internally.
The gotcha: containers in two different Compose projects cannot talk to each other by service name even if you know both project names, because they're on entirely separate bridge networks by default. Engineers debugging 'why can't service A reach service B' often don't realize the two are actually defined in separate docker-compose.yml files (maybe checked out from two different repos) and were never meant to share a network — the fix isn't a DNS problem to debug, it's adding both services to a common external: true network, and forgetting this is one of the most common multi-repo Compose setup mistakes.