Setting up CircleCI 2.0 for Rails

Nick Charlton

In July of 2017, CircleCI released their new platform, 2.0. It’s much more powerful and flexible, but it’s also more complex to get up and running with for Rails apps. Let’s walk through it…

A Base Configuration

The CircleCI configuration file is now in .circleci/config.yml. Initially, it should start looking something like this:

---
version: 2
jobs:
    build:
        working_directory: ~/your-app-name
    steps:
        - checkout

This doesn’t do all that much (and wouldn’t work on its own). But it’s the basic shell of the config file we’ll need. These parts will be there throughout.

Choosing a base Docker Image

For our use case, the best choice is the Docker executor, which will compose the environment from a set of Docker images. CircleCI provide set of pre-built images that we’ll use. These cover lots of different Ruby versions, with variations for common requirements like a browser and/or JavaScript.

These variations can be specified with tags on the Docker image, like so:

---
version: 2
jobs:
    build:
        working_directory: ~/your-app-name
        docker:
            - image: circleci/ruby:2.4.1
            environment:
                RAILS_ENV: test
    steps:
        - checkout

      # Setup the environment
      - run: cp .sample.env .env

      # Run the tests
      - run: bundle exec rake

This adds the Ruby image for 2.4.1. If we need a browser, we might use the 2.4.1-browsers tag (for Selenium, for example). If we have JavaScript tests, we might use 2.4.1-node and if we have both we might want 2.4.1-node-browsers.

Now we have an environment we can run in, we’ll also go about including what we need to run the tests. Yours might vary here.

Services

A Rails app is rarely without a database and we can introduce that with another Docker image. We’ll use PostgreSQL here, but others would work in the same manner:

---
version: 2
jobs:
  build:
    working_directory: ~/your-app-name
    docker:
      - image: circleci/ruby:2.4.1
        environment:
          PGHOST: localhost
          PGUSER: your-app-name
          RAILS_ENV: test
      - image: postgres:9.5
        environment:
          POSTGRES_USER: your-app-name
          POSTGRES_DB: your-app-name_test
          POSTGRES_PASSWORD: ""
    steps:
        - checkout

      # Setup the environment
      - run: cp .sample.env .env

      # Setup the database
      - run: bundle exec rake db:setup

      # Run the tests
      - run: bundle exec rake

This adds a postgres image, but also adds some environment configuration. We provide similar values to what we’re using in config/database.yml. Specifying POSTGRES_DB on the postgres image ensures that the correct database is created.

Additionally here, we add the rake commands for configuring our database.

Our configuration file should now be successfully running tests. It was at this point that I found I needed to iterate on the base Docker image to make sure I was covering all of my dependencies and most runs were passing successfully.

Avoiding race conditions

One problem we should take care of though is race conditions to do with how the additional services come up. It’s possible that Postgres might take slightly longer to be available and so our tests might start running …and then all fail.

To solve this, we can use dockerize to wait for the appropriate port to be available. Add this as a run step:

- run: dockerize -wait tcp://localhost:5432 -timeout 1m

Caching dependencies

Running bundle for each test run can be slow and is easy to cache. To give us a little speed improvement, we can do something like this:

# Restore Cached Dependencies
- type: cache-restore
  name: Restore bundle cache
  key: your-app-name-{{ checksum "Gemfile.lock" }}

# Bundle install dependencies
- run: bundle install --path vendor/bundle

# Cache Dependencies
- type: cache-save
  name: Store bundle cache
  key: your-app-name-{{ checksum "Gemfile.lock" }}
  paths:
    - vendor/bundle

This will try and restore any cached dependencies using a checksum based on your lock file. So if you add more things and it’s invalid it won’t try and use something which isn’t useful. Installing dependencies to a specific path allows us to use cache-save to save that particular directory.

Putting it all together

All of these come together to look like this:

---
version: 2
jobs:
  build:
    working_directory: ~/your-app-name
    docker:
      - image: circleci/ruby:2.4.1
        environment:
          PGHOST: localhost
          PGUSER: your-app-name
          RAILS_ENV: test
      - image: postgres:9.5
        environment:
          POSTGRES_USER: your-app-name
          POSTGRES_DB: your-app-name_test
          POSTGRES_PASSWORD: ""
    steps:
      - checkout

      # Restore Cached Dependencies
      - type: cache-restore
        name: Restore bundle cache
        key: your-app-name-{{ checksum "Gemfile.lock" }}

      # Bundle install dependencies
      - run: bundle install --path vendor/bundle

      # Cache Dependencies
      - type: cache-save
        name: Store bundle cache
        key: your-app-name-{{ checksum "Gemfile.lock" }}
        paths:
          - vendor/bundle

      # Wait for DB
      - run: dockerize -wait tcp://localhost:5432 -timeout 1m

      # Setup the environment
      - run: cp .sample.env .env

      # Setup the database
      - run: bundle exec rake db:setup

      # Run the tests
      - run: bundle exec rake

You can see this configuration in action on administrate.