Set up a multi-container app with docker compose
Tags: docker, go, devops, webdev
Published: 2023-02-11 22:10:15

Usually, we use Docker to build a whole application environment in different machines, this can be on an AWS instance, a VM, or even on your localhost.

Of course, you can write a giant Dockerfile to run all the components of your application, database, backend, web server configuration, even a cache service, this is a solution, with some disadvantages:
- Building the image will take so long
- If any component requires an update, the whole image will be rebuilt
- Debugging any component will be a nightmare

Well, the other option is to build many Dockerfiles to actually manage multiple containers, this solution of course solves all the issues above, since we can work with each component individually.

Yet not everything is perfect, now we’ll need to handle many containers manually, run `docker run ...` for each component, creating a network to connect all our containers, setup many environment variables, and passing aliases to each container to communicate them is quite another nightmare.

How do we fix this? Maybe creating an `init.sh` file with each `docker run ...` instruction is a suitable approach, but there is another solution offered by `Docker` to handle this kind of situations, it is called `docker compose`.


##`docker compose`
Compose is a tool for defining and running multi-container applications. It allows us to define all the services in a single file called `docker-compose.yml` file and then start, or stop, all of them with a single command.

###Installation
Most of the newer versions of Docker Desktop already comes with docker compose command, although, you can always check the installation instructions at their official [GitHub repository](https://github.com/docker/compose).

###Post’s code
For this docker compose tutorial, I will use a simple REST API written in Go with a MySQL database. Since the app’s functionality is not in our scope I won’t explain it, yet if you have any question related to the example app, please don’t hesitate to reach me out by [email](mailto:me@aldorvv.dev) or on [Twitter](https://twitter.com/aldorvv__).

You can find all the code used in this post [here](https://github.com/aldorvv/BlogCode/tree/main/MultiContainerWithDockerCompose)

##`docker-compose.yml`

In the [repository for this post](https://github.com/aldorvv/BlogCode/tree/main/MultiContainerWithDockerCompose) you will find a file named `docker-compose.yml` this file will be the “entry point” for our application, let’s take a look into it.

```yaml
version: "3.7"

services:
  db:
    build: ./db
    platform: linux/x86_64
    container_name: todos-db
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=todos
    image: db:1.0
    volumes:
      - type: volume
        source: db-volume
        target: /data/mysql

  app:
    depends_on:
      - db
    build: ./app
    container_name: todos-api
    environment:
      - DB_URI=root:root@tcp(db:3306)/todos?parseTime=true
    image: api:1.0
    ports:
      - "80:8080"

volumes:
    db-volume:
        name: db-volume
```

The `docker-compose.yml` schema requires the `docker compose schema version` at the top of the file, the latest version is `3.8` but any `3` version will work for this project in particular, if you are eager to learn more about the changes between versions, the change-log is published [here](https://docs.docker.com/compose/compose-file/compose-versioning/#version-3).

Immediately, we need to define our services, each of them require a name, for example, my database service is named `db` this names also **works as aliases within the network**.

Let’s take a closer look at db service.

```yaml
db:
  build: ./db
  platform: linux/x86_64
  container_name: todos-db
  environment:
    - MYSQL_ROOT_PASSWORD=root
    - MYSQL_DATABASE=todos
  image: db:1.0
  volumes:
    - type: volume
      source: db-volume
      target: /data/mysql
```

Ok, now field by field:
- `build` specifies which Dockerfile to use to create this service, in the project’s repository, we have a `db` folder, and within in we have a `Dockerfile` with some customizations for our database
- `platform` is not actually necessary, but a good tip in case you are working with an Apple Silicon Processor.
- `container_name` as you might have guessed, specifies the name of the container created, is equivalent to `docker run -n` flag
- `environment` allows us to specify environment variables used by the container, is equivalent to `docker run -e` flag
- `image` is for specifying the tag for this image. Is equivalent to `docker build -t` flag.
- `volumes` is a list of the `docker volumes` being used by this container

Pretty straightforward, right? There are many more fields that can be specified, `docker compose` allows customizing each aspect of the containers, this configuration is a pretty simple, but solid, one.

But we have another service, app is for defining my REST API application container, let’s take a look at it.

```yaml
app:
  depends_on:
    - db
  build: ./app
  container_name: todos-api
  environment:
    - DB_URI=root:root@tcp(db:3306)/todos?parseTime=true
  image: api:1.0
  ports:
    - "80:8080"
```

And again, let’s explore each field:
- `depends_on` is a quite useful field, it allows us to make our container wait until the service specified here is up, since we depend on a database to store To-dos, it is important to wait until the `db` service is up.
- `build` again, receives the context to build a new image, within the app folder we have a Dockerfile to build the API
- `container_name` as you might have guessed, again, is for naming our container
- `environment` allows us to specify environment variables used in our containers, this time we only use one variable, but as you can see, the URI points to `db:3306` remember when I said _this names also works as aliases within the network_? `docker compose` creates a network with each service connected to it; therefore, we can point each service using its alias.
- `image` again is for tagging our images
- `ports` is a list of the ports exposed by this container and the mapping to the host’s actual ports, is equivalent to `docker run -p` flag

##`docker compose up`
Ok, but now what? As I said at the beginning of this post, `docker compose` allows us to run all our containers with a single command, this command will build all the images (if they don't exist) for our application and also run all the required containers.

But that’s not it, as I mentioned, it also creates a new `docker network` and attach all the containers to it, same thing for all the volumes specified in the volumes field at `docker-compose.yml`.

Let’s then run it using the `-d` flag to run our containers in the background.

```bash
docker compose up -d
```

If the images don’t exist, all the building log will be displayed; otherwise, the output should be something similar to:

```
[+] Running 2/0
 ⠿ Container todos-db   Running                                                                                                                   0.0s
 ⠿ Container todos-api  Running   
```
As you can see, the `*.container_name` field is already reflected here.

Then if you list your images you should see something like:
```
$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED             SIZE
api          1.0       b5c4cf711443   49 minutes ago      18.1MB
db           1.0       53c16ac987e5   About an hour ago   517MB
```

Again, the field `*.image` was used to name our images and add the version of the image to it.

But let’s see our containers! The output will be something similar to:
```
$ docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                  NAMES
806258079a07   api:1.0   "./server"               4 minutes ago   Up 4 minutes   0.0.0.0:80->8080/tcp   todos-api
d01357663cea   db:1.0    "docker-entrypoint.s…"   4 minutes ago   Up 4 minutes   3306/tcp, 33060/tcp    todos-db
```

Where you can see the port mapping that we used in the app.ports field the name in `*.container_name` fields and even the `*.image` name.

Finally, let’s take a look at our Docker networks!

```
$ docker network ls
NETWORK ID     NAME                    DRIVER    SCOPE
f8d14f7ddf0b   dockercompose_default   bridge    local
```

There is a new network, being used for this specific project, and we didn’t need to do nothing! It was automatically created and all the containers are already attached to it.

By the way, you can test the API with these commands:

```bash
# Create a new TODO
curl -X POST http://127.0.0.1/todos -H "Content-Type: application/json"  -d '{"description": "Write a comment!"}'

# Get TODOs
curl -X GET "http://127.0.0.1/todos"
curl -X GET "http://127.0.0.1/todos/1"

# Mark as complete
curl -X PUT "http://127.0.0.1/todos/1?completed=1"

# Delete a TODO
curl -X DELETE "http://127.0.0.1/todos/1"

```
###Conclusion
`docker compose` is a great tool, even it is not the best option to be used in production environments, is really useful to replicate different environments in different hosts, quick, easy to read and easy to share. I encourage you to build your next project using a `docker-compose.yml` file!

####Note from the OP
Hi! Long time no see, I recently moved so I was very **very** busy, but I’m back with many ideas and drafts for new posts!

Thank you so much for all your support and reading my content, I’ll really appreciate any feedback or suggestion!