Docker Compose Networking: Bridge, Host, and Custom Networks Explained
Understand Docker Compose networking — how containers find each other, how to isolate services, and how to expose only what needs exposing.
Docker networking is where most beginners get confused. Containers seem to work fine until you need two services to talk to each other — then suddenly nothing resolves. This guide explains how Docker Compose networking works from first principles, so you can build stacks that communicate reliably.
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.
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.
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.
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.
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.”
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.
Ports 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.
The host Network Mode
An alternative to bridge networking is network_mode: host. This skips Docker networking entirely and gives the container direct access to the host’s network stack.
services:
pihole:
image: pihole/pihole:latest
network_mode: host
Useful for services like Pi-hole that need to bind to port 53 without NAT complications, or services that need to discover things on your LAN via broadcast/multicast.
Avoid it for most services — it removes the network isolation Docker provides, and port conflicts become your problem to manage.
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.
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 Related
Portainer: The Best GUI for Managing Docker on Your Homelab
Install and configure Portainer CE to manage Docker containers, images, networks, and volumes through a browser-based dashboard — no command line required.
Traefik + Docker Compose: Automatic HTTPS for Every Service
Set up Traefik as a reverse proxy with Docker Compose and get automatic Let's Encrypt HTTPS certificates for every service on your homelab.
Docker Compose in 20 Minutes: Your First Self-Hosted Stack
A step-by-step walkthrough of setting up Docker Compose and running your first self-hosted services. No prior Docker experience required.