Building Docker Containers for our Rails Apps

BY Matthew Hazlehurst

May 24, 2019

undefined

In a recent post, we talked about the 8 things that you know about Docker containers, and what you should know about them. Hopefully we cleared up any confusion you might have had about the Docker ecosystem. Perhaps with all that talk, it got you thinking about trying it out on one of your own applications? Well in this post we’d like to show you how easy it is to take your existing Ruby on Rails applications and run them inside a container. So, let’s assume you have an existing Rails project with a PostgreSQL database, and let’s walk you through the steps it would take to run this in a container instead. It’s a lot easier than you probably think!

Creating the Dockerfile

The first thing that we need to do to get our application to run in a container is to define our custom image that we will run as a container, and we can do this in a Dockerfile. This Dockerfile is essentially a set of instructions for Docker to use when it builds our container image, remember this was one of the 8 things about Docker we talked about. The idea is that this file is all that is required to produce an identical container on any system, and so we can add this file to source control so that everybody in our team can utilize it. Let’s create the following file called “Dockerfile” and place it inside our project’s root directory:

# Start from the official ruby image, then update and install JS & DB
FROM ruby:2.6.2
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client

# Create a directory for the application and use it
RUN mkdir /myapp
WORKDIR /myapp

# Gemfile and lock file need to be present, they'll be overwritten immediately
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock

# Install gem dependencies
RUN bundle install
COPY . /myapp

# This script runs every time the container is created, necessary for rails
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start rails
CMD ["rails", "server", "-b", "0.0.0.0"]

Let’s take a step-by-step look at this file to understand exactly what we are asking Docker to do. With the ‘FROM’ statement, we start from an official ruby Docker image (hosted on Docker Hub), and copy this into a brand new image. Inside this new image, we call ‘RUN’ which updates and installs a JavaScript runtime and a PostgreSQL DB client inside our new image. With the next ‘RUN’ command, we create a directory inside our image called myapp (note that you should change references of “myapp” to your application’s directory name) and then we set this as the working directory of our image. The next step is to use ‘COPY’ to get our Gemfile and Gemfile.lock files from our project into this image. We need these files to be present so that we can do ‘RUN bundle install’ and install all the gems into this image. We then use ‘COPY’ to copy our entire project into the image. The next four lines are specific to Rails projects to allow them to run correctly in containers, don’t worry too much if you don’t understand this part. But we will need to add the file shown below to our projects directory and call it entrypoint.sh for this to work. The final line of the Dockerfile, ‘CMD’, will kick off the Rails server when the image is ran in a container.

This is the entrypoint.sh file that we need to create and add to our project:

#!/bin/bash

# Rails-specific issue, deletes a pre-existing server, if it exists

set -e

rm -f /myapp/tmp/pids/server.pid

exec "$@"

So using this Dockerfile and the entrypoint.sh script, we can build our image with a single command. But you might have noticed that we haven’t yet specified our PostgreSQL database details yet. Database engines usually run in their own containers, separate from your web application. The good news is that we don’t have to define a custom image with a Dockerfile for this database container, we can just use a standard PostgreSQL image from Docker Hub and use it as it is. But it does still mean that we will have two separate containers that need to communicate with each other. How do we do that? That’s where Docker Compose comes in.

Creating the docker-compose.yml file

As you will no doubt remember from our last blog post, Docker Compose is a tool that allows us to connect multiple containers together into a multi-container environment that we can think of as a service. For example, in our situation we have a database engine container and a rails environment container. So we could use Docker Compose to combine these into a multi-container environment which we can conceptually view as our complete application. To use Docker Compose, we need to create a docker-compose.yml file, in addition to the Dockerfile that we already have and place this in your project’s directory. This file will tie together the rails container we previously defined in the Dockerfile (let’s call that “web”), and another container for the database which we will call “db”:

version: '3'

services:
 db:
   image: postgres
   volumes:
     - ./tmp/db:/var/lib/postgresql/data
 web:
   build: .
   command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
   volumes:
     - .:/myapp
   ports:
     - "3000:3000"
   depends_on:
     - db

As you can see, for the db part, we just specify the ‘postgres’ image from Docker hub and then mount the location of our database into the container. For the web part however, we build the image defined in our Dockerfile, run some commands and mount our application code into the container. Note that if you are using a system that enforces SELinux (such as Red Hat, Fedora or CentOS), then you will need to append a special :z flag to the end of your volume paths.

Putting it all together

So now that we have a Dockerfile, docker-compose.yml and entrypoint.sh script in our project’s directory, there are just a few more steps we need to do before we build our image and run the application as a container.

First of all, it is a good idea to clear out the contents of our Gemfile.lock file before we proceed:

$ rm Gemfile.lock
$ touch Gemfile.lock

Next, we need to update the database settings, as the credentials for the PostgreSQL image differ from the credentials you would use on your local install. The main difference is that here we specify our ‘db’ container as the host. You can make these changes to the relevant part of your config/database.rb file:

default: &default                                                               
 adapter: postgresql                                                           
 encoding: unicode                                                             
 host: db                                                                      
 username: postgres                                                            
 password:

The next step is to build our custom image that we defined in the Dockerfile, we can do that like this:

$ docker-compose build

Now we have an image for our web application, we need to prepare our database (inside the container).

$ docker-compose run web rake db:create
$ docker-compose run web rake db:migrate

Running in a container

And that’s it! We’re done! All you need to do now to run you entire application (this time, and in future) is this one command:

$ docker-compose up

When you are finished, to shut down correctly and remove your containers, you run:

$ docker-compose down

rails-docker.gif

What’s next?

Install Scout of course! Whether you are deploying your application in Docker containers or not, sign up for a free trial here and start optimizing your site today. Because as much as containers are great, they won't fix your performance problems! In fact performance monitoring is just as important in a containerized environment as it is in a non-containerized environment. Contact us on support or reach out to us on Twitter and we’ll show you can monitor your Ruby, Elixir and Python apps. And to thank you for reading this far, click on Doggo below for a reward!

undefined

Docker containers are not only great but they are also very easy to set up and use, so there really is no excuse for not giving them a try. But maybe you are a Python or Elixir developer? Well the good news is that it is just as easy to containerize with these languages too! We’ll be talking about that in a future post.

Maybe you are already using Docker containers for your applications, and perhaps you have a much more complicated set of containers than the examples we covered here. If that is the case then you might be interested in an upcoming blog where we’ll be talking about container orchestration and how you can use it to autonomously manage your containerized applications. Stay tuned for more posts soon!