Transcription
Docker Compose is a tool that allows you to manage multiple Docker containers as a single unit. Forget about long Docker run commands for starting each container and project startup scripts; Compose does everything for you.
In this video, we will explore what Docker Compose is, why it is needed, how the Docker Compose YAML configuration file is structured, and how to deploy using Docker Compose.
Let's start with what Docker Compose is and its purpose, using an online store application as an example. Architecturally, such an application can be represented as follows: there is a database that stores information about products, orders, customers, and other data. There is a backend service that handles the main logic of the application, such as order processing, payment acceptance, and search result generation.
We may also have a search engine that helps create relevant product listings based on user queries. Additionally, there is a web application, perhaps written in React, that interacts with end users, and this web application communicates with our backend service.
Besides the components mentioned, there can also be separate services for admin panels, analytics, and more. As you can see from this example, even a seemingly simple application can consist of many components. Docker helps us package each component of the system into an image, which can then be run in a separate container.
Deploying an application made up of multiple containers can also be a challenging task, especially when the services depend on each other and need to be started in the correct order. This is where Docker Compose comes to the rescue.
While Docker is used to manage individual containers that make up an application, Docker Compose is used for simultaneous management of multiple containers. Compose simplifies application management by allowing you to easily manage containers, networks, and volumes, describing everything necessary in a clear YAML configuration file.
Then, with a single command, you create and start all the services defined in this configuration file. Docker Compose works in all environments: production, staging, development, testing, and CI processes. It also provides commands for managing the entire lifecycle of your application: starting, stopping, and rebuilding services, viewing the status of running services, checking logs in real-time, and executing one-off commands on a service.
Let's look at how to work with this tool. If you have installed Docker Desktop locally on your computer, Docker Compose should also be available along with the main Docker tools. You can check if Docker Compose is installed with the command `docker compose version`. For example, I have version 2.29 installed. In older versions, you would need to write `docker-compose` with a hyphen. However, in my case, with version 2.29 on macOS, that would be considered an error.
If Docker Compose is missing, you can install it using standard package managers with a command like `brew install docker-compose` or `sudo apt install docker-compose-plugin`, depending on your system.
Now, let's see how to use this tool. We will study Docker Compose using the previously discussed online store web application, which consists of several parts: frontend, backend, database, and search service. We will use PostgreSQL as the database and Elasticsearch as the search engine. The web application is written in React, and the backend is in Java using the Spring framework.
You don't need to know all these technologies to work with Docker Compose right now; this variety of technologies is chosen specifically to illustrate how Docker Compose helps organize an application made up of completely different parts. For PostgreSQL and Elasticsearch, we will use ready-made images of these services, while we will package the backend and frontend into containers ourselves using Dockerfiles.
The project structure of our application is currently on the screen. Without going into the details of the service implementations, let's look at the main points in organizing the project. There are two folders: `backend` for the Java application service and `frontend` for the React application.
In addition to the code for the services, these folders also contain Dockerfiles for building these applications. For the backend, it is a standard two-stage file: in the first stage, the project is built, and in the second, the artifact is copied and the application is launched on port 8080.
For the frontend, it is a bit more complex. Here, we also see a two-stage file where the first stage involves building through `npm install` and `npm build`, and in the second stage, the artifacts from the first stage are copied. Then, a script `docker-entrypoint.sh` is executed, which creates a configuration file for Nginx based on a template with parameters passed from environment variables, and then the Nginx server is started with our React web application on port 80.
If these files seem unclear to you, I recommend checking out the video on Docker basics on my channel. A link to it should appear now in the upper right corner, and it will also be in the description of this video. And of course, feel free to ask questions in the comments about any points that are unclear.
At this stage, it is important to understand that we already have ready-made backend and frontend services that are packaged according to their own rules into Docker images. For the frontend, we need to specify in the environment variables the host and port that the backend will be accessed at. For the backend, we need to specify the connection parameters to the PostgreSQL database and the Elasticsearch search engine. We will do all this using Docker Compose.
The first thing we need to do is create a configuration file. This file should be created in your project directory and must be structured in a specific way. In older versions of Docker Compose, at the beginning of the file, you need to specify the Compose file version. The version determines the format of the Compose file and its compatibility with the Docker Engine. It looks like this: `version: '3.8'` if we want to use version 3.8.
However, in modern versions of Docker Compose, specifying the version is not necessary, and we will not do that. The next key element of the file is the `services` property. Here, we describe all parts of our application that we will run in containers. In our case, we need to describe four services: backend, frontend, db, and search. The names can be completely arbitrary; the main thing is that they are understandable to you.
For each of the planned services, we need to describe several things: how to build or where to get the image for the service, which services the current service depends on so that Docker Compose can start everything in the correct order, and also define environment variables, volumes, and which ports should be opened for this service.
Let's start with the database service. The first thing we need to define is the container image. We will take a ready-made image of PostgreSQL version 16, so we write `image: postgres:16`. This image will be downloaded from the remote repository Docker Hub.
Next, we can define database parameters such as password, username, and database name. You can find all available configuration parameters on the image page in Docker Hub. We will only set a few of them now by using environment variables in this container. To define environment variables, we need to write `environment` and then specify which variables and their values we want to set.
We will set `POSTGRES_USER` and `POSTGRES_PASSWORD`. We can set the values of the variables directly in this file, meaning we could write `POSTGRES_PASSWORD: '1234'`. However, for parameters like login and password, it is better not to do this. Instead, we will pass these parameters from external environment variables.
We will write `POSTGRES_PASSWORD: ${DB_PASSWORD}`. What is happening here? We are setting the environment variables in the PostgreSQL container: `POSTGRES_PASSWORD`, `POSTGRES_USER`, and `POSTGRES_DB` with values from the environment variables when this file is executed, which will be named `DB_PASSWORD`, `DB_USER`, and `DB_NAME`. These variables can be passed as arguments when running Docker Compose or by creating an environment variable file.
Let's create an `.env` file and write the variables there.
Next, let's move on to the health check. Health checks are necessary when the operability of other services depends on the state of the current service. In our case, the backend service depends on the database, and if the backend service is started before the database is ready to accept connections, that startup will fail with an error.
Therefore, it is important to wait for the database to be fully operational. To determine the conditions under which we understand that the database is ready, we need to write a `healthcheck`. Here, we specify the command that will check the operability of this service, as well as the retry policy and timeouts for checking this condition.
Let's write `test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}"]`. This is a standard command for checking readiness. We will define the interval as 10 seconds, the timeout as 5 seconds, and the retries as 3.
Now, for the database, we need to define a data volume. If we don't do this, the data will be stored in the container and may be lost when it is recreated. This is not the behavior we expect from a database, and we need data persistence that is independent of the container's lifecycle.
To do this, we define `volumes`. Let's write `volumes: db_data:` and specify the standard path for PostgreSQL data. The volumes in the Compose file are defined in a separate `volumes` section.
Let's write `volumes:` and our volume `db_data`. If you have any questions while watching, don't forget to ask them in the comments.
And with that, the configuration for the database service is complete, and we can move on to the next service: the search service. The Elasticsearch search service is defined similarly. Here, we also specify the base image on which the container is built, environment variables, health checks, and data volumes. There is nothing new compared to the database.
Now, let's move on to the backend service. We will write the image name: `image: shop-backend`. Unlike the database and search services, there is no image with this name on Docker Hub, so we need to build it ourselves.
To do this, we write `build:` and specify where to look for the Dockerfile to build this image. Our Dockerfile is located in the `backend` folder relative to the current Compose file, so we write `context: ./backend`.
This service depends on the database and search services, so we need to explicitly state this for the correct order of container startup. We write `depends_on:` and specify `db`. We will also indicate the condition under which we check that the service is available. The service indicates that we need to check the health check we defined ourselves. If this is not specified, by default, it will check based on the container's startup condition, and similarly for the search service.
Finally, we will define environment variables for this container. The application is written in Spring, so we will set Spring Boot-specific environment variables where we specify the connection parameters to the database and Elasticsearch. Here, we refer to the database and Elasticsearch by the names defined in this file: `db` and `search`.
We will also take the database connection parameters from external environment variables. Remember, we defined them separately in the `.env` file.
With that, the backend service definition is complete, and we only need to describe the frontend service. Let's do that.
The beginning is the same as in the previous case: we write the image name and the build section. The frontend is a service that needs to be accessible externally so that external users can interact with it. To do this, we need to expose the application port. We write `ports: - "8080:8080"`, meaning the host system's port 8080 is mapped to our frontend.
To know where to send requests, we write `environment: BACKEND_HOST: backend` and `BACKEND_PORT: 8080`. This service depends on the backend, so we will specify that.
In total, we have defined four services: database, search, backend, and frontend, which depend on each other, have volumes, and open ports for external access.
Let's run this configuration file. This is done with a single command: `docker compose up --build`. We need this to build the frontend and backend images. In older versions of Docker Compose, you would need to write the command as `docker-compose up --build`, but in the rest, the command looks similar.
Now, the command is downloading the missing images from Docker Hub and building our own. After the images are ready, the containers will be started in the specified order.
We see that the search service is in a "ready" state, and only after that does the backend service start, just as we specified.
Let's take a look at the fruits of our labor. Let's go to the browser and see if we have reached our React application. Let's try to navigate to the first card. Everything works, and let's write a query for "Apple." We see all the products that match the search.
This means all components of our application are working as they should: the frontend renders a beautiful UI, the backend processes requests, and Elasticsearch performs the search, while the database retrieves the necessary data.
To stop the Compose execution, we will execute `Ctrl + C`. For a subsequent restart, we no longer need to rebuild; we just need to write `docker compose up` and, for example, run it in detached mode. To do this, we write `-d`.
We see that the application starts up much faster since all the necessary images are already downloaded and built. To view the latest logs, we can use the command `docker compose logs`.
To see the list of running Compose containers, we can use the command `docker compose ps`. Here is the list of our containers. If we want to stop all our services, we just need to write `docker compose down`. We see that the services are stopped.
Let's summarize briefly: Docker Compose is a tool that allows you to manage multiple Docker containers as a single unit. All configuration is done in the Compose configuration file. In the `services` section, we describe all services of our application, and in the `volumes` section, we specify the volumes used for data.
When defining a service, you need to specify the base image and how to build this image. Environment parameters are described in the `environment` section, and the order of container startup is determined by the `depends_on` conditions and health checks. The `ports` section is used to specify which ports we open externally, and in `volumes`, we describe the volumes used.
This is far from everything that Docker Compose offers, but it is enough for a confident start and use in your work. I recommend downloading the project and experimenting with it and the Compose file. A link will be in the description of the video.
As always, thank you for watching, and see you in the next video!