Docker Compose lets you define and run multi-container Docker applications. When you use docker-compose up, it automatically creates a default network for your services to communicate on. But what if you need more control, like isolating services or connecting containers that aren’t part of the same docker-compose.yaml file? That’s where custom networks come in.

Let’s say you have a web application and a database. By default, Compose would put them on the same network. But what if you have multiple web apps and you want to ensure they can’t see each other’s databases directly? Or maybe you want to set up a separate network for testing that mimics your production environment?

Here’s a docker-compose.yaml that defines two custom networks: frontend-net and backend-net.

version: '3.8'

services:
  webapp:
    image: nginx:alpine
    ports:
      - "8080:80"
    networks:
      - frontend-net

  api:
    image: python:3.9-slim
    command: ["sleep", "infinity"]
    networks:
      - frontend-net
      - backend-net

  database:
    image: postgres:13-alpine
    environment:
      POSTGRES_DB: mydatabase
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    networks:
      - backend-net

networks:
  frontend-net:
    driver: bridge
  backend-net:
    driver: bridge

When you run docker-compose up -d, Docker creates these networks. The webapp and api services are attached to frontend-net. The api and database services are attached to backend-net. Notice that api is on both networks. This acts as a bridge, allowing webapp to talk to api (via frontend-net), and api to talk to database (via backend-net). The webapp cannot directly reach the database because they don’t share a common network.

The driver: bridge is the default and most common network driver in Docker. It allows containers on the same network to communicate with each other using their service names as hostnames. Docker’s embedded DNS server handles name resolution. So, from within the api container, you can simply refer to the database as database (its service name) instead of needing an IP address.

You can also configure network-specific options. For example, to assign a specific subnet to a network:

version: '3.8'

services:
  web:
    image: httpd:alpine
    ports:
      - "80:80"
    networks:
      - app-network

networks:
  app-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

Here, app-network will use IP addresses from the 172.28.0.0/16 range. This can be useful for more complex setups or to avoid IP address conflicts with other networks on your host.

The real power comes when you want to connect services defined in different docker-compose.yaml files or run services outside of Compose. You can define a network once in a top-level networks block and then reference it in multiple services across different Compose files.

Consider this scenario: you have a core database service defined in docker-compose.yml and a set of microservices, each in their own docker-compose.override.yml file. You want all microservices to be able to access the database.

docker-compose.yml (core services):

version: '3.8'

services:
  database:
    image: mongo:latest
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: password
    networks:
      - shared-db-net

networks:
  shared-db-net:
    name: my-app-shared-network # Explicitly name the network

docker-compose.override.yml (microservice 1):

version: '3.8'

services:
  user-service:
    image: my-user-service
    networks:
      - shared-db-net # Connect to the shared network

networks:
  shared-db-net:
    external: true # Indicate this network already exists

By running docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d, both the database and user-service will be attached to a network named my-app-shared-network. The external: true tells Compose not to create the network if it doesn’t exist, but to use an existing one. The name: my-app-shared-network in the first file ensures it’s created with a predictable name that other Compose files can reference. This allows user-service to connect to database by its service name.

The most surprising thing about custom networks is how they abstract away the underlying network topology. You don’t need to worry about IP addresses or port mappings between containers on the same network; Docker handles it seamlessly via DNS resolution and internal routing. This means your application configurations can remain stable even if the host’s network configuration changes or if you move your containers to a different host.

The next logical step is to explore network policies and more advanced drivers like overlay for multi-host networking.

Want structured learning?

Take the full Docker course →