explainer / Oct 14, 2026
External Docker Networks Are Not Public Networks
Compose uses external for network ownership and internal for connectivity. Understanding the difference prevents accidental sharing between homelab apps.

In Docker Compose, external means managed outside this Compose project, not exposed to the internet. Internal means restricted external connectivity, not owned by this project. These settings answer different questions. A home server can use a shared external network without publishing an application port, and a project-created network can carry services that are reachable from the home LAN.
That distinction matters when a self-hosted dashboard, reverse proxy and database live in separate Compose projects. Joining a shared network may solve service discovery while quietly widening which containers can contact each other. The useful decision is who needs a connection, not whether a network has a reassuring name.
Ownership and reachability are separate decisions
Docker's network attribute reference defines external networks by their lifecycle. With external: true, Compose looks for an existing network instead of creating it. A missing network causes an error; the setting does not ask Docker to open a router port or assign a public address.
By contrast, internal: true changes connectivity when a network is created. It is not the opposite spelling of external. An already-created network can have internal isolation properties and later be referenced by a Compose project as external. Its real configuration lives in Docker, not in the name selected for the Compose entry.
Do not combine external: true with internal: true in the same Compose network declaration. For an external network, Docker documents that attributes other than name are rejected. The project consumes the existing network; it does not redefine that network's properties.
Sharing a network also shares a discovery space
Consider a reverse proxy in one project and a photo application in another. If their containers join the same user-defined bridge network, the proxy can address the application by its service name and container port. It does not need a published host port for that connection. Docker's Compose networking guide explains that service-to-service traffic uses the container port, while host mappings serve a different access path.
The database need not join that shared proxy network. It can remain on the application's backend network, with only the application connected to both. This is a design choice about membership, not a guarantee created by naming a network backend.
A shared network also makes naming coordination important. Use unambiguous service names or network aliases across projects rather than assuming each project's generic api or web name is unique within the shared network. Avoid treating dynamically assigned container IP addresses as permanent configuration.
Internal does not mean inaccessible to everything
An internal network restricts ordinary external connectivity, but containers sharing it can still communicate. A container attached to both an internal network and a normal network may still reach outside through the normal one. Inspect all its network attachments before describing it as offline.
Host access is another boundary. Docker's port-publishing documentation describes how an ordinary internal bridge can still have a host-side bridge address. Internal is therefore not a complete isolation guarantee against the host, Docker administrators, or a compromised application that legitimately joins the network. Application authentication remains necessary.
These distinctions concern ordinary single-host Linux bridge networking. Host networking, macvlan, Swarm overlays and deliberately configured direct routing have different behaviour. Docker Desktop also adds a virtual-machine boundary. Do not use a bridge-network explanation as a universal statement about every homelab runtime.
Published ports are a different entrance
A port mapping such as 8080:80 connects a host port to a container port. With Docker's default binding configuration and no explicit host address, it binds broadly rather than only to loopback. Whether the internet can reach that port additionally depends on routing and upstream controls; external network ownership says nothing about that path.
A loopback mapping such as 127.0.0.1:8080:80 narrows the host-facing entry point under the documented default NAT behaviour. It does not stop same-network containers reaching the application's container port. Docker also warns that releases older than 28.0.0 have a same-L2 exception for localhost-published ports. Custom direct-routing settings require their own assessment.
The resulting questions are separate:
- Which containers share a network with this service?
- Which host addresses and ports publish it?
- Does a reverse proxy, tunnel or another attached network provide an additional route?
- What authenticates a request after it reaches the application?
Read the deployed boundary before changing it
For a read-only review, docker network inspect NETWORK_NAME shows the actual network; replace NETWORK_NAME with the deployed name. Check the driver, internal flag, options and attached containers. Docker access is required and should be treated as privileged administration, even when the immediate command only reads state.
From the correct existing Compose project, docker compose port SERVICE CONTAINER_PORT reports a published mapping when one exists. A missing mapping is not proof that the service has no other access route. Successful access from an intended peer and failed direct access from an unrelated LAN device answer different questions; neither result establishes the entire boundary alone.
A network called public can be private in practice, and one called internal can be ordinary unless its configuration says otherwise. Keep lifecycle ownership, network membership, egress and published entry points as four explicit decisions. That makes a shared homelab easier to reason about without giving a YAML label more security meaning than Docker assigns it.
Verification ledger