Common Docker Compose Errors covers the mistakes that break a Compose file structurally — bad YAML, undeclared volumes, a service with no image. These five are different: every example here is valid, parseable Compose, and the mistake is purely in how networking behaves once it's actually running. Every example below was run against a real Docker Compose install (v5.2.0).
network_mode and networks can't be combined
services:
web:
image: nginx:latest
network_mode: "service:api"
networks:
- frontend
api:
image: nginx:latest
networks:
frontend:network_mode: "service:api" puts web directly on api's network namespace — same IP, same localhost, ports bound by one are reachable through the other. That's a different, incompatible mechanism from Compose's normal networks: membership, and Compose rejects the combination outright rather than guessing which one you meant: service "web" declares mutually exclusive network_mode and networks: invalid compose project. The fix is picking one — either give web its own networks: entry, or drop networks: and rely entirely on api's network membership.
ports: silently does nothing under network_mode: host
services:
web:
image: nginx:latest
network_mode: host
ports:
- "8080:80"Unlike the previous case, this one isn't rejected — docker compose config accepts it and even echoes the port mapping back in its resolved output. It just does nothing at runtime. network_mode: host puts the container directly on the host's network stack, and host networking bypasses Docker's port-publishing path entirely — the process inside binds 80 on the host directly, and there's no NAT layer left for a ports: mapping to configure. Nginx ends up reachable at localhost:80, not localhost:8080, and nothing warns you the mapping was ignored.
An external: true network isn't looked up by its project-scoped name
networks:
shared:
external: trueCompose normally prefixes a network it creates with the project name — myproject_shared, not shared. Mark it external: true with no explicit name:, and that prefixing rule stops applying: Compose looks up the bare key, shared, exactly as written — verified via docker compose config, which resolves it to name: shared, not the project-scoped variant. Reference a network some other project created without knowing its exact real name — either the bare name you assumed, or a mismatched project prefix from wherever it actually came from — and Compose fails with network <name> declared as external, but could not be found. The fix is an explicit name: under the network entry, set to whatever docker network ls actually shows.
The same alias on two services makes resolution non-deterministic
services:
web:
image: nginx:latest
networks:
back-tier:
aliases: [database]
api:
image: nginx:latest
networks:
back-tier:
aliases: [database]
networks:
back-tier:Compose accepts this without complaint — verified, docker compose config resolves both services with the identical database alias and no warning. Docker's own docs are explicit about what happens next: "a single alias can be shared by multiple containers, or even services", but which container actually answers when something looks up database is not guaranteed. It's a plausible way to build a rolling-restart or A/B setup on purpose — and an easy accident to introduce when copy-pasting a networks: block between two services without updating the alias.
localhost inside a container never means another container
This one produces no error at all, which is exactly why it's so common — it just connects to the wrong thing, or nothing. Every container gets its own network namespace, so localhost inside api always means api itself, never db, never the host machine, regardless of what network they share. The fix is always the service name — db, not localhost — which is exactly what Docker's embedded DNS server exists to resolve. See How Docker DNS Resolution Works for how that resolution actually happens, and what changes on the one driver where localhost genuinely does mean the host: Docker Bridge vs Host vs Overlay Networks.
Try it yourself
Docker Compose Validator catches a network reference that was never declared in the top-level networks: section before any of this comes up. Docker Compose Visualizer renders which services actually share a network, so a network_mode or alias mistake is visible before you run anything. Once containers are running, Docker Network Inspector reads real docker network inspect output and flags a default-bridge network being relied on for name resolution directly from the JSON. All three run entirely in your browser.