Skip to content

Docker Compose Networks


Networks are one of the places where Docker Compose starts to feel much more useful than manually managing containers. You already know how Docker networks work; now Compose gives you a way to describe the application’s network structure alongside its services.

Why Use Networks With Compose?

Imagine an application with two services:

app
database

The application needs to communicate with the database.

With the Docker commands you’ve already learned, you would create a user-defined bridge network and connect both containers to it.

Conceptually:

        application network
        ┌──────────────────┐
        │                  │
        │   app ─────── database
        │                  │
        └──────────────────┘

With Compose, the network can become part of the application’s configuration.

Instead of separately creating the network and then connecting containers to it, you describe the relationship in compose.yaml.

This is another example of the central Compose idea:

Describe the application and its relationships in configuration instead of manually assembling them with separate commands.

The Default Compose Network

Compose Creates a Network for the Application

Start with a small example.

Create a directory:

mkdir compose-networks
cd compose-networks

Create compose.yaml:

compose.yaml
services:
  first:
    image: alpine
    command: ["sleep", "infinity"]

  second:
    image: alpine
    command: ["sleep", "infinity"]

There is no networks section yet.

Run:

docker compose up -d

Note

Even though you did not explicitly define a network, Compose creates a default network for the application.

The important idea is:

compose.yaml
     │
     ├── first service
     └── second service
            │
            ▼
      Compose network

Both services are connected to that Compose-managed network.

This means Compose gives the application a network environment even when you don’t explicitly configure one.

The default network is associated with the Compose project. You don’t need to memorize the exact generated name. What matters is the relationship:

Compose project
      │
      ├── first
      ├── second
      │
      └── default network

The services in the same Compose application can communicate through this network.

This is especially useful because you don’t have to manually create a network before starting the application.

Service Names Become Useful on the Network

Let’s make the example demonstrate communication between services.

We’ll use two containers:

  • client
  • server

Create this compose.yaml:

compose.yaml
services:
  server:
    image: alpine
    command: ["sh", "-c", "while true; do echo 'hello from server'; sleep 5; done"]

  client:
    image: alpine
    command: ["sleep", "infinity"]

Start the application:

docker compose up -d

Both services are now part of the same Compose application’s default network.

A very useful property of this network is that services can use their service names to refer to one another.

So from the application’s point of view:

client ───────► server
                ↑
          service name

The name server identifies the other service within the Compose network.

This is much more useful than trying to discover and hard-code a container’s IP address.

Note

Container IP addresses are implementation details that can change. In a Compose application, you normally communicate with another service using its service name rather than depending on its IP address.

Why Service Names Matter

Suppose your application has:

services:
  app:
    ...

  database:
    ...

The application can conceptually connect to:

database

rather than something like:

172.18.0.4

The relationship becomes:

                 Compose network
        ┌─────────────────────────────┐
        │                             │
        │   app ───────────────► database
        │                             │
        └─────────────────────────────┘

This makes the configuration much easier to understand.

You are saying:

“The app talks to the database service.”

rather than:

“The app talks to whatever container currently happens to have this IP address.”

Explicitly Defining Networks

Explicitly Defining a Network

The default network is convenient, but Compose also allows you to explicitly define networks.

Let’s modify the example.

Your compose.yaml file:

compose.yaml
services:
  server:
    image: alpine
    command: ["sh", "-c", "while true; do echo 'hello from server'; sleep 5; done"]
    networks:
      - app-network

  client:
    image: alpine
    command: ["sleep", "infinity"]
    networks:
      - app-network

networks:
  app-network:

Now the structure is explicit:

services
├── server ─────┐
│               │
└── client ─────┤
                │
                ▼
           app-network

The networks section at the bottom declares the network:

networks:
  app-network:

Each service then says which network it belongs to:

services:
  ...
  networks:
    - app-network

This is useful when you want the network structure to be visible and controlled as part of the application’s configuration — which is often if not always.

One Network, Multiple Services

A single network can connect several services.

For example:

services:
  app:
    image: alpine
    command: ["sleep", "infinity"]
    networks:
      - app-network

  database:
    image: alpine
    command: ["sleep", "infinity"]
    networks:
      - app-network

  worker:
    image: alpine
    command: ["sleep", "infinity"]
    networks:
      - app-network

networks:
  app-network:

The resulting structure is:

                  app-network
              ┌───────┼───────┐
              │       │       │
             app   database  worker

All three services are attached to the same network.

This is the Compose version of the user-defined bridge network concept you already learned.

Multiple Networks

Compose also becomes useful when an application needs more than one network.

For example, imagine:

frontend
backend
database

You might want:

frontend ───── frontend-network ───── backend
                                      │
                                      │
                               backend-network
                                      │
                                      ▼
                                   database

You can describe this explicitly:

services:
  frontend:
    image: alpine
    command: ["sleep", "infinity"]
    networks:
      - frontend-network

  backend:
    image: alpine
    command: ["sleep", "infinity"]
    networks:
      - frontend-network
      - backend-network

  database:
    image: alpine
    command: ["sleep", "infinity"]
    networks:
      - backend-network

networks:
  frontend-network: {}
  backend-network: {}

Now the relationships are:

frontend
   │
   │ frontend-network
   ▼
backend
   │
   │ backend-network
   ▼
database

This demonstrates a powerful principle:

Network membership controls which services share a network.

The Compose file becomes a description of the application’s communication structure.

Why This Is Better Than Manually Connecting Containers

Without Compose, you might have to think through several individual operations:

Create network A
Create network B
Create container A
Connect container A to network A
Create container B
Connect container B to networks A and B
Create container C
Connect container C to network B

With Compose, the relationships are visible together:

services
├── frontend → frontend-network
├── backend  → frontend-network + backend-network
└── database → backend-network

networks
├── frontend-network
└── backend-network

The Compose file is now acting almost like a diagram of the application’s communication architecture.

That is one of the reasons Compose configuration becomes valuable as applications grow.

Networks as Project Resources

Compose Networks Are Project Resources

Remember the lifecycle lesson.

Compose manages the resources described by the application.

Networks are part of that picture too.

When you run:

docker compose up -d

Compose creates the networks required by the configuration if they don’t already exist.

When you run:

docker compose down

Compose removes the networks it created for the application, subject to the network’s configuration and lifecycle rules.

You don’t need a separate manual network-creation workflow for a network that belongs to the Compose application.

A Network Can Belong to Several Services

A common beginner question is:

“Does each service need its own network?”

No.

A network is a shared communication space.

For example:

                 app-network
        ┌───────────┼───────────┐
        │           │           │
       app       database     worker

Services that need to communicate can share the same network.

The important question isn’t:

“Does every container need a network?”

The better question is:

“Which services need to communicate with each other?”

Then design the network membership around that requirement.

Network Design Is About Communication Boundaries

This is the mental model to carry forward:

             Network A
        ┌─────────────────┐
        │ app       worker│
        └─────────────────┘

             Network B
        ┌─────────────────┐
        │ worker  database│
        └─────────────────┘

Here:

app    <-> worker
worker <-> database

But app and database are not directly connected to the same network.

The network structure itself communicates something about the application’s architecture.

You aren’t just creating networks because Docker requires them.

You’re defining which components share a communication boundary.

When Should You Explicitly Define Networks?

For a small Compose application, the automatically created default network is often enough.

For example:

services:
  app:
    image: alpine

  database:
    image: alpine

You don’t need to write a networks section just to make these services share a network.

Explicit networks become useful when you want meaningful network names in the configuration, have multiple networks, different services need different network memberships, or you want the application’s communication structure to be obvious from the Compose file.

Don’t add network configuration just because you can.

Tip

Start with the default Compose network. Define explicit networks when the application’s communication requirements actually need them.

External Networks And Resource Flags

What About External Networks?

If you need a network that should be attached to your service container but whose lifecycle is managed outside the Compose file (i.e. you create / rm the network outside the Compose file), then you can use that network with the external: true key value.

services:
  mybox:
    image: busybox
    networks:
      - other
networks:
  other: {}

Important

If the other network doesn’t exist, Compose reports an error even with the --all-resources flag.

What Is the --all-resources Flag?

If you define a top-level resource but don’t use it in any services, Compose won’t create that resource by default. You have to tell it explicitly with the --all-resources flag.

For example, you have this compose.yaml:

services:
  web:
    image: nginx
  mybox:
    image: busybox
networks:
  mynetwork: {}

Here you defined mynetwork but never referenced or used it in any services (web or mybox). If you run:

docker compose up

Compose won’t create your mynetwork resource. You should use the --all-resources flag:

docker compose up --all-resources

Putting It All Together

A Complete Small Example

Create compose.yaml:

compose.yaml
services:
  frontend:
    image: alpine
    command: ["sleep", "infinity"]
    networks:
      - frontend-network

  backend:
    image: alpine
    command: ["sleep", "infinity"]
    networks:
      - frontend-network
      - backend-network

  database:
    image: alpine
    command: ["sleep", "infinity"]
    networks:
      - backend-network

networks:
  frontend-network:
  backend-network:

The architecture is:

                 frontend-network
              ┌────────────────────┐
              │                    │
         frontend ◄──────────────► backend
                                   │
                                   │
                              backend-network
                                   │
                                   ▼
                                database

The Compose file expresses the architecture directly.

The frontend and backend share one network.

The backend and database share another.

The backend acts as the bridge between those two application areas.

You can start it with:

docker compose up -d

And when you’re finished:

docker compose down

Mermaid View of the Same Architecture

The network structure can also be represented visually:

    flowchart LR
    frontend["Frontend"]
    backend["Backend"]
    database["Database"]

    frontend_net(("frontend-network"))
    backend_net(("backend-network"))

    frontend --- frontend_net
    backend --- frontend_net
    backend --- backend_net
    database --- backend_net
  

The important part isn’t the diagram syntax.

The important part is what the diagram communicates:

  • frontend shares a network with backend
  • backend shares another network with database
  • frontend and database do not share a network directly

Cleanup

Stop and remove the Compose application:

docker compose down

Then return to the parent directory:

cd ..

If you no longer need the example:

rm -rf compose-networks

Warning

rm -rf permanently removes the specified directory and its contents. Make sure you are in the correct parent directory before running it.

What You Should Remember?

Compose doesn’t replace Docker networking.

It gives you a way to describe Docker networking as part of the application.

The progression is:

Docker networking
      │
      ▼
You learned how containers
communicate through networks
      │
      ▼
Compose lets you describe
those relationships
      │
      ▼
compose.yaml
      │
      ├── services
      │
      └── networks

For a simple application, the default Compose network is often enough.

When you need more control, define explicit networks and assign services to them.

The most important mental model is:

Services are the components of your application. Networks define which of those components can communicate through a shared network.

Next, you’ll apply the same Compose approach to another Docker resource you’ve already learned: volumes. This will let you describe not only how your services communicate, but also where their persistent data lives.

Last updated on