Defined Docker Networks for Seamless Communication Between Projects

If you have a containerized application composed of multiple services, it can become a burden to string together in-network communication. Fortunately, Docker Compose solves this networking problem by making it easy for your containers to communicate without knowing one another’s context. In a couple of short steps, I’ll show you how to quickly get a custom network running in your application and inject container references into your project’s environment for quick setup.

Networking Basics

You should already have your individual projects set up with Docker files. In the root of your project, add a docker-compose.yml file that looks similar to this:


version: "3.7"
services:
  api-external:
    build:
      context: .
      dockerfile: ./MyApp.APIExternal/Dockerfile
    ports:
      - "5000:80"
  api-internal:
    build:
      context: .
      dockerfile: ./MyApp.APIInternal/Dockerfile
    ports:
      - "6000:80"
  db:
    image: postgres
    ports:
      - "8001:5432"
  web:
    build:
      context: .
      dockerfile: ./MyApp.Web/Dockerfile
    ports:
      - "9220:80"

In this example, I’ve set up a web project with two APIs (internal and external) and a Postgres database. The web project requires the use of the external API, whereas the internal API relies on the database.

When we run this compose file, a default network called myapp_default is created. Each container (api-external, api-internal, db, and web) joins the myapp_default network and can now look up the hostnames of the others.

To show that, let’s make some changes to the docker-compose.yml file. We’d like web to get the IP of api-external, so we’ll pass an environment variable using Docker’s environment key. It will match an AppSettings variable defined in the web’s appsettings.json under AppSettings:APIBaseUrl.


web:
  build:
    context: .
    dockerfile: ./MyApp.Web/Dockerfile
  ports:
    - "9220:80"
  environment:
    - AppSettings:APIBaseURL=http://api-external

Inside the container, the APIBaseURL will look like http://api-external. But from the host machine, the url will resolve to http://{DOCKER_IP}. Now we can have full communication between containers.

Put it all together for the other services, and it looks something like this:


version: "3.7"
services:
  api-external:
    build:
      context: .
      dockerfile: ./MyApp.APIExternal/Dockerfile
    ports:
      - "5000:80"
    environment:
      - AppSettings:APIBaseURL=http://api-internal
  api-internal:
    build:
      context: .
      dockerfile: ./MyApp.APIInternal/Dockerfile
    ports:
      - "6000:80"
    environment:
      - AppSettings:DBConnectionString=postgres://db:5432
  db:
    image: postgres
    ports:
      - "8001:5432"
  web:
    build:
      context: .
      dockerfile: ./MyApp.Web/Dockerfile
    ports:
      - "9220:80"
    environment:
      - AppSettings:APIBaseURL=http://api-external

Custom Networks

This works really well on its own, but lets say we don’t want web to know anything about api-internal or db. In that case, we’ll need to put these services on separate networks by defining them using the network key. Here we’ll define two networks: frontend and backend.


networks:
  frontend:
  backend:

We can now have a service be connected to a defined network using the networks key:


web:
  networks:
    - frontend
  build:
    context: .
    dockerfile: ./MyApp.Web/Dockerfile
  ports:
    - "9220:80"
  environment:
    - AppSettings:APIBaseURL=http://api-external

Specifying these two new custom networks now allows us to create specific connections between services. We’ll also add the depends_on key to ensure that each service builds in the correct order. Put it all together, and it looks like this:


version: "3.7"
services:
  api-external:
    depends_on:
      - api-internal
    networks:
      - frontend
      - backend
    build:
      context: .
      dockerfile: ./MyApp.APIExternal/Dockerfile
    ports:
      - "5000:80"
    environment:
      - AppSettings:APIBaseURL=http://api-internal
  api-internal:
    depends_on:
      - db
    networks:
      - backend
    build:
      context: .
      dockerfile: ./MyApp.APIInternal/Dockerfile
    ports:
      - "6000:80"
    environment:
      - AppSettings:DBConnectionString=postgres://db:5432
  db:
    networks:
      - backend
    image: postgres
    ports:
      - "8001:5432"
  web:
    depends_on:
      - api-external
    networks:
      - frontend
    build:
      context: .
      dockerfile: ./MyApp.Web/Dockerfile
    ports:
      - "9220:80"
    environment:
      - AppSettings:APIBaseURL=http://api-external
networks:
  frontend:
  backend:

The web service now only knows of other services on its shared network. If we try to pass the reference of, lets say, api-internal through the environment variables of web, it will be unable to resolve.


web:
  networks:
    - frontend
  build:
    context: .
    dockerfile: ./MyApp.Web/Dockerfile
  ports:
    - "9220:80"
  environment:
    # This will fail!
    - AppSettings:APIBaseURL=http://api-internal

Now we have all the tools we need to quickly define networks for easy communication between our projects and services.

There is obviously much more you can do with Docker Compose networks, including using external networks and custom drivers. To read more about that, I highly recommend checking out the documentation from Docker.