Docker Compose for Beginners

Sat 20 July 2019 | tags: software, docker, linux,

A lot of people get stuck on Docker in the "what is it?" phase. Is it a virtual machine? Is it a sandbox? How does one get things into it (and out of it)? Lord knows I had these questions when I first started.

Beginners tend to misunderstand that containers are meant to do one thing and one thing only, therefore, they load them up with everything needed for a project and then rush that out into the world. This quickly comes back to bite them when they need to do an upgrade to one part of the whole and everything goes wrong.

The Golden Rule of Docker is to keep things separate and keep things pure. A MySQL container is meant to be just the code necessary to handle your database. It's not your database data, it's not your website data, it's not any of that. It is to remain stateless. Think of it like a VM that has a base system and only MySQL installed. Additionally, the system only had enough space to hold the MySQL process, not the actual database data. Beyond this, whenever you restarted that machine, every setting you changed was set back to the default. That's essentially what a pristine container is.

Bringing It All Together

So that sounds great as long as I only need MySQL. What if I'm using an app that requires multiple processes? For instance, Wordpress? You need the Wordpress application and MySQL for anything to even work. Should we make a container that has both in it? (Hint: the answer is no.)

Because we want to keep these things separate, we'll create two separate containers to process our content and simply point them at each other. The Wordpress application will talk to MySQL when it needs to. Sounds great in theory, but how? As with everything awesome, this can be set up with a simple yml file. Here is an example file for Wordpress that we will then breakdown.

version: '3.3'

services:
   db:
     image: mysql:5.7
     volumes:
       - db_data:/var/lib/mysql
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: somewordpress
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: wordpress

   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     ports:
       - "8000:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD: wordpress
       WORDPRESS_DB_NAME: wordpress

volumes:
    db_data: {}

Breakdown

version: '3.3'

This is just letting Docker know what version of their docker-compose syntax you'll be using. Version 3 is the current version, but I still have some version 2 compose files around and they still work.

services:

This lets docker know that you are going to be defining the services or processes that your application will use for the work it will accomplish. The section directly beneath this will define each of those services.

   db:
     image: mysql:5.7
     volumes:
       - db_data:/var/lib/mysql
     restart: always
     environment:
       MYSQL_ROOT_PASSWORD: somewordpress
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: wordpress

Here, we are defining all the aspects of the MySQL process. We give it an easily memorable name with the db: line. We can use this name as a dns entry for any other container in the same docker network. In the image: mysql:5.7 line, we are telling docker which version of MySQL we want to use. This is the name of a docker image on docker hub. Most major softwares are available as a single name, but images created by third parties are usually available in a username\image format. If you use one of those, you need both the username and image names in this section. The volumes: section is about declaring where your storage should be located. As I mentioned before, docker is meant to be stateless. No data should be stored in your MySQL or Wordpress containers. So we have two main choices about where to store the data: either in a storage-only docker container or mapped directly to the host system's filesystem. Each has their own advantages and disadvantages (which I won't get into now). The important thing to notice is that I'm not storing the entire filesystem in the storage container, just anything under /var/lib/mysql, that is, the database itself. This is the only folder on the MySQL container that will change when we introduce our database to it, so it's the only one we need to save. The restart: always field is pretty easy to understand. If the container should fail for any reason, restart it. Then we set some environmental variables for use in the program. In this case, the db user/passwd for Wordpress to use. So MySQL is ready, but we still don't have Wordpress set up. We need to define another service!

   wordpress:
     depends_on:
       - db
     image: wordpress:latest
     ports:
       - "8000:80"
     restart: always
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD: wordpress
       WORDPRESS_DB_NAME: wordpress

Just as with the db container, we start by giving this container a helpful name wordpress:. What's next? Oooooh, a new thing. With depends_on: we are telling Docker that this service can't really run unless another one is running. Why didn't we define that relationship for db though? Simply put, you CAN run MySQL without running Wordpress, but Wordpress cannot do much of anything without MySQL. Again, with image: wordpress:latest we are telling Docker exactly what version of the software we want. The ports: section tells Docker how to connect up ports that are part of the host machine and ports that are internal to the container, respectively. So in the next line "8000:80" we are telling Docker to make available (via port 8000 on the host) whatever is available in the container on port 80. Therefore, in order to access this Wordpress site, we'd have to put in http://ip.address.of.server:8000. restart: always is the same as before and the environment: section should also look familiar as it goes over the same things the previous section did. Basically declaring the db user/passwd that the db user should use. (Also note at for the host, we just put db:3306. We didn't need to declare the ports above in the services section because these two containers will exist in the same Docker network allowing them to communicate with each other. Additionally, we didn't need to guess at the ip address of the db service because Docker will automatically handle the DNS lookup of other resources in the same Docker network. Its a bit like having a network subsection hosting your db and wordpress services. You only need to make sure that port 80 on the wordpress service is reachable from the outside.

volumes:
    db_data: {}

This final little part just defines the db_data container that we chose for storing our database in.

Notice how I don't have any storage set up for the Wordpress container? This means every time I start up the Wordpress container it will be completely brand new (except for anything that resides in the database). So, all my posts, users, etc. will be restored, but what if I want to use a custom theme? What if I want to store images in the instance for use in other places? I have two options: store these things in another container as I did with the database:

volumes:
    - wp_data:/var/www/html

or store it alongside my docker-compose file:

volumes:
    - ./wp_data:/var/www/html

You'll immediately notice that this is very similar to the way we defined our ports in host:container format. We want to map the containers /var/www/html folder to a docker container called wp-data or to our localhost filesystem at <this_directory>/wp_data. (Note: if you define your storage as a docker container, you'll need to add it below the declaration of the db_data container)

Launching the Containers

So we've created the docker-compose.yml file. What do we do with it? That's the easy part! Just change to the folder containing the docker-compose.yml file (my usual file structure is /docker/application_name/docker-compose.yml for example /docker/wordpress/docker-compose.yml). Once you're in the directory just run:

docker-compose up -d

This command launches all of the containers and hooks up any networking and storage you described in the yml file. The -d tag simply launches it in the background so you don't need to keep your terminal session open for it to continue. However, if you experience an issue, it can sometimes be helpful to launch without the -d tag so you can see what is happening and what container might be failing out.

Shutting it Down

So we can start it up, but how do we shut it down? Equally simple. Just run:

docker-compose down

This gracefully shutdowns the containers and saves any data that may be being processed.

The Takeaway

Finding docker-compose has changed the way I use Docker. Despite the fact that it's typically used to orchestrate multiple services, I now use it for launching even single container apps. It's a great way to have a completely reproducible build of a docker container. Before, I would save my docker container launch code in a bash file and hope for the best, but docker-compose.yml is a much more elegant solution.

Additionally, it's been very easy to get people started with Docker because of docker-compose. I can share my working configs and I know that the build-once, run-everywhere aspect of Docker will make it much easier to get services up and running.