A Docker Compose network is a user-defined bridge network that Compose creates for each project. Run docker compose up and every service joins one called <project>_default, where it can reach every other service by its service name on the container port, without any ports: entry. Declare a top-level networks: key only to split, rename or share networks.
Commands use docker compose, the successor to the hyphenated docker-compose.
The Default Network
When you run docker compose up without specifying any networks, Docker creates a default bridge network for your stack. Every container in the Compose file joins this network automatically.
Its name comes from the project name, taken from the directory the Compose file lives in: a stack in ~/stacks/media gets media_default, unless --project-name or COMPOSE_PROJECT_NAME overrides it. docker compose down removes it with the containers; external networks are never removed.
Within this network, containers can reach each other using their service name as a hostname. If you have a service named db in your Compose file, any other container in the same Compose file can connect to it at db:5432 (or whatever port the database listens on).
This works because Docker injects DNS resolution into each container. The service name resolves to the container’s IP address within the network.
services:
app:
image: myapp:latest
environment:
# "db" resolves to the db container's IP inside the network
- DATABASE_URL=postgres://user:pass@db:5432/mydb
db:
image: postgres:16
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mydb
The app container can reach the db container at the hostname db. No ports section needed on the db service — the port is only needed if you want to access the database from outside the containers.
Inside the network, always use the container port. If db published "8001:5432", app would still connect to db:5432: service-to-service communication uses the CONTAINER_PORT, and the host port only matters from outside.
To change the default network, add an entry named default under the top-level networks: key. This swaps in a network that already exists:
networks:
default:
name: proxy # created earlier with docker network create
external: true
One trap: only services without their own networks declaration join default. Give a service networks: [frontend] and it loses every sibling left on the default network, unless you list default too.
Container Name vs Service Name
Two things look similar but behave differently:
- Service name (
db) — the DNS name inside the Docker network. Use this when containers talk to each other. - Container name (
container_name: my-db) — the name shown indocker ps. You can also use this as a hostname within the same network.
If you don’t set container_name, Docker generates one automatically (like myapp-db-1). The service name always works for inter-container DNS regardless of the container name.
Default Network Isolation
The default network created by docker compose up is isolated per Compose project. Two separate Compose projects can’t reach each other’s services by default, even if they’re on the same host. That shared-network requirement is the single most common cause of a broken reverse proxy setup; see how to set up Traefik with Docker Compose.
This is often what you want — your Nextcloud stack and your Vaultwarden stack shouldn’t need to talk to each other.
When they do need to communicate (or when Traefik needs to route to them), you need a shared external network.
Custom Networks
You can define named networks in your Compose file:
services:
app:
image: myapp:latest
networks:
- frontend
- backend
db:
image: postgres:16
networks:
- backend
nginx:
image: nginx:alpine
networks:
- frontend
networks:
frontend:
backend:
In this example:
appcan reach bothdb(viabackend) andnginx(viafrontend)dbcan only be reached from services on thebackendnetworknginxcan only be reached from services on thefrontendnetwork
db and nginx cannot communicate directly, even though both are in the same Compose file. This is useful for defense in depth — if a frontend component is compromised, it can’t directly reach the database.
How Do You Create a Network in Docker Compose?
Declare the network under the top-level networks: key, list it under every service that should join, and run docker compose up -d. Compose creates it as <project>_<name> with the default bridge driver. For a network several stacks share, create it once with docker network create and mark it external: true in each file.
- Declare it under a top-level
networks:key, addingdriver_optsor anipamblock with asubnetonly if you need them. - Attach services by listing it under each service’s
networks:, keepingdefaultfor any service that still needs the rest of the stack. - Run
docker compose up -d, which creates the network before starting the containers. - Check it with
docker network lsanddocker network inspect <project>_<name>.
External Networks
An external network is created outside of any Compose file and shared across multiple stacks. This is how Traefik connects to services in separate Compose projects:
# Create once
docker network create traefik-proxy
Then reference it as external in each Compose file:
services:
myservice:
image: myimage:latest
networks:
- traefik-proxy
networks:
traefik-proxy:
external: true
The external: true tells Docker Compose “this network already exists — don’t try to create or delete it.” If it is missing, Compose returns an error instead of creating it, so run docker network create first on every new host.
Without external: true, Compose would try to create a network named traefik-proxy scoped to this project, which would be a different network than the one Traefik is watching.
Creating a Network by Hand
docker network create takes options that a Compose-managed network cannot easily be given after the fact, which is the main reason to create one up front:
# Plain user-defined bridge, the usual case
docker network create traefik-proxy
# With a fixed subnet, so container addresses are predictable
docker network create --subnet 172.28.0.0/16 homelab
# Confirm what exists and who is attached
docker network ls
docker network inspect traefik-proxy
docker network inspect is the fastest way to answer “why can’t these two containers see each other” — its Containers block lists every attached container with the name Docker’s DNS has registered for it. If the container you expect is not in that list, the problem is attachment, not DNS.
A network created this way outlives docker compose down, which is exactly what you want for a shared proxy network. Compose-created networks are removed with the project.
Naming: Project Prefixes and the name Key
Compose prefixes the networks it creates with the project name, so a network declared as backend in a project called media is actually created as media_backend. That prefix is why an external: true reference has to use the real name, not the short one.
To pin a Compose-managed network to an exact name with no prefix, use the name key:
networks:
backend:
name: homelab-backend # created exactly as written, no project prefix
Network Aliases
A service is reachable by its service name automatically. An alias adds extra DNS names for the same container on a given network:
services:
db:
image: postgres:16
networks:
backend:
aliases:
- postgres
- database
networks:
backend:
Other containers on backend can now reach it as db, postgres, or database. Two situations make this genuinely useful rather than cosmetic.
The first is migration: an application with a hardcoded hostname from an older setup keeps working if you alias the new service to the old name, with no change to the application’s configuration.
The second is per-network identity. A container attached to two networks can carry a different alias on each, which lets a frontend and a backend refer to the same service by different names:
services:
api:
image: myapi:latest
networks:
frontend:
aliases:
- api-public
backend:
aliases:
- api-internal
Aliases are not unique. Several containers can share one alias on the same network, in which case Docker’s DNS returns all of their addresses and the client picks one — a crude round-robin that is occasionally useful for a stateless service and is a source of confusion for everything else.
How DNS Works in Docker Compose
Containers on a Compose network resolve names through Docker’s embedded DNS server at 127.0.0.11. It answers for service names, container names and aliases on shared networks, and forwards everything else to the host’s DNS servers. Containers on the default bridge network get a copy of the host’s resolver file instead.
- Never hardcode container IPs. A recreated container gets a new IP address but keeps its name.
- Check the resolver.
docker compose exec app cat /etc/resolv.confshould listnameserver 127.0.0.11; anything else means service names will not resolve.
For names Docker does not own, set the service-level keys:
services:
app:
dns:
- 192.168.1.2 # e.g. a Pi-hole on the LAN
extra_hosts:
- "nas.home.lan=192.168.1.20"
dns sets the upstream servers, queried from the container’s own namespace, so 127.0.0.1 there means the container, not the host. extra_hosts writes /etc/hosts entries, which matters because the host machine’s own /etc/hosts is not inherited.
links Is Legacy — Use Networks
links predates user-defined networks. It was the original way to let one container reach another, and it worked by injecting /etc/hosts entries and environment variables into the dependent container:
services:
app:
image: myapp:latest
links:
- db # legacy - do not use in new files
In current Compose, links only adds an alias and an implicit start-order dependency, like depends_on. It does not connect networks: services with no network in common stay unreachable whatever links says. On the default bridge network, links are deprecated and scheduled for removal in Docker Engine 30.0.
For services sharing a Compose bridge network it is redundant: db resolves without links doing anything. Two reasons to remove it when you inherit it:
- It implies a start-order dependency that people mistake for a readiness guarantee. It is not one; use
depends_onwith a healthcheck condition, as shown at the end of this guide. - The alias form (
links: ["db:database"]) is better expressed as a network alias, which works consistently across all attached services rather than only the linked one.
If you need the alias behaviour, use the aliases key above. If you need ordering, use depends_on. There is no remaining case for links in a new Compose file.
Docker Compose Ports: Publishing vs No Ports
services:
db:
image: postgres:16
ports:
- "5432:5432" # exposes to the host — accessible from your LAN
vs
services:
db:
image: postgres:16
# no ports — only reachable from containers on the same Docker network
The ports: section publishes the container port to the host machine, making it accessible from outside Docker — your LAN, or the internet if you have port forwarding.
Without ports:, the service is only reachable from other containers on the same Docker network. This is what you want for databases, caches, and anything that shouldn’t be directly accessible from outside.
Rule of thumb: only publish ports for services that need to be accessed directly from outside Docker. Let everything else communicate via Docker networks.
Port Syntax
The short form is [HOST:]CONTAINER[/PROTOCOL], host side first. Always quote it to avoid YAML base-60 float conflicts:
| Entry | Result |
|---|---|
"8080:80" | Host port 8080 on all interfaces (0.0.0.0) to container port 80 |
"127.0.0.1:8080:80" | Same, reachable only from the Docker host |
"6060:6060/udp" | UDP instead of the default TCP |
"3000" | Container port 3000 on any unassigned host port |
docker compose port web 80 prints the public port bound to container port 80, which is how you find the one Docker picked.
Published Ports Skip ufw
A mapping without a host address binds to every host address, so Docker calls publishing insecure by default. With ufw it is worse: published traffic is routed in the nat table before ufw’s rules apply, so "5432:5432" is open to your LAN whatever ufw says. Bind to 127.0.0.1 when only the host needs a port, or use a Cloudflare Tunnel, which reaches containers by name with no published ports.
Is a Compose Network a Bridge Network?
Yes. On a single host Compose builds its networks with the bridge driver, but as user-defined bridges, which behave differently from Docker’s built-in bridge network, the one network_mode: bridge selects. Docker’s bridge driver docs call that built-in network a legacy detail not recommended for production, and these differences decide whether names resolve:
Built-in bridge (docker0) | Compose network (user-defined bridge) | |
|---|---|---|
| Name resolution | IP addresses only, or legacy links | Service name, container name, aliases |
| DNS server | Copy of the host’s resolver file | Embedded server at 127.0.0.11 |
| Who shares it | Every container started without a network | Only containers attached to it |
| Joining or leaving | Stop and recreate the container | On the fly |
When Bridge Is Not the Right Network
Start with a bridge network unless the application’s documented discovery requirements or LAN addressing needs call for something else. For sharing the host’s network stack, follow the Docker Compose host networking guide; for a separate LAN address, follow the Docker macvlan networking guide. The Pi-hole Docker Compose guide applies those choices to DNS and DHCP deployment.
Common Problems and Fixes
“Service ‘db’ not found” or connection refused between containers:
Check that both containers are on the same network. Services in different Compose projects are on different default networks and can’t reach each other unless you add them to a shared external network.
“Name or service not known” when using the service name:
Verify the service name in the YAML matches what you’re connecting to. Check with docker network inspect <network-name> to see which containers are on the network and what names they’re registered with.
Port conflict on the host:
Two containers trying to publish the same host port. Change one of the host ports (the left side of the host:container mapping). Container ports (right side) don’t conflict — they’re inside separate namespaces. Which is why a proxy lets you drop published ports entirely; see the Nginx Proxy Manager tutorial.
Container exits immediately, can’t connect to db:
Database containers take a few seconds to initialize. Your app may be starting before the database is ready. Add depends_on with a healthcheck condition to make the app wait.
services:
app:
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
Where to Go Next
Docker networks control what containers can reach inside the host. They say nothing about what reaches the host in the first place — that boundary belongs to your router and firewall, which the homelab firewall comparison at firewallcompare.com covers.
Inside the host, the next step is usually a reverse proxy, so that a single published port serves every service by hostname instead of a growing list of port numbers. Start with the Traefik with Docker Compose guide for label-driven routing, or the Nginx Proxy Manager tutorial if you would rather configure it through a web UI. To generate a stack with the networks and proxy already wired together, the interactive compose stack builder produces a single valid docker-compose.yml from a list of services.
Related across the network
- Open WebUI Not Connecting to Ollama: Fixes — openwebuihq.com
- Open WebUI vs LibreChat: Which to Self-Host — openwebuihq.com
Related on this site
FAQ
what is the difference between ports and expose in docker compose
ports publishes a container port on the host, on every interface unless you prefix an address such as 127.0.0.1, so your LAN can connect. expose publishes nothing. On a shared bridge network, every container port is already reachable by other containers, so expose mostly documents which port a service listens on.
how do i give a docker compose container a static ip address
Set ipv4_address under the service’s entry for that network, and give the network an ipam block whose subnet covers the address; Compose requires both. You rarely need one, though: a recreated container gets a new IP but keeps its name, so connect by service name and pin addresses only for software that insists on an IP.
do i still need links in docker compose
No. Services on a shared Compose network already resolve each other by service name, the job links used to do. In current Compose, links only adds an alias and an implicit start order, and it cannot connect services that share no network. Use aliases for extra names and depends_on with a healthcheck condition for start ordering.