Docker Guide

Bakery allows you to easily bake an AWS AMI which can be shaped using your Ansible playbooks, any way you want. Your playbook is supposed to leave a bake EC2 in a state that allows to create an AMI of it, which when launched will run your application, probably as a Linux service.

You may also want to run your application not as a Linux service on an EC2 instance but inside a Docker container to enjoy from greater portability. Let's say you have successfully built a Docker image that runs your application. How do you go about baking an AMI that pulls down this image and runs it, thus executing your application inside a container?

That is exactly what this guide is about. It takes a sample Rails application and goes through all the steps necessary to run it in a Docker container.

The Application

Before we start, we need to choose a sample application. It so happens that the AWS Ruby team has released a Rails demo application that we will use for the purpose of this guide. It is a simple TODO list and when run it looks like this:

TODO Sample App

I've forked this application and added all files necessary for it to be built and run as a Docker container:

The Flow

Before diving into the code I'd like to review what we're going to do and how we're going to run our application.

  1. We're going to build a Docker image containing our Rails application.
  2. Then we're going to build, test and publish this image on CircleCI, using its convenient Docker integration.
  3. After that we're going to develop an Ansible playbook that will auto-start a Docker container when Ubuntu Linux instance boots up.
  4. Following this step we're going to set up a Bakery pipeline to bake an AMI using Ansible playbooks from step 3 to auto-start a Docker container built at step 2.
  5. Once our AMI is baked we're going to launch it as a new EC2 instance. When this EC2 instance is started or restarted - our image will be pulled down from Docker Hub and run automatically.

Now, let's review each section in detail.

1. Docker

When building a Docker image the first question you need ask yourself is which image to base it on. In this case I've used a "evgenyg/ruby" Docker image with Ruby 2.1.3 distribution that I've built previously. So our Dockerfile for a Rails application is going to look like this:

FROM       evgenyg/ruby
ADD     .  /todo-sample-app
WORKDIR    /todo-sample-app
EXPOSE     3000
RUN        ./docker/
CMD        ./docker/

It runs a small "bundle install" script when the image is built and "rails server" script when container runs. It uses a CMD command meaning it's just a default used only when no other command is specified. So you can still run this image as "docker run <image> bundle exec rake <task>" which we're using to execute Rake tests task when testing an image on CircleCI.


A word on database. My fork of this application uses MySQL database and a "development" Rails environment. Database host and credentials are specified with DB_HOST, DB_USER, DB_PASS environment variables so every time we run the application we need to make sure these 3 environment variables are defined and are properly passed to Docker.

For running MySQL I used an AWS RDS service, an easy way to have your MySQL instance up and running in a matter of seconds.

Build Docker image locally

To make sure you have a functional Docker image you can build and run it locally:

docker build -t=todo-sample-app .
docker run -dp 3000:3000 --env DB_HOST --env DB_USER --env DB_PASS todo-sample-app

As I just mentioned, you need to have your DB-related environment variables defined.

2. CircleCI

After seeing how Docker container can be built and run locally it is now a time to automate its creation! We want to build, test and publish our Docker image to Docker Hub every time a change is pushed to a GitHub repo. From many cloud CI services available we enjoy using CircleCI but the process shouldn't be different with Travis, Shippable, Drone or Codeship (could you imagine we'll have so many amazing CI services to choose from just a year or two ago?)

Here's our "circle.yml" for building, testing and deploying an image. A complete reference can be found here.

    - docker

    - "~/docker"
    - docker info
    - if [[ -e ~/docker/ruby.tar ]]; then docker load -i ~/docker/ruby.tar; fi
    - docker build -t=evgenyg/todo-sample-app .
    - if [[ ! -e ~/docker/ruby.tar ]]; then mkdir -p ~/docker; docker save -o ~/docker/ruby.tar evgenyg/ruby; fi

    - docker run evgenyg/todo-sample-app bundle exec rake test
    - docker run -dp 3000:3000 --env DB_HOST --env DB_USER --env DB_PASS evgenyg/todo-sample-app
    - sleep 10
    - curl -f http://localhost:3000

    branch: master
      - docker login -u evgenyg -p $DOCKER_AUTH -e $DOCKER_EMAIL
      - docker push evgenyg/todo-sample-app

    - docker stop $(docker ps -a -q)
    - docker kill $(docker ps -a -q)

Once this file is pushed to the repo, a new project can be added in CircleCI. It'll run immediately and fail as we haven't specified any DB-specific environment variables. Neither we specified our Docker Hub credentials to use for pushing the image. This can be done in a secure manner using CircleCI mechanism for specifying project's environment variables without committing them to repo (which you never do anyway, right?). These are the variables that you need to have specified:


Once all variables are provided you can re-run your build and it should look like this:

CircleCI build log

When finished successfully your image is deployed to Docker Hub! Mine is available at "evgenyg/todo-sample-app".

3. Ansible

Now, having our Docker image built, tested, and deployed automatically to Docker Hub we can take care of an Ansible playbook to start it automatically.

Here's the playbook:

- name:  TODO Sample App auto-run
  hosts: all
  sudo:  yes

    - { role:       docker,
        app_name:   'todo-sample-app',
        ports:      [ 3000 ],
        image:      'evgenyg/todo-sample-app',
        env_vars:   [ 'DB_HOST', 'DB_USER', 'DB_PASS' ] }

This playbook relies on a "docker" role for installing Docker and auto-starting an image when Ubuntu Linux instance boots up.

  • "app_name" is your application's name. You'll find Docker and application logs at "/var/log/{{ app_name }}.log" and Upstart init script at "/etc/init/{{ app_name }}.conf".

  • "ports" is a list of ports exposed by a running container. Each container port specified is exposed under the same port number as a host port. You could see it in "circle.yml" above when we ran "curl -f http://localhost:3000" to ensure the application is up and running.

  • "image" is an image to auto-run on boot, the one we build, test and deploy to Docker Hub using CircleCI.

  • "env_vars" is a list of Ansible extra variables passed by Bakery when it runs your playbook. These variables are converted into environment variables file at "/opt/{{ app_name }}.env" that is fed to container when it starts.

4. Bakery

We can now turn our attention to Bakery to bake an AMI which will automatically start our image when launched as an EC2 instance.

Setting up a new pipeline in Bakery consists of 3 steps:

  • Setting up a Git repository:

Bakery - Git Repo

  • Setting up an AWS account:

Bakery - AWS Account

  • Setting up the pipeline:

Bakery - Pipeline

A successful bake of an AMI takes about 10-15 minutes and looks like this:

Bakery - Successfull Bake

5. TODO Sample App

We're almost done! Having a successful bake process means you're now having an AMI created in your AWS account:

AWS - TODO Sample App AMI

So all that is left is to launch it. When your image is started, visit its public IP on port 3000:

AWS - Started EC2

And we're done!

Running TODO Sample App

6. Summary

As you see, the entire process of setting up a Rails application to run in a Docker container may take a while. But that's because a number of different technologies are involved here. We had to deal with Dockerfile for our application, CircleCI configuration for building, testing and publishing the image, Ansible playbook to auto-start it, Bakery to bake an AMI and final EC2 instance to launch. That's quite a few moving parts to take care of.

We hope you've found this guide useful. Here are some additional references that may help you:

These two repos can be very helpful in bootstrapping your own playbooks.

What do you think of this page? Tell us about it