Continuous Integration of a Dockerized Rails Application

If you’ve ever worked with continuous integration then you know it’s a valuable tool. Having a CI server run your test suite after each push to a branch is great for discovering integration bugs and for building team confidence. Maintaining a traditional CI server, however, can be a pain. It’s close to, but not quite, a production environment, and it is not a development environment. Instead it’s a nuanced version of both. That difference inevitably leads to someone saying “tests passed on my machine, not sure why they are failing in CI.” If you don’t want to hear those words then I suggest you use Docker. If your whole team is developing in containers, and those same containers are used to run tests in CI, then you are much more likely to see green builds all day. That is CI nirvana, and achieving it was my goal after dockerizing my Rails application. Thankfully this was not difficult because of the awesome service that is CircleCI.

Not a word from a sponsor

First of all, I am not in any way affiliated with CircleCI. I’m simply a fan of their service. That said, in order for you to get maximum fulfillment from this tutorial I encourage you to open a free account so you can see everything work as described. That’s a recommendation. You can ignore it and use the commands in circle.yml (below) on Jenkins or some other hosted CI service. It should work because, ultimately, circle.yml is just a shell script. The only real prerequisite for this article is that you have a Docker Compose-based Rails application. Ideally you followed my other tutorial and are working with a similar setup. This article assumes that is the case. If you need an app to work with checkout my example on GitHub which has all the code from this article.

Continuous integration with Docker on CircleCI

CircleCI makes it really easy to get up and running with their service. In the default CircleCI environment my tests ran green on the first try. That’s impressive, but I wanted my tests to run in the containers I wrote them in. To make that happen I had to custom configure my CircleCI build. You can do the same by creating a file named circle.yml in the root of your Rails app with the following:

circle.yml # configure the build server # https://circleci.com/docs/configuration#machine machine: # set the time zone to your local so server logs are easy to understand # useful if you SSH into a build (https://circleci.com/docs/ssh-build) timezone: America/Chicago # tell CircleCI that we need Docker (Compose) available for our build services: - docker # set the Rails test environment at the server level environment: RAILS_ENV: test # configure project dependencies # https://circleci.com/docs/configuration#dependencies dependencies: override: # output information about the docker environment - docker info # create a .env file to configure our application container - | cat << VARS > .env SECRET_KEY_TEST=$SECRET_KEY_TEST POSTGRES_DB=$POSTGRES_DB POSTGRES_USER=$POSTGRES_USER VARS # build our Docker images - docker-compose build # configure the database # https://circleci.com/docs/configuration#database database: pre: # boot all containers - docker-compose up -d # wait a second to make sure all containers are up and running before the next step - sleep 1 # create our test DB and load the schema. The DB is named $POSTGRES_DB, as defined in our environment. override: - docker-compose run app rake db:create db:schema:load # configure the test run # https://circleci.com/docs/configuration#test test: # run the test suite in the application container override: - docker-compose run app rspec spec 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 # configure the build server # https://circleci.com/docs/configuration#machine machine : # set the time zone to your local so server logs are easy to understand # useful if you SSH into a build (https://circleci.com/docs/ssh-build) timezone : America/Chicago # tell CircleCI that we need Docker (Compose) available for our build services : - docker # set the Rails test environment at the server level environment : RAILS_ENV : test # configure project dependencies # https://circleci.com/docs/configuration#dependencies dependencies : override : # output information about the docker environment - docker info # create a .env file to configure our application container - | cat < < VARS > . env SECRET _ KEY _ TEST= $ SECRET _ KEY _ TEST POSTGRES _ DB= $ POSTGRES _ DB POSTGRES _ USER= $ POSTGRES _ USER VARS # build our Docker images - docker-compose build # configure the database # https://circleci.com/docs/configuration#database database : pre : # boot all containers - docker-compose up -d # wait a second to make sure all containers are up and running before the next step - sleep 1 # create our test DB and load the schema. The DB is named $POSTGRES_DB, as defined in our environment. override : - docker-compose run app rake db :create db :schema :load # configure the test run # https://circleci.com/docs/configuration#test test : # run the test suite in the application container override : - docker-compose run app rspec spec

Most of the file is straight-forward to understand given the comments. For a standard, dockerized Rails app there are really only two sections that you might want to tweak.

Generating a .env file

circle.yml dependencies: override: - | cat << VARS > .env SECRET_KEY_TEST=$SECRET_KEY_TEST POSTGRES_DB=$POSTGRES_DB POSTGRES_USER=$POSTGRES_USER VARS 1 2 3 4 5 6 7 8 dependencies : override : - | cat < < VARS > . env SECRET _ KEY _ TEST= $ SECRET _ KEY _ TEST POSTGRES _ DB= $ POSTGRES _ DB POSTGRES _ USER= $ POSTGRES _ USER VARS

Here I am creating a file named .env that lists the environment variables I use to configure my Rails application container. The env_file: .env directive in my docker-compose.yml relies on this file. The values for each variable are read from the CircleCI environment, which I have defined in the project settings. You may want to replace these variables, or define new ones. If you do not use environment variables to configure your application then you can disregard this section all together.

Configuring the test run

circle.yml test: override: - docker-compose run app rspec spec 1 2 3 test : override : - docker-compose run app rspec spec

I run my tests with RSpec. You may use Minitest. You may also have Capybara, Cucumber, or Jasmine tests. You can duplicate and/or modify the above line to meet any of those needs. Simply prefix whatever command you use to run your suite with docker-compose run app, where “app” is the name of your Docker Compose-defined application container. This will ensure the suite is run inside your Docker container.

What it looks like

The companion application for this article is hooked up to CircleCI. Check out the results of the last test run on the build page. The page is available publicly because the project is open source, and CircleCI has public features for OSS projects.

Conclusion

Once you have dockerized your Rails application it is easy to configure CircleCI to build your images on their servers, spin up the containers, run your test suite in the containers, and notify you of the results. Doing so will give your team a hosted continuous integration environment that is nearly identical to the environment used during development. As a result all failing tests in CI should be immediately reproducible in development where they can be fixed for the next build. Ultimately, containerization of your application should eliminate the hard-to-kill bugs that often result from differences between native CI and development environments.

Stay tuned to this blog, or follow me on Twitter, to be aware of follow-up articles. In a forthcoming post I’ll change the CircleCI setup outlined here to do continuous deployment. In another I’ll discuss my experience with hosted CI in general and why I work with CircleCI.

Got questions or feedback? I want it. Drop your thoughts in the comments below or hit me @ccstump.

Thanks for reading!

Addendum

3/17/16 I’ve published the last article of the series. Give it a read and learn how to expand your new CI setup with continuous deploy.