Skip to content

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 service

The 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
backend

We want these rules:

  1. The web container should be reachable from the host.
  2. The web container should be able to reach backend.
  3. backend should remain internal to the Docker network.
  4. web should refer to backend by 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
backend

Only web needs to be exposed.

backend is an internal service.

Build the Network First

Create a user-defined bridge:

docker network create app-net

Now 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:alpine

We 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-net

You 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:alpine

There are two important options here:

--network app-net

puts web on the same network as backend.

And:

--publish 8080:80

makes 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 service

Test the External Side

From the host, open:

http://localhost:8080

You should see the nginx welcome page from the web container.

The important part is that the host can reach web because we published:

8080:80

Without 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://backend

We can test that from another temporary container on the same network:

docker container run --rm \
  --network app-net \
  alpine:latest \
  wget -qO- http://backend

You should receive the nginx HTML from backend.

The important part isn’t the HTML.

The important part is this:

http://backend

There is no IP address.

Docker’s DNS resolves:

backend
   ↓
backend's current container IP

Then 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.3

It only needs:

backend

That 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:alpine

There is no:

--publish

Yet another container can still reach it.

Why?

Because port publishing and container-to-container networking solve different problems.

Container-to-container

web ───────────────> backend
       app-net

The containers are already connected to the same network.

No published port is required.

Host-to-container

Host ──8080──> web:80

The 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.2

We could configure the application to call:

http://172.18.0.2

But that creates a problem.

Remove the backend:

docker container rm -f backend

Create it again:

docker container run -d \
  --name backend \
  --network app-net \
  nginx:alpine

The replacement container may receive a different IP.

The application shouldn’t have to care.

It still calls:

http://backend

Docker 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-net

Suppose another container joins that network:

docker container run -d \
  --name outsider \
  --network other-net \
  nginx:alpine

Now 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 outsider

Now 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
 ↓
database

Each service can use the next service’s network name:

api
database
cache

rather 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 port

Inspect the Finished Setup

At this point, inspect the network:

docker network inspect app-net

You should see both:

web
backend

and, because we connected it earlier:

outsider

The 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 web

Look 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 outsider

The 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-net

The 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
backend

And inside the bridge:

"backend"
    |
    v
Docker DNS
    |
    v
backend's current IP

So 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.

Last updated on