Convert an Existing Rails Application to Work with Docker

Tue Oct 4, 2016 - 900 Words

Today we’re going to take an existing Ruby on Rails application and convert it so that it be developed/deployed using Docker.

Goal for this Tutorial:

  • Dockerizing an existing rails application.

Up to this point we’ve been working with “hello world” applications when working with Docker, but today we’re going to take an existing application (that I didn’t write) and get it to work with Docker.

Dockerizing a Rails Application

The application that we’re going to convert today is an open source Rails application for time tracking called “Hours”. This particular application requires a few different services:

Since this project isn’t currently set up to be used with Docker, the first thing we’ll do is clone the repo and create a docker-compose.yml file to lay out all of our services.

Cloning:

$ git clone git@github.com:DefactoSoftware/Hours.git

Now let’s crack open our docker-compose.yml:

docker-compose.yml

version: "2"

volumes:
  db-data:
    external: false

services:
  cache:
    image: memcached:1.4-alpine

  db:
	  environment:
		  POSTGRES_USER:
		  POSTGRES_PASSWORD:
    image: postgres:9.5
    volumes:
      - db-data:/usr/local/pgsql/data

	jobs:
	  env_file: .env
    build: .
    volumes:
	    - .:/usr/src/app
		command: bundle exec rake jobs:work
    depends_on:
      - db

  app:
	  env_file: .env
    build: .
    volumes:
	    - .:/usr/src/app
    ports:
      - "8080:8080"
    depends_on:
      - db
      - cache

Now we need to set up our Dockerfile so that we can build an image to run our application on. This application uses Capybara-webkit for testing so we’ll need to install some extra dependencies for that to work:

FROM ruby:2.3.1

RUN apt-get update -yqq \
  && apt-get install -yqq --no-install-recommends \
    postgresql-client \
    nodejs \
    qt5-default \
    libqt5webkit5-dev \
  && apt-get -q clean \
  && rm -rf /var/lib/apt/lists

WORKDIR /usr/src/app
COPY Gemfile* ./
RUN bundle install
COPY . .

CMD bundle exec unicorn -c ./config/unicorn.rb

We’ll need to create config/database.yml and config/secrets.yml before our application will work. Hours is build around deploying to Heroku, and Heroku manages some of this configuration for you.

Database:

config/database.yml

# config/database.yml
default: &default
  adapter: postgresql
  host: <%= ENV["POSTGRES_HOST"] %>
  username: <%= ENV["POSTGRES_USER"] %>
  password: <%= ENV["POSTGRES_PASSWORD"] %>
  encoding: utf8
  min_messages: warning
  pool: 2
  timeout: 5000

development:
  <<: *default
  database: hours_development

test:
  <<: *default
  database: hours_test

production:
  <<: *default
  database: hours_production

config/secrets.yml

development:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

development:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

You’ll notice that we’ve used quite a few environment variables thus far. Environment variables are potentially the easiest way to configure our application, but they can be tedious. Thankfully, docker-compose allows us to use a .env file to store the values that we would like to use in our containers and we don’t need to set them in our own environment.

Let’s create our own .env file now by copying the .sample.env file and making some adjustments.

POSTGRES_HOST=db
POSTGRES_USER=hours
POSTGRES_PASSWORD=super_secure

SECRET_KEY_BASE=development_secret
GMAIL_USERNAME="example@mail.com"
GMAIL_PASSWORD="password"
HELPFUL_URL=https://example.io/incoming_message
HELPFUL_ACCOUNT=helpful_account
SINGLE_TENANT_MODE=true
S3_BUCKET_NAME="s3_bucket_name"
AWS_ACCESS_KEY_ID="aws_access_key_id"
AWS_SECRET_ACCESS_KEY="aws_secret_access_key"

Now we’re ready to take our group of containers for a spin.

Spinning up our Containers

Like we had to when learning to develop a Rails application using Docker, we’re going to need to do a few things before our application will actually run. First, we’re going to need to create our database. We can do that by starting our services that are going to run from images, those shouldn’t have any issues.

$ docker-compose up -d db cache

This will create the network that all of our containers connect to to communicate with one another and also start our memcached and postgres containers.

Next up, we’ll need to build our app image before we can run commands. This is going to need to install all of the gems and dependencies so it will likely take awhile. A nearly identical image will be used for our jobs container so we’ll build that now also.

$ docker-compose build app jobs

After we’ve build our image we can run some commands with it. First, we need to create and migrate our database:

$ docker-compose run --rm app rake db:create db:migrate

Second, we want to create a user for ourselves within the application.

$ docker-compose run --rm app rake create_user
First name: Coder
Last name: Journey
email: keith@coderjourney.com
Password:
The user is created

Now we’re good to spin up our app and jobs containers.

$ docker-compose up -d app jobs

Before moving any further we should make sure that all of our containers are running using docker-compose ps:

$ docker-compose ps
    Name                   Command               State           Ports
-------------------------------------------------------------------------------
hours_app_1     /bin/sh -c bundle exec uni ...   Up      0.0.0.0:8080->8080/tcp
hours_cache_1   docker-entrypoint.sh memcached   Up      11211/tcp
hours_db_1      /docker-entrypoint.sh postgres   Up      5432/tcp
hours_jobs_1    bundle exec rake jobs:work       Up

Now we can head over to localhost:8080 and check out our new time tracking application.

Fixing the Test Suite

Right now, if we try to run the test suite we’ll get some errors because it can’t find certain pieces of the page to click on. This is caused by this application already using a .env file to load environment variables into the application and us having explicitly defined environment variables in our containers. We’re going to get around this by overloading the environment in the test so that it’s always the same.

spec/spec_helper.rb

# Environment set to "test" above

require "dotenv"
Dotenv.overload(".sample.env")

# environment file loaded below

It’s important that we put these two lines in the right spot so that we set the environment variables before loading the rails app for our tests, just in case those ENV values are stored in constants somewhere.

Now we can see if it worked by running:

$ rake

The tests should all be green! Congrats on converting your first application to using Docker.

Recap

All of the code for this tutorial can be found on Github.

I’m looking for feedback on upcoming topics. Head over and answer my single question survey go let me know what you think I should cover.