Every homelab running Docker Compose eventually runs into the same
problem: you have one docker-compose.yml for your media stack, another
for your monitoring stack, and a third for your reverse proxy. They all
need to talk to each other, but Compose creates separate networks by
default.
This post covers the networking patterns you need to make multiple Docker Compose stacks communicate reliably — bridge networks, DNS-based service discovery, external network sharing, and multi-network containers — with real configurations you can drop into your homelab today.
How Compose Networking Works (Default Behavior)
When you run docker compose up, Compose automatically creates a
default bridge network named <project>_default. Every service in that
Compose file joins this network. Services can reach each other by their
service name, which Compose resolves via embedded DNS.
|
|
App connects to db:5432 — no IPs needed. This is the default bridge
network, and it Just Works for a single Compose file.
The problem starts when you have two Compose stacks:
|
|
These two app services are on completely separate networks. stack-a
cannot resolve app from stack-b, and stack-b cannot resolve app
from stack-a. No DNS, no connectivity, nothing.
Pattern 1: External Networks for Cross-Stack Communication
The cleanest way to connect multiple Compose stacks is to define a shared external network that every stack joins. This is the pattern used by every homelab reverse proxy setup.
First, create the shared network:
|
|
Then each Compose file joins it:
|
|
|
|
|
|
DNS resolves container names — Traefik discovers nextcloud:80 and
immich:2283 through the shared proxy-net. No manual IPs. No
Compose-level dependencies across stacks.
Pattern 2: Multi-Network Containers (The Traefik Sandwich)
A more advanced pattern: a single container belongs to multiple networks simultaneously. Traefik is the classic example — it needs to be on both the public network (to accept inbound traffic) and on every internal network (to reach backend services).
|
|
Traefik sees every service on all three networks. Backend services only
need to be on their respective network — they never expose ports to the
host. This is defense-in-depth: even if a service in internal-apps-net
is compromised, it cannot reach services on monitoring-net.
|
|
No port mapping on Grafana — it’s only reachable through Traefik on the
shared monitoring-net. A container in internal-apps-net cannot touch
it.
Pattern 3: Internal Networks for Database Isolation
Databases should never be exposed to external networks — not even to the shared proxy network. The pattern: create a separate internal network, put only the database and its consumer on it.
|
|
The database is unreachable from Traefik, unreachable from the internet,
unreachable from any other Compose stack. Only nextcloud can talk to
it. The internal: true flag on the network means the database cannot
even initiate outbound connections.
For PostgreSQL and MySQL, add --network=nextcloud-db-net to your
backup container:
|
|
Only the database, the app, and the backup container exist on this network. Nothing else.
Pattern 4: Default Networks with Custom Names and Subnets
Sometimes you want more control than Compose’s auto-generated
<project>_default. Define the default network explicitly:
|
|
Benefits:
- Predictable subnet — useful if your firewall rules reference Docker IPs
- Named network — easier to reference as
external: truefrom other stacks - No collision with other Compose defaults (all named
media-stackinstead ofproject_a_defaultvsproject_b_default)
Check what IP a container got:
|
|
Pattern 5: Aliases and Container Names for Stable DNS
Compose services get DNS entries by their service name. But if you need
a service to respond on multiple DNS names, use aliases:
|
|
Services on db-net can reach this container as postgres, database,
primary-db, or app-database (the container name). This is useful
when migrating — you can run both old and new databases temporarily
with the same DNS aliases.
Container names also work as DNS entries within the same network:
|
|
Pattern 6: The Database URI with Docker DNS
When configuring apps to connect to databases, use the service name as the hostname:
|
|
Docker DNS resolves postgres, redis, and rabbitmq to the correct
container IPs on the shared network. No config files, no IP hardcoding,
no round-robin confusion.
If the database fails and restarts, Docker re-issues the same DNS name with the new IP. Apps reconnect automatically as long as they have connection retry logic.
Pattern 7: Depends On for Network Startup Ordering
Services on the same network still need startup ordering. Use
depends_on with health checks for production services:
|
|
Without condition: service_healthy, depends_on only waits for the
container to start, not for the database to accept connections. The
app starts, connects to postgres:5432, Docker DNS resolves it, but
PostgreSQL isn’t ready yet — connection refused.
With health checks + condition: service_healthy, Compose waits for
pg_isready to succeed before starting the app. The network is
available, DNS resolves, and the database is listening.
Pattern 8: Connect Running Containers to Networks
Sometimes you need to connect an already-running container to a network without restarting it. Useful for debugging or temporary access:
|
|
This is a runtime operation — the container keeps running. Use it when you need to temporarily add a debugging container to a network:
|
|
The netshoot image is purpose-built for Docker network debugging —
it includes dig, nslookup, curl, tcpdump, ping, mtr, and
netstat.
Pattern 9: Network Troubleshooting Toolkit
When containers can’t reach each other, here’s the diagnostic sequence:
Step 1 — Verify network exists:
|
|
Step 2 — Check which containers are on each network:
|
|
Step 3 — Test DNS resolution from inside a container:
|
|
Step 4 — Test TCP connectivity:
|
|
Step 5 — Check container-level network config:
|
|
Common failures:
| Symptom | Likely Cause | Fix |
|---|---|---|
| DNS resolution fails | Container not on the same network | Add network to networks: in Compose |
| Connection refused | Service not listening on that interface | Check bind 0.0.0.0 in app config |
| Connection timeout | Firewall blocking the port | Check iptables -L DOCKER on host |
| Can’t reach by hostname | Missing container_name in Compose |
Use service name instead |
| Works on host but not in container | Port exposed on 127.0.0.1 |
Expose on 0.0.0.0 in the app |
Real-World Homelab Network Topology
Here’s how these patterns compose into a full homelab:
Internet
│
┌───┴───┐
│ WAN │
└───┬───┘
│
┌───┴───┐ public-net
│Traefik│◄──── (443/80)
└───┬───┘
│
┌─────────────────────────┼─────────────────────┐
│ │ │
┌─────┴─────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ nextcloud │ │ immich │ │ grafana │
│ :80 │ │ :2283 │ │ :3000 │
└─────┬─────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌─────┴─────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ postgres │ │ postgres │ │ prometheus │
│ (internal)│ │ (internal) │ │ (monitoring)│
└───────────┘ └─────────────┘ └─────────────┘
Networks:
| Network | Type | Contains | External Reach |
|---|---|---|---|
public-net |
bridge | Traefik | Internet (80, 443) |
media-net |
bridge | Sonarr, Radarr, Plex | LAN only |
monitoring-net |
bridge | Grafana, Prometheus | Traefik only |
nextcloud-db-net |
internal | Nextcloud + Postgres | None |
immich-db-net |
internal | Immich + Postgres | None |
Compose file structure:
|
|
Every stack is its own Compose file. No single docker-compose.yml
with 30 services. Each stack can be updated, restarted, or torn down
independently.
The One-Liner: Creating All Networks
Bootstrap all the shared networks before deploying:
|
|
Then deploy each stack independently:
|
|
Want to rebuild monitoring without touching the media stack?
|
|
Everything else keeps running. That’s the point of multi-stack networking.
Summary
Docker Compose networking is deceptively simple until you cross stack boundaries. The core patterns are straightforward:
- External networks — create once, reference from every Compose file
- Multi-network containers — put Traefik on every network that needs it
- Internal networks — lock databases behind
internal: true - Service name DNS — use container names, not IPs, everywhere
- Health checks — wait for
service_healthybefore connecting
These five patterns cover 90% of homelab networking needs. No service mesh, no Kubernetes, no overlay networks. Just well-planned bridge networks with clean boundaries.