Docker networks explained - part 2: docker-compose, microservices, chaos monkey
Tags: docker
Published: 2022-03-15 08:12:56


In my previous article on [docker networks](https://accesto.com/blog/docker-networks-explained-part-1/), I've touched the basics of network management using the docker CLI. But in real life, you probably won't work this way, and you will have all the containers needed orchestrated by a docker-compose config.

This is where this article comes into play - let's see how to use networks in real life. Covering the basics of network management with docker-compose, how to use networks for multi-repository/multi-docker-compose projects, microservices and also how to make use of them to test different issue scenarios like limited network throughput, lost packets etc.

Let's start with some very basic stuff, if you are already familiar with docker-compose you might want to skip some sections below.

## Docker-compose basics

### Exposing ports with docker-compose

The first thing you might want to do is to simply expose a port:

```yaml=
version: '3.6'
services:
  phpmyadmin:
    image: phpmyadmin
    ports:
      - 8080:80
```

For those of you new to Docker - expose means to open a port to the outside world. You can limit it by IP, but by default this will mean everyone can access it. The port is exposed on your network interface, not the container one. In the above example, you can access port 80 of PhpMyAdmin container on your port 8080 (localhost:8080).

As you can see, it's pretty simple, you just pass the ports to be exposed, following the same idea as in docker CLI, so `localPort:containerPort`. Adding the listening interface for local port is obviously also possible:

```yaml=
version: '3.6'
services:
  phpmyadmin:
    image: phpmyadmin
    ports:
      - 127.0.0.1:8080:80
```

**This might be handy when you do not want to expose some services to the outside.**

### Connecting containers within a docker-compose file

As mentioned in the [docker networks](https://accesto.com/blog/docker-networks-explained-part-1/) post, **docker-compose creates a network by default**. You can easily check this by creating a very simple docker-compose config file:

```yaml=
version: '3.6'
services:
  db:
    image: mariadb:10.3
    environment:
      MYSQL_ROOT_PASSWORD: secret
  phpmyadmin:
    image: phpmyadmin
    restart: always
    ports:
      - 8080:80
    environment:
      - PMA_HOSTS=db
```

Let's run it and see what happens:

```bash
docker-compose up -d
Creating network "myexampleproject_default" with the default driver
Pulling db (mariadb:10.3)...
(...)
```

And as you might have noticed in the first line, a default network called `myexampleproject_default` is created for this project. It is also visible in the docker CLI:

```bash
docker network ls
NETWORK ID     NAME                      DRIVER    SCOPE
a9979ee462fb   bridge                    bridge    local
d8b7eab3d297   myexampleproject_default  bridge    local
17c76d995120   host                      host      local
8224bb92dd9b   none                      null      local

```

**All containers from this docker-compose.yaml are connected with this network**. This means, they can easily talk to each other and hosts are resolved by name:

```bash
docker-compose exec phpmyadmin bash
root@b362dbe238ac:/var/www/html# getent hosts db
172.18.0.3      db
```

## Docker-compose networks and microservice oriented architecture

When writing a microservice oriented project, it's very handy to be able to simulate at least a part of the production approach in the development environment. This includes separating and connecting groups of containers, but also testing network latency issues, connectivity problems etc.

### Dividing containers into separate networks

But what if you DO NOT want the containers to be able to talk to each other? Maybe you are writing a system where one part should be hidden from the other one?
In practice, such parts of the system are separated using AWS VPC or similar mechanisms, but it would be nice to test this on a development machine, right?

No problem, let's take a look at this config file:

```yaml=
version: '3.6'
services:
  service1-db:
    image: mariadb:10.3
    environment:
      MYSQL_ROOT_PASSWORD: secret
  service1-web:
    image: nginxdemos/hello
    ports:
      - 80:80
  service2-db:
    image: mariadb:10.3
    environment:
      MYSQL_ROOT_PASSWORD: secret
  service2-web:
    image: nginxdemos/hello
    ports:
      - 81:80
```

As you can see we have two separate services, each consists of a web and db container. We would like to make db accessible only from their web service, so service2-web is not able to access service1-db directly.

Let's check how it works now:

```bash
docker-compose exec service1-web ash
/ # getent hosts service1-db
172.19.0.2        service1-db  service1-db
/ # getent hosts service2-db
172.19.0.5        service2-db  service2-db
```

Unfortunately, the services are not separated in the way we would like them to be.

![All services connected](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aw1gv4cg8flst3cfcu6c.jpeg)
 


No worries, this can be achieved adding very simple changes:

```yaml=
version: '3.6'
services:
  service1-db:
    image: mariadb:10.3
    environment:
      MYSQL_ROOT_PASSWORD: secret
    networks: 
      - service1
  service1-web:
    image: nginxdemos/hello
    ports:
      - 80:80
    networks: 
      - service1
      - web
  service2-db:
    image: mariadb:10.3
    environment:
      MYSQL_ROOT_PASSWORD: secret
    networks: 
      - service2
  service2-web:
    image: nginxdemos/hello
    ports:
      - 81:80
    networks: 
      - service2
      - web

networks:
  service1:
  service2:
  web:
```

We have introduced three different networks (lines 30-31) - one for each service and a shared one for web services. Why do we need the third one? It is required in order to allow communication between service1-web and service2-web. We also added network configuration for each of the services (lines 7-8, 13-15, 20-21, 26-28).

Let's check how service1-web resolves the names now:

```bash
dco exec service1-web ash
/ # getent hosts service2-web
172.22.0.2        service2-web  service2-web
/ # getent hosts service2-db
/ # getent hosts service1-db
172.20.0.3        service1-db  service1-db
```

![Split networks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x14oyhbrtvdekzqwyecy.jpeg)
 


As you can see, we can quite easily achieve a separation between containers by introducing networks, and connecting together only selected containers.


### Connecting containers between multiple docker-compose files

Quite often, such projects as the above one are split between git repositories, or at least between docker-compose.yaml files. So a developer can launch each of the services separately. How can we connect such services? Let's take a look.

Let's assume we decided to split the previously used project into two separate repositories. One for service1, and a second one for service2. This would mean we have two docker-compose.yaml files:

```yaml=
#service1/docker-compose.yaml
version: '3.6'
services:
  service1-db:
    image: mariadb:10.3
    environment:
      MYSQL_ROOT_PASSWORD: secret
  service1-web:
    image: nginxdemos/hello
    ports:
      - 80:80

```


```yaml=
#service2/docker-compose.yaml
version: '3.6'
services:
  service2-db:
    image: mariadb:10.3
    environment:
      MYSQL_ROOT_PASSWORD: secret
  service2-web:
    image: nginxdemos/hello
    ports:
      - 81:80
```

If we launch both configurations, service1-web and service2-web won't be able to communicate with each other, as they will be added to two different networks: each docker-compose.yaml file creates its own network by default.

```bash
docker-compose up -d
Creating network "service1_default" with the default driver
Creating service1_service1-web_1 ... done
Creating service1_service1-db_1  ... done
```

Let's start with adding back the network configuration for service1 with a small change:

```yaml=
version: '3.6'
services:
  service1-db:
    image: mariadb:10.3
    environment:
      MYSQL_ROOT_PASSWORD: secret
    networks: 
      - service1
  service1-web:
    image: nginxdemos/hello
    ports:
      - 80:80
    networks: 
      - service1
      - web

networks:
  service1:
  web:
    name: shared-web
```

We added a bit of configuration in line 20. In this case I wanted to give my web network a fixed name. By default, the name consists of `PROJECTNAME_NETWORKNAME`, and project name by default is the directory name. The directory we are in might have different names for different developers, so the safe option to go is to enforce this name.

Now, for service2, we need to act a bit differently:

```yaml=
version: '3.6'
services:
  service2-db:
    image: mariadb:10.3
    environment:
      MYSQL_ROOT_PASSWORD: secret
    networks: 
      - service2
  service2-web:
    image: nginxdemos/hello
    ports:
      - 81:80
    networks: 
      - service2
      - web

networks:
  service2:
  web:
    external: true #needs to be created by other file
    name: shared-web
```

As you can see in lines 20-21, in this case we configure an **external network**. This means, docker-compose won't try to create it and will fail if it is not available. But once it is, it will just reuse it.

That's it. **Service1 and 2 web containers can reach each other, but their databases are separated.** Both can also be developed in separate repositories.

As an extension to the above, you can take a look at container [aliases](https://docs.docker.com/compose/compose-file/compose-file-v3/#aliases) to make routing easier, or [internal](https://docs.docker.com/compose/compose-file/compose-file-v3/#internal) to even more isolate services.


## Chaos testing

As you know, when it comes to an outage, *the question is not if it will happen, but when*. It's always better to prepare for such scenarios and test how the system behaves in case of different issues.
What will happen if some packets are dropped, or the latency goes up? Maybe a service goes offline?

Chaos testing is all about getting prepared for this.

I highly recommend looking at [Pumba](https://github.com/alexei-led/pumba) a project that lets you pause services, kill them, but also add network delay, loss, corruption etc.

Fully describing pumba would take lots of time, so let's just have a look at a very simple network delay simulation.

Let's spin up a container that pings 8.8.8.8:

```
docker run -it --rm --name demo-ping alpine ping 8.8.8.8
```

Now, looking at the output, run the following command in a separate console tab:

```
pumba netem --duration 5s --tc-image gaiadocker/iproute2 delay --time 3000 demo-ping
```

![Chaos monkey example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/00n15lxmnj950jtzv10m.gif)
 

And that's it!

You can also implement other chaos tests within minutes.


## Summary

Docker and docker-compose are great tools to emulate different network configurations without the need for setting up servers or virtual machines. The commands and configs are pretty easy and simple to use. Combined with external tools like Pumba you can also test problematic situations and prepare for outages.

If you are interested in [Docker](https://accesto.com/blog/what-is-docker-and-why-to-use-it/), check out my e-book: [Docker deep dive](https://accesto.com/books/docker-deep-dive/)