Docker Compose
Explore Docker Compose to manage multi-container applications effectively. Understand how to define and run services, configure networks, and use volumes for persistent data. This lesson helps you coordinate containerized microservices to build complex applications with ease.
The challenge of running multiple services
So far, we’ve focused on containerizing a single application and running a single container. But in reality, a fully functioning application is rarely made of one component. Most applications are made of numerous services.
Let’s consider an e-commerce SalesGuru’s website as an example. Like other applications, we could visit the website through our browser and register our account. We may even add items to carts and wishlists, add payment methods, and place an order.
A separate microservice could power each section of the application as shown in the image below:
We could have an order service responsible for managing our orders. A product catalog service could be used to manage and display product catalogs. The payment section of the application could be a separate service allowing customers to make payments.
Each of these services is known as a microservice.
Microservices are an architectural approach to building applications where pieces of an application work independently but together.
Each microservice runs in its own container. A product catalog service could be implemented in a language different from the order service.
Working with these separate services locally or in a CI environment can be excruciatingly difficult if there’s no easy way to manage them.
It could mean wrangling some Bash scripts to manage multiple containers, which includes starting and stopping them. It’s important for us to remember that some applications may require other services to be running before they can start. For example, some back-end applications require successful connections to the database before they can start.
We would have to keep these dependencies in mind and also remember the order in which our services should start.
Thanks to Docker Compose, which appears in the code as docker-compose, we don’t have to do all that. In fact, we don’t even need to write a fancy script to start multiple containers.
docker-compose
The docker-compose tool defines and runs multi-container Docker applications. With docker-compose, we define our services and determine how we want to start them in the YAML file. Then, we bring them up in one command.
With docker-compose, we can do the following:
- Start, stop, and rebuild our services.
- Examine the status of the running services.
- Stream the log output of the running services.
- Run a one-time command on a service.
The docker-compose tool is a separate tool maintained by Docker, Inc. It requires Docker Engine to function. Remember, it’s a way of managing multiple containers. Therefore, we can’t do meaningful work with docker-compose without a container engine.
The docker-compose tool is also included in the Docker Desktop for Mac and Windows. If we install Docker through Docker Desktop, docker-compose should already be ready and available for use.
The easiest way to verify docker-compose is shown below:
This is the output we get:
docker-compose version 1.29.1, build c34c88b2
If we’re running on Linux, we follow the steps below to install docker-compose:
- We run this command to download the current stable release of Docker Compose:
- To install a different version of
docker-compose, we substitute 1.29.2 with the version we want to use:
- We test the installation by running
docker-compose --version.
Try it out
The anatomy of docker-compose
The three fundamental components of docker-compose are services, networks, and volumes.
Services
Let’s return to the SalesGuru application as an example. There are order services, payment services, and so on. The docker-compose tool includes a section for defining all these compute components as services.
A service is an abstract definition of a computing resource within an application that can be scaled or replaced independently from other components. Services are backed by containers and are run by the platform according to replication requirements and placement constraints. Services are defined by a Docker image and set of runtime arguments.
On a high level, services are defined in docker-compse as follows:
Networks
There’s a network layer that allows various services to communicate with one another.
Let’s return to the SalesGuru application. After a successful payment, the payment service may be required to send an invoice. The payment service may thus obtain the user’s email address from another service.
To make these communications possible, some form of low-level networking must be in place. This is where networks come into play.
Networks are the layer that allows services to communicate with each other.
By default, docker-compose creates a single network for our application, and all services automatically join this network. This allows them to communicate with one another.
This means without defining networks in the docker-compose file, our services can still talk to each other.
However, there are cases where we may need a more advanced network configuration, and we may have to define our own network.
On a high level, network configurations are defined under the networks component in the docker-compose file as follows:
...
networks:
frontend-tier: {}
backend-tier: {}
Volumes
Volumes are persistent data stores implemented by the platform where we run our workloads.
Since containers are transient, it’s ideal for keeping persistent data outside the container. As a result, volumes store data generated and used by Docker containers.
If we want to preserve the generated data even when the container is killed, we should use volumes.
Another example is when a service needs to write data to a file or read data. The user service might store the user’s avatar to a storage volume. Another service might also read the users’ avatars from the storage volume.
On a high level, volumes are defined in docker-compose under the volumes section. Here’s an example of this:
....
volumes:
db-data:
driver: flocker
driver_opts:
size: "10GiB"
Putting all these together, a typical but simple docker-compose file looks like this:
Code explanation
- Line 1: We define the version of
docker-compose. - Line 3: We specify the
servicesconfigurations. - Line 4: We define the front-end service.
- Line 11: We define the back-end service.
- Line 18: We define the
volumesfor the back-end service. - Line 24: We define
networksconfiguration.