Running a Rails Development Environment in Docker

Written by: Marko Locher

8 min read

As we prepare our new Docker-based infrastructure for running your tests, we’d like to show you how you can use the same environment for development as well. Feel free to follow along as I demonstrate how to move a simple Rails applications into Docker during development.

Suppose we have a (very) simple new Rails application:

gem install rails bundler
rails new demo
cd demo
bundle install

Without any further configuration, this provides us with a Rails application, using SQLite as a database. And if we want to, we can run our tests and start the development server to take a look in our browser.

bundle exec rake test
bundle exec rails server

We can now access the default start page at localhost:3000. It's not a very useful app, but it'll do for our purpose.

Now, let's move this app to use in a Docker-based environment.

Step 1: Installing Docker

If you already have Docker up and running, you can skip this step and move on to Step 2, Dockerizing right away. If not, let's get Docker running on your machine.

At Codeship, we recommend using Docker Machine. It's a young project and still in beta, but we've had great success using it internally.

See their installation instructions for how to get it running on your computer. Those instructions will include the necessary commands to get Docker itself running as well. Once you have Docker Machine installed, create a new environment (e.g., based on Virtualbox) and configure your local host to use that environment for Docker.

https://js.hscta.net/cta/current.js

hbspt.cta.load(1169977, 'c7747d5f-9e9d-4329-8362-71fbdb824ee9', {});

Step 2: Dockerizing a Rails Application

Now that we have Docker installed and running, it is time to get our application running on it. Docker applications are configured via a Dockerfile, which defines how the container is built.

The easiest Dockerfile includes a single line, the base image to use. The following one would, for example, provide an Ubuntu Trusty based system:

FROM ubuntu:14.04

Many images are readily available, and you can search for a suitable base image at the Docker Hub. We'll use the ruby:2.2 base image.

FROM ruby:2.2
MAINTAINER marko@codeship.com
# Install apt based dependencies required to run Rails as
# well as RubyGems. As the Ruby image itself is based on a
# Debian image, we use apt-get to install those.
RUN apt-get update && apt-get install -y \
  build-essential \
  nodejs
# Configure the main working directory. This is the base
# directory used in any further RUN, COPY, and ENTRYPOINT
# commands.
RUN mkdir -p /app
WORKDIR /app
# Copy the Gemfile as well as the Gemfile.lock and install
# the RubyGems. This is a separate step so the dependencies
# will be cached unless changes to one of those two files
# are made.
COPY Gemfile Gemfile.lock ./
RUN gem install bundler && bundle install --jobs 20 --retry 5
# Copy the main application.
COPY . ./
# Expose port 3000 to the Docker host, so we can access it
# from the outside.
EXPOSE 3000
# The main command to run when the container starts. Also
# tell the Rails dev server to bind to all interfaces by
# default.
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

After adding the above file as Dockerfile to your repository, we can now build the container and start running commands with it. We specify a tag via the -t option, so we can reference the container later on.

docker build -t demo .
docker run -it --rm demo bundle exec rake test
docker run -itP demo

Here are some explanations for the commands above:

  • docker run runs tasks in a Docker container. This is most commonly used for one-off tasks but is also very helpful in development.

  • The -P option causes all ports defined in the Dockerfile to be exposed to unprivileged ports on the host and thus be accessible from the outside.

  • If we don't specify a command to run on the command line, the command defined by the CMD setting will be run instead.

We now have our Rails application running inside a Docker container, but how do we actually access it from our computer? We will use docker ps, a handy tool to list running Docker processes as well as additional information about them.

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                     NAMES
eb018d2ca6e2        demo           "bundle exec 'rails    10 seconds ago      Up 9 seconds        0.0.0.0:32769->3000/tcp   pensive_ritchie

We can see the container ID, the image it is based on, which command it is running, and the mapping of any exposed ports. With this information at hand, we can now open the app in our browser http://localhost:32769.

Note: If you don't have Docker running on your local machine, you need to replace localhost in the above URL with the IP address of the machine Docker is running on. If you're using Docker Machine, you can run docker-machine ip "${DOCKER_MACHINE_NAME}" to find out the IP.

The application is up and running and accessible from our development machine. But, each time we make a change, we need to build a new container. That's not very helpful. Let's improve our setup.

Step 3: Docker Volumes

Docker supports what it calls volumes. These are mount points which let you access data from either the native host or another container. In our case, we can mount our application folder into the container and don't need to build a new image for each change.

Simply specify the local folder as well as where to mount it in the Docker container when calling docker run, and you're good to go!

docker run -itP -v $(pwd):/app demo

Step 4: Improvements

Dockerfile Best Practices lists some ways to improve performance and create easy to use Dockerfiles. One of those tips is using a .dockerignore file.

.dockerignore

Similar to a .gitignore file, .dockerignore lets us specify which files are excluded and not transferred to the container during the build. This is a great way to speed up the build times, by excluding files not needed in the container (e.g., the .git subdirectory). Let's add the following .dockerignore file to our project

.git*
db/*.sqlite3
db/*.sqlite3-journal
log/*
tmp/*
Dockerfile
README.rdoc

Entrypoint

Because most of the commands we run on the Rails container will be prepended by bundle exec, we can define an [ENTRYPOINT] for all our commands. Simply change the Dockerfile like this:

# Configure an entry point, so we don't need to specify
# "bundle exec" for each of our commands.
ENTRYPOINT ["bundle", "exec"]
# The main command to run when the container starts. Also
# tell the Rails dev server to bind to all interfaces by
# default.
CMD ["rails", "server", "-b", "0.0.0.0"]

You can now run commands without specifying bundle exec on the console. If you need to, you can override the entrypoint as well.

docker run -it demo "rake test"
docker run -it --entrypoint="" demo "ls -la"

Locales

If you're not happy with the default locale in your Docker container, you can switch to another one quite easily. Install the required package, regenerate the locales, and configure the environment variables.

...
# Install apt based dependencies required to run Rails as
# well as RubyGems. As the Ruby image itself is based on a
# Debian image, we use apt-get to install those.
RUN apt-get update && apt-get install -y \
  build-essential \
  locales \
  nodejs
# Use en_US.UTF-8 as our locale
RUN locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
...

Step 5: Moving your Development Environment to PostgreSQL

While SQLite might be fine for a simple app, you wouldn't use it in production. So let's move our development environment over to PostgreSQL instead.

We could add the database to our container, but there's a better way to do this. Use Docker Compose to provision the database in a separate container and link those two together.

To get Docker Compose installed, please follow the installation instructions on their website.

Basic Compose Configuration

Once this is done, let's duplicate our configuration to work with Compose. Add a docker-compose.yml file to your repository and include the following configuration:

app:
  build: .
  command: rails server -p 3000 -b '0.0.0.0'
  volumes:
    - .:/app
  ports:
    - "3000:3000"

With the configuration above, running your development environment is as simple as running two commands:

docker-compose build
docker-compose up

Even for a single container environment this has some (smaller) improvements over using docker directly. We can specify the VOLUME definition directly in the configuration file; we don't need to specify it on the command line. We can also define the port on the Docker host our application will be available at and don't need to look it up.

Adding PostgreSQL

We could now create a new Dockerfile for running PostgreSQL, but luckily we don't need to. There is a readily available PostgreSQL Docker image available on the Docker Hub, so let's just use that instead.

app:
  build: .
  command: rails server -p 3000 -b '0.0.0.0'
  volumes:
    - .:/app
  ports:
    - "3000:3000"
  links:
    - postgres
postgres:
  image: postgres:9.4
  ports:
    - "5432"

We defined a new container called postgres, based on the PostgreSQL 9.4 image (there are images for previous versions available as well), configured the port on the new image, and told our app container to define a link to the database.

But how do we access the database from within our Rails application? Fortunately for us, Docker Compose exposes environment variables for linked containers, so let's take a look at those.

# build new container images first
docker-compose build
docker-compose --rm app env

This will print a bunch of environment variables, including these two:

...
POSTGRES_PORT_5432_TCP_ADDR=172.17.0.35
POSTGRES_PORT_5432_TCP_PORT=5432
...

We can now use those in our database.yml to access the database server.

default: &default
  adapter: postgresql
  encoding: unicode
  pool: 5
  timeout: 5000
  username: postgres
  # please see the update below about using hostnames to
  # access linked services via docker-compose
  host: <%= ENV['POSTGRES_PORT_5432_TCP_ADDR'] %>
  port: <%= ENV['POSTGRES_PORT_5432_TCP_PORT'] %>
development:
  <<: *default
  database: app_development
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run
# "rake". Do not set this db to the same as development or
# production.
test:
  <<: *default
  database: app_test

We also need to change our Gemfile and remove the sqlite3 gem and add pg instead.

# Use PostgreSQL as the database for Active Record.
gem 'pg'

Having made those changes, let’s rebuild our containers and configure the database.

docker-compose build
docker-compose up
docker-compose run app rake db:create
docker-compose run app rake db:migrate

Update, Compose now recommends to use the hostnames instead of environment variables to access linked services. The database.yml mentioned above should now look like the following snippet, further changes are not required.

default: &amp;default
  adapter: postgresql
  encoding: unicode
  pool: 5
  timeout: 5000
  username: postgres
  host: postgres
  port: 5432
...

Step 5.5: Extending Your Rails Development Environment

With the steps shown above, it's easy to extend the rails development environment. Need a Redis server? Look for a suitable image on the Docker Hub, extend the docker-compose.yaml configuration file with a few lines, and restart your environment. The same goes for memcached or other services you require.

Want to test against a different Ruby version? Modify the Dockerfile, spin up the environment and run your tests.

Conclusion

Switching your development environment to Docker does take some amount of work, but the benefits are well worth it. You get an environment that's easy to share with fellow team members, you can model it to closely resemble your production environment, and extend it in a simple way. With Codeship's upcoming Docker infrastructure, you'll be able to use the same environment to run your tests on Codeship and then push the built and tested containers to your production servers and deploy them. Easy, fast, efficient, and with less room for errors.

The best part is that it’s easy to clean up once you’re done with a project. You don’t pollute your computer with all those different libraries you only need for that single project!

Extra Resources

PS: If you liked this article you might also be interested in one of our free eBooks from our Codeship Resources Library. Download it here: Automate your Development Workflow with Docker

Posts you may also find interesting:

Architecting Ruby on Rails Apps as Microservices

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.