Welcome back to our series covering all things Docker. In our previous article, Docker for Beginners: Deploying Containers & the Anatomy of Commands, we covered a basic test example of deploying a Ghost webpage inside a Docker container, as well as analyzing the components that make up the command we used to execute it. In this article, we’ll cover Docker-Compose, an addon to Docker which makes managing and replicating your containers even easier. We’ll also recreate our Ghost website test page from the previous article, but using Docker-Compose.
So, to get started, what is Docker-Compose?
Introducing Docker-Compose
Remember how in the previous article we mentioned that docker run
isn’t typically used for production deployments? This is because all the variables and options for our deployment are merely flags in a command. This isn’t ideal because there could be multiple environment variables, port mappings, volumes, and more.
For instance, what if we want to have a service that has multiple containers working in tandem? We would have to type docker run
multiple times, which sounds quite silly for managing large deployments.
Or, what about scenarios where we need multiple containers to interact in a reproducible and consistent manner?
This is where Docker-Compose comes in. Docker-Compose is a tool built on top of Docker that allows one to manage entire Docker deployments within a docker-compose.yaml file.
But what does that look like?
Let’s take the same Ghost service we used in the previous article and modify it for a typical production scenario with Docker-Compose.
Docker-Compose File for Running a Ghost Website
Here is our Docker-Compose file for converting our test instance of Ghost from the previous article to a production instance.
1 version: '3.8'
2
3 services:
4
5 ghost:
6 container_name: ghost
7 image: ghost:latest
8 restart: always
9 ports:
10 - 8080:2368
11 environment:
12 database__client: mysql
13 database__connection__host: db
14 database__connection__user: root
15 database__connection__password: example
16 database__connection__database: ghost
17 url: http://localhost:8080
18 networks:
19 - ghost-net
20 db:
21 container_name: db
22 image: mysql:8.0
23 restart: always
24 volumes:
25 - ghost-data:/var/lib/mysql
26 environment:
27 MYSQL_ROOT_PASSWORD: example
28 networks:
29 - ghost-net
30 networks:
31 ghost-net:
32 volumes:
33 ghost-data:
34
Now, before diving into the structure of a Docker-Compose file, we need to know a little about .yaml. All Docker-Compose files are in the .yaml format. In this format we don’t use brackets, semicolons, or tabs, because .yaml files are structured with spacing and colons instead. It’s important to know that every value that moves forward will increase by 3 spaces.
Every Docker-Compose file created should either be docker-compose.yaml or docker-compose.yml. Otherwise, the filename will need to be explicitly stated when using Docker-Compose CLI commands.
In the example above, I have created a docker-compose.yaml file for our Ghost website. Notice there are two containers defined, not one. We have added a MySQL container so our production Ghost instance will have a database with better performance.
Docker-Compose Structure
In the Docker-Compose file example above, we see there are several properties followed by colons, then followed by a value on each line. Let’s run through these individual values to get a better idea of how a compose file works.
version – The version of the Docker-Compose file-format. This dictates which compose parameters (properties) and types of syntax are available to use. I recommend Docker-Compose version 3.8 as that has the most features and syntax available. Be mindful however that some Docker-Compose files on the internet will have certain features and/or syntax that work best with an earlier version.
services – A property that is always placed in a Docker-Compose file before the individual services. A service is the definition for a single container in the compose stack along with all its properties and values.
ghost – Ghost is the name for our service containing the Ghost image. Further below, db is also the name for the service for our MySQL container. We see our service has a single image, along with restart, ports, environment, and network properties.
container_name – Simply a property that tells Docker to name the container with the following value. This is useful because Docker will sometimes add a prefix to the container name. This value forces Docker to use Ghost as the container name with nothing tacked on.
image – The property that tells our service which container image it will be using. Here we are stating that we want the latest image of the Ghost image. Notice below with MySQL we stated a version to use, which was version 8.0 of MySQL. Sometimes stating the version explicitly is better in case an update causes unwanted changes.
restart – Restart is a policy that determines when a container should be restarted in case it stops. In this case, always states it should always be restarted unless the container is removed.
ports – The property that determines which ports the container uses. The left side value is the port used by the host, the right side value is the port used within Docker. We can think of the left value as the port that is publicly reachable and the right value as the port used to communicate with other containers within.
environment – The property for any environment variables for the Docker image being used. These can vary per image. For Ghost we have variables that allow our Ghost image to interface with our MySQL container.
networks – Defines what Docker networks our containers will be using. For two containers to communicate with one another, they will need to reside in the same network. You can think of a Docker network as a VLAN in Docker terms.
volumes – Defines a volume for a Docker container. For our MySQL container we define a volume named ghost-data that maps to /var/lib/mysql in the container.
The /var/lib/mysql directory is where the MySQL container stores its databases. So when we map our volume to that directory, we declare the database for the container as persistent.
If we decide to kill and remove both containers, our database is still intact because of our volume declaration.
At the very bottom we declare our volumes and networks again. This is because we might share volumes or networks across containers in our compose stack. Because of this we also state them at the bottom so Docker knows which volumes and networks to create.
They can be declared with no options like above, or they can have special properties like driver and external. For instance, if we desire a network to be available to containers created in different compose files, we configure our network as such:
1 networks:
2 ghost-net:
3 external: true
Now our ghost-net network is available to containers running outside of our compose file.
To learn more about how compose files work, I recommend visiting the official compose file reference:
Deploying with Docker-Compose
We now have a docker-compose.yaml file that can be used for a production instance of our Ghost site.
To run the necessary commands, run these commands:
Debian/Ubuntu:
sudo apt install docker-compose
RHEL/CentOS/Fedora/Almalinux:
sudo dnf install docker-compose
or if on older version of CentOS: sudo yum install docker-compose
Now while inside the same directory as our compose file, run docker-compose up -d
. This command deploys all the containers in a compose file, and the -d
stands for detached. To bring down the entire compose stack and kill then remove the containers, run docker-compose down
.
Docker will then pull the images, extract them, and start the containers. View the same IP address of the hosts on port 8080 and the Ghost site will be present again.
With this compose file it’s possible to migrate or backup the data with our volume, edit our environment variables, change network configuration and more all within one file.
Working with Docker
So, that wraps up a lot of useful information on what Docker is, the advantages to using Docker, and how we can deploy services with Docker and Docker-Compose. But now, we need to understand how to work with Docker to accomplish tasks.
In our next article in this series, Working with Docker: Must-Know Docker Commands, we’ll cover some essential commands that will help any sysadmin utilizing Docker, and provide examples using the ghost and db container examples from this Docker-Compose article as a basis for creating more complex networks of containers. Read on to learn more or check out our blog and knowledge base for more great content and industry insights from the hosting experts at Hivelocity.
– written by Eric Lewellen