Skip to main content
Self-Hosting & Privacy

Docker Compose Networking Explained

Two containers on the same host could reach each other, and neither one was supposed to. Both had landed on Docker Compose's default bridge network — a network that doesn't ask permission before connecting things.

milanbuha00August 20, 20267 min read
ShareXin
Docker Compose Networking Explained

Two containers on the same host could reach each other over the network, and neither one was supposed to be able to. Nobody had written a firewall rule allowing it — it just worked, because both containers happened to land on Docker Compose's default bridge network, and that network doesn't ask permission before connecting things.

TL;DR

  • Every docker compose up creates its own bridge network automatically; every service in that file joins it and can reach every other service by name.
  • Two separate compose projects are isolated from each other by default — the danger is inside one project, not between them.
  • internal: true removes outbound internet access from a network entirely — the right call for a database.
  • external: true lets two independent compose stacks share one network on purpose, without either one destroying it on docker compose down.
  • This site's homelab gives every stack its own internal network, plus one shared external network that only the reverse proxy joins.

KEY-STAT: 1 — Shared external network is all it takes to let a single reverse proxy front every isolated stack on this homelab, without flattening them onto one network

The default: one bridge network per compose project

Run docker compose up with no networks: block at all, and Compose creates a bridge network named after the project directory and puts every service in that file onto it. Containers reach each other by service name — Docker's embedded DNS resolves vaultwarden to that container's internal IP without anyone hardcoding an address. Two separate docker compose projects on the same host get two separate default networks and cannot see each other unless you explicitly connect them. That isolation is doing more work than most people realize; the danger isn't between stacks, it's inside one stack where every service can reach every other service whether it needs to or not.

Why that default breaks down with a reverse proxy

A reverse proxy like Caddy or Traefik needs to reach containers that live in other compose projects — Jellyfin's stack, Vaultwarden's stack, n8n's stack, each defined in its own file as covered in the reverse proxy comparison. Putting everything on one flat network so the proxy can reach it all defeats the isolation Compose gives you for free — now Vaultwarden and n8n can talk to each other too, for no reason anyone chose deliberately.

This site's real topology: one internal network per stack, one shared network for the proxy

The actual layout on this homelab, first built following the homelab beginner's guide: each stack — media, credentials, automation — gets its own private network that never leaves that compose file. Caddy joins a second, separate network that every stack also joins, and only that network. Nothing in the media stack can reach anything in the credentials stack; both can reach Caddy, and Caddy can reach both.

The proxy stack's compose file declares the shared network as external and joins only that:

networks:
  proxy_net:
    external: true

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    networks:
      - proxy_net
    ports:
      - "80:80"
      - "443:443"

The Vaultwarden stack's compose file then joins that same shared network, plus its own private one:

networks:
  proxy_net:
    external: true
  vw_internal:
    driver: bridge

services:
  vaultwarden:
    image: vaultwarden/server:latest
    restart: unless-stopped
    networks:
      - proxy_net
      - vw_internal

Vaultwarden joins both networks: proxy_net so Caddy can reach it, and its own vw_internal network for anything else in that stack (a backup sidecar, for example) that has no business being reachable from the media or automation stacks. The n8n and Jellyfin stacks follow the identical pattern, each swapping in its own internal network name.

Tip

Name the shared network something specific like proxy_net, not internet or web — a name collision between two unrelated compose projects both trying to create a network with the same generic name is a real, confusing failure mode.

Service-name DNS: how containers find each other

Within a shared network, Compose's embedded DNS resolves a service name to that container's current IP — no static IPs, no /etc/hosts editing, and it survives container restarts even though the underlying IP can change. This only works for containers on the same network; a container on vw_internal cannot resolve or reach a service that only exists on proxy_net's peer stacks. That distinction matters when debugging: curl: could not resolve host means the two containers don't share a network at all, while connection refused means they do share a network but the target service isn't listening or isn't up — check its restart status first, as covered in the restart policies guide, before assuming it's a networking bug.

internal: true — the network that can't reach the internet

Marking a network internal: true removes its outbound route entirely — containers on it can talk to each other, but nothing on that network can reach the public internet, and nothing outside the host can reach in. That's the right setting for a database container that has no legitimate reason to phone home:

networks:
  db_net:
    driver: bridge
    internal: true

services:
  postgres:
    image: postgres:17
    restart: unless-stopped
    networks:
      - db_net

Warning

internal: true blocks outbound traffic completely, including the image's own update checks or telemetry calls some software makes on startup. If a container silently fails to start after adding this, check its logs for a blocked outbound connection before assuming it's a Compose bug.

external: true — joining infrastructure Compose doesn't own

external: true tells Compose "this network already exists, use it, and never create or destroy it as part of this project's lifecycle." Create it once, by hand, before either stack starts:

docker network create proxy_net

From then on, both the proxy's compose file and every app stack's compose file reference proxy_net as external: true. Running docker compose down on any one of them leaves the shared network untouched — only a stack-specific internal network gets torn down with its stack.

Decision table: which network setup for which situation

Situation Setup Why
A single, self-contained stack with no reverse proxy Default network, no networks: block needed Nothing to isolate from — the default already isolates you from other projects
A database or backend service with no reason to reach the internet Dedicated network with internal: true Removes an entire class of exfiltration and unwanted-update risk
Multiple independent stacks fronted by one reverse proxy One external: true shared network, plus a private network per stack Proxy reaches everything; stacks can't reach each other
Two stacks that legitimately need to talk (e.g. an app and its dedicated cache) A second, purpose-named internal network joined by only those two services Narrower than a whole-homelab shared network, still isolated from everything else

Debugging: is this a network problem or a restart problem?

Before chasing a networking issue, confirm the target container is actually running: docker compose ps — an Exited status means no restart policy brought it back, which is a restart-policy problem, not a networking one. If it shows Up, confirm both containers share a network with docker network inspect <network_name> and check the Containers list. Then test reachability directly from inside one container:

docker compose exec caddy ping -c 2 vaultwarden

A successful ping with resolved DNS means the network layer is fine and the problem is one level up — likely a port or app-level misconfiguration, not Compose networking.

The bottom line

Let Compose's default per-project network handle single-stack setups, reach for internal: true the moment a service has no business talking to the internet, and use one deliberately shared external: true network — never a fully flattened one — the moment a reverse proxy needs to front more than one stack.

Frequently asked questions

Do containers in the same docker compose file share a network by default?

Yes. Compose automatically creates one bridge network per project and joins every service in that file to it, letting them reach each other by service name without any extra configuration.

What does internal: true do in a Docker Compose network?

It removes the network's outbound route entirely. Containers on that network can still reach each other, but none of them can reach the public internet, and nothing outside the host can reach in.

How do you connect two separate docker compose stacks to the same network?

Create the network once by hand with docker network create <name>, then reference it as external: true in both compose files. Neither project will create or destroy that network as part of its own lifecycle.

Why can't my reverse proxy reach a container in another compose project?

Almost always because the two projects don't share a network. Each docker compose up creates its own isolated default network, so a proxy in one project cannot resolve or reach a service in another project unless both explicitly join a common external: true network.

Related stories

More from Self-Hosting & Privacy

Stay in the loop

Get the latest articles delivered to your inbox. No spam, unsubscribe anytime.

Read next

Docker Compose Volumes and Backups Explained

I ran docker compose down -v to force a clean restart, out of muscle memory from a different project where that flag was harmless. It wasn't harmless here — it deleted a named volume along with the containers.

Continue Reading