Making Docker an art with Compose

After bundling Sinatra in a Docker container in the first part of my Learning Docker series, let’s look at orchestration with docker-compose now. This is the foundation of the multi-container environment we want to build. Get this right, and adding a database is just two lines of code. Sounds interesting, right?

Remember the goal

Before we dive into docker-compose, let’s quickly recap what we set out to do:

The goal for this whole series is to create a Sinatra app that serves a JSON API. Since it should be a template for something you can use in production, we also wanted to add a database from which the app retrieves its content.

There are two different ways how you could achieve this:

  1. Run the database on your machine, and run the app inside the container.
  2. Run both the database and the app inside a container.

The first approach is pretty straightforward. You might already be running a database on your development machine, and accessing it from within the container is simply a matter of your network configuration. However, the downside to this approach is that you deprive yourself of the chance to build a truyl portable and reproducable development environment.

Let’s take a look at docker-compose to see what I mean by that.

Introducing docker-compose

Instead of trying to reinvent the wheel, I’m just going to quote the documentation of docker-compose to tell you what it does:

Compose is a tool for defining and running multi-container Docker applications.

How does this help us?

Let’s think about the different components our app has: From the standpoint of a consumer, our app has an API that provides content. Where things get interesting now is the fact that the content has to come from somewhere. This could be a database, an in-memory data store (redis, memcached, …) or simply a file.

While your first instinct might be to pack the data source in the same container as the application (mine certainly was!), separating them into different containers makes way more sense. First off, you can scale them independently if you need to, which is awesome for production. Second, you apply the principle of single responsibility, and create containers that serve a single very specific purpose.

And thanks to docker-compose, taking this approach is dead simple.

Defining our service(s)

Before we can use docker-compose to start our application, we need to configure it. This is done via a docker-compose.yml file. You can read about all the available configuration parameters in the Compose file reference, but for now the Overview is more than enough.

We add the docker-compose.yml file to the top-level folder of our project. The directory should look like this:

$ tree .
.
├── Dockerfile
├── app
│   ├── Gemfile
│   ├── app.rb
│   └── config.ru
├── docker
│   └── vhost.conf
└── docker-compose.yml

Let’s open the file and start by defining our application as a new service. We name it api. You define a service by adding it as a new top-level element in the docker-compose.yml file:

api:

The next step is to tell docker-compose how to build the service. This is done with the build directive, which should point to the service’s Dockerfile. In our case, this would look like this:

api:
  build: .

Great job! You can now start the application by executing docker-compose up. You should see a bunch of output that either tells you that the application got build or that it got started.

There is one thing that doesn’t work yet, and that is accessing the application. While it starts and runs, the port the application uses inside the container is not mapped to a port on either the docker-machine or localhost. We need to tell docker-compose to do that with the ports directive:

api:
  build: .
  ports:
    - "4567:80"

This tells docker-compose to map port 4567 on localhost (or docker-machine) to port 80 on the container. After restarting docker-compose, you should be able to access the app on the same URL as before and see its output "Hello World!".

Wrapping up

While this wasn’t a very long or technial post, its results provide a very nice foundation for our next endeavour: adding a database. This will make a true multi-container application out of your Sinatra app, and then docker-compose can show its full potential.

If you want to browse the code, you can find it on GitHub. For a snapshot of this specific version, see version 0.2.0.

Hope to see you in the next part!

Jan David