Real World Example of Using Bridge Network
The previous lesson showed that a user-defined bridge gives containers something important: they can find each other by name.
Now let’s stop talking about that feature in isolation and use it to solve a small application problem.
Imagine an application with two components:
Browser
|
v
Web service
|
v
Internal serviceThe browser should reach the web service. The web service should reach the internal service.
The internal service should not need a published port just so another container can use it.
That is exactly the kind of situation where a user-defined bridge network makes sense.
The Problem
Let’s say we have two containers:
web
|
v
backendWe want these rules:
- The
webcontainer should be reachable from the host. - The
webcontainer should be able to reachbackend. backendshould remain internal to the Docker network.webshould refer tobackendby name, not by IP address.
The desired architecture is:
flowchart LR
Browser["Browser / Host"] -->|"localhost:8080"| Web["web"]
Web -->|"http://backend"| Backend["backend"]
Backend -.->|"No published port"| Outside["Outside"]
Notice the important boundary:
Outside
|
| published port
v
web
|
| Docker network
v
backendOnly web needs to be exposed.
backend is an internal service.
Build the Network First
Create a user-defined bridge:
docker network create app-netNow we have a private application network where both web and backend will reside (not yet). So, later this network will have:
app-net
┌──────────────────────────────┐
│ │
│ web backend │
│ │ │ │
│ └───────────────────┘ │
│ │
└──────────────────────────────┘The network itself does not create any containers.
It simply gives us a place where containers can communicate.
Start the Backend
For this example, we’ll use nginx as a simple internal HTTP service.
Start it on app-net:
docker container run -d \
--name backend \
--network app-net \
nginx:alpineWe intentionally don’t publish port 80.
The backend is not supposed to be directly reachable from the host.
Its purpose is to be reachable by other containers on app-net.
We can verify that the container is attached:
docker network inspect app-netYou should see backend listed under the network’s containers.
Start the Web Container
Now start another nginx container:
docker container run -d \
--name web \
--network app-net \
--publish 8080:80 \
nginx:alpineThere are two important options here:
--network app-netputs web on the same network as backend.
And:
--publish 8080:80makes the web container reachable through the host.
So our setup is now:
flowchart LR
H["Host<br/>localhost:8080"] -->|"published port"| W["web<br/>:80"]
W -->|"app-net"| B["backend<br/>:80"]
This is a very common pattern:
External traffic
|
v
Exposed service
|
v
Internal serviceTest the External Side
From the host, open:
http://localhost:8080You should see the nginx welcome page from the web container.
The important part is that the host can reach web because we published:
8080:80Without that mapping, the host would not use localhost:8080 to reach the container.
Test the Internal Side
Now let’s pretend web is the application that needs to call backend.
The web container can reach the backend using its container name:
http://backendWe can test that from another temporary container on the same network:
docker container run --rm \
--network app-net \
alpine:latest \
wget -qO- http://backendYou should receive the nginx HTML from backend.
The important part isn’t the HTML.
The important part is this:
http://backendThere is no IP address.
Docker’s DNS resolves:
backend
↓
backend's current container IPThen the request travels across the user-defined bridge.
What Actually Happened?
Let’s follow the request:
sequenceDiagram
participant C as Temporary client
participant D as Docker DNS
participant N as app-net
participant B as backend
C->>D: Resolve "backend"
D-->>C: Backend's IP address
C->>N: HTTP request to backend IP
N->>B: Forward traffic
B-->>C: nginx response
The application doesn’t need to know:
172.18.0.2
172.18.0.3It only needs:
backendThat is the practical value of the user-defined bridge.
Why Didn’t We Publish the Backend?
This is worth pausing on because it is one of the easiest Docker networking concepts to mix up.
We started backend like this:
docker container run -d \
--name backend \
--network app-net \
nginx:alpineThere is no:
--publishYet another container can still reach it.
Why?
Because port publishing and container-to-container networking solve different problems.
Container-to-container
web ───────────────> backend
app-netThe containers are already connected to the same network.
No published port is required.
Host-to-container
Host ──8080──> web:80The host is outside the Docker network.
We publish the port so the host can reach the container.
So:
A container port does not need to be published for another container on the same network to use it.
Why Use a Name Instead of an IP?
Suppose backend currently has:
172.18.0.2We could configure the application to call:
http://172.18.0.2But that creates a problem.
Remove the backend:
docker container rm -f backendCreate it again:
docker container run -d \
--name backend \
--network app-net \
nginx:alpineThe replacement container may receive a different IP.
The application shouldn’t have to care.
It still calls:
http://backendDocker resolves the current address.
That’s the whole reason name-based service discovery is so useful.
What If the Containers Are on Different Networks?
Now let’s deliberately break the setup.
Create another network:
docker network create other-netSuppose another container joins that network:
docker container run -d \
--name outsider \
--network other-net \
nginx:alpineNow we have:
flowchart LR
subgraph App["app-net"]
Web["web"]
Backend["backend"]
end
subgraph Other["other-net"]
Outsider["outsider"]
end
Web --> Backend
Outsider -. "not on app-net" .-> Backend
web and backend share app-net.
outsider does not.
The fact that all three containers are running on the same Docker host does not mean they are automatically part of the same user-defined network.
That’s another important property of these networks:
A user-defined network gives you an explicit communication boundary.
Connect an Existing Container
What if we decide that outsider actually needs to communicate with backend?
We don’t have to recreate the container.
We can connect it to app-net:
docker network connect app-net outsiderNow outsider belongs to both networks:
app-net
┌─────────────────┐
│ │
web backend
│ │
└───────┬─────────┘
│
outsider
│
┌───────┴─────────┐
│ other-net │
└─────────────────┘It can now communicate with containers on app-net.
This is useful when a container legitimately needs to sit between two network groups.
A More Realistic Architecture
The two-container example is deliberately small, but the same idea scales to a common application structure:
flowchart TB
User["User / Browser"]
Web["web"]
API["api"]
DB["database"]
Cache["cache"]
User -->|"published port"| Web
subgraph AppNet["application network"]
Web --> API
API --> DB
API --> Cache
end
The external user only needs to reach web.
The other services can remain internal:
web
↓
api
↓
databaseEach service can use the next service’s network name:
api
database
cacherather than hard-coded container IP addresses.
We aren’t building all of those services here because the point of this exercise is the networking, not the application stack.
But the networking model is already the same.
The Key Design Decision
The important question isn’t:
“Should I publish this container’s port?”
Instead, ask:
“Who needs to reach this container?”
If the answer is:
Another container on the same Docker network
then you usually don’t need port publishing.
If the answer is:
The host or something outside Docker
then you may need to publish the port.
That gives you a simple mental model:
Who needs access?
|
┌─────────┴─────────┐
│ │
Another container Host / outside
│ │
v v
Docker network Publish portInspect the Finished Setup
At this point, inspect the network:
docker network inspect app-netYou should see both:
web
backendand, because we connected it earlier:
outsiderThe network has become a concrete description of which containers are part of this application’s communication space.
You can also see the networks attached to a container:
docker inspect webLook under its network configuration.
This is useful when you’re trying to understand why two containers can or cannot communicate.
Clean Up
We created a few containers and two networks during the exercise.
Remove the containers:
docker container rm -f web backend outsiderThe temporary Alpine container used for testing was started with --rm, so Docker already removed it after it finished.
Now remove the networks:
docker network rm app-net other-netThe host is back to its original state.
The Mental Model
This is the picture to keep:
Outside
|
published port
|
v
┌─────┐
│ web │
└──┬──┘
|
user-defined bridge
|
v
┌───────────┐
│ backend │
└───────────┘The important boundaries are:
Outside
|
| publish
v
web
|
| user-defined bridge
v
backendAnd inside the bridge:
"backend"
|
v
Docker DNS
|
v
backend's current IPSo the complete idea is:
Publish ports for outside access. Use a user-defined bridge for container-to-container communication. Use container names instead of container IPs.
That’s the practical reason user-defined bridge networks are the normal choice for multi-container applications on a single Docker host.
What’s Next
We’ve now used a bridge network to solve a real communication problem: one container is externally reachable, while another stays internal and is discovered by name.
There are other Docker network drivers, though.
The next interesting one turns the isolation model almost completely upside down: host networking, where the container shares the host’s network stack instead of getting its own isolated one.