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:
- Run the database on your machine, and run the app inside the container.
- 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.
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:
Let’s open the file and start by defining our application as a new service. We
api. You define a service by adding it as a new top-level element in
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
Dockerfile. In our case, this would look like this:
Great job! You can now start the application by executing
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
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
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.
Hope to see you in the next part!