Web Dev

Running a Rails Development Environment in Docker

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.

Step 2: Dockerizing a Rails Application

Now that we have Docker installed and running, it’s 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 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.

“Setting up a Rails dev env in @Docker in 5 steps” via @codeship

Click To Tweet

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 run -it 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 
  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

Step 5.5: Extending Your Development Environment

With the steps shown above, it’s easy to extend the 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

Reference: Running a Rails Development Environment in Docker from our WCG partner Florian Motlik at the Codeship Blog blog.

Marko Locher

Support Deckhand at Codeship. Answering your questions, updating our documentation and generally making sure everybody is happy! In my spare time I like volleyball, sailing and skiing.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button