Nowadays, Docker integration is a part of almost every software application lifecycle. It invaded the world of modern development, along with Microservices architecture patterns, a few years ago and enables resolution of deployment and scalability problems in large, sophisticated applications. According to the latest reports containerisation is rapidly gaining popularity in companies of all sizes and types.

In this article I am going to show how a Rails 5 application can be set up with Docker containers running on a Passenger server. We will start with the creation of a blank Rails app but the following steps are applicable for existing Rails apps as well.

$ rails new simple-app

Once the generation and gem installation processes have finished, run the app:

$ cd simple-app
$ rails s

and then confirm that everything works by accessing http://localhost:3000 in your web browser. If you see the following image then everything is OK so far:

Docker

So what is Docker?

If you do not have Docker-engine installed yet, you can follow the official Docker installation guide.

Docker containers are run from imagesBasically an image is an isolated operating system with a pre-installed set of libraries/frameworks defined in a Dockerfile or inherited from another image. Images are built with a special command: docker build.

Thus our goal is to build our own image which can then be run as a staging or production server. As you may have already guessed, we’re going to start by creating a Dockerfile just inside the app directory with the following contents:

FROM phusion/passenger-full:latest

EXPOSE 80

ENV APP_HOME /app

RUN mkdir $APP_HOME
WORKDIR $APP_HOME
RUN bash -lc 'rvm --default use ruby-2.3'

# nginx confs
RUN rm -f /etc/service/nginx/down

# Remove the default site
RUN rm /etc/nginx/sites-enabled/default
ADD webapp.conf /etc/nginx/sites-enabled/webapp.conf
ADD rails-env.conf /etc/nginx/main.d/rails-env.conf

ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock

RUN bundle install

ADD . /app
RUN chown -R app:app /app

A Dockerfile is a set of instructions on how to build an image. Here is an explanation of each line:

FROM phusion/passenger-full:latest

The FROM command takes the image from the Docker Hub on which the new image will be based, with a tag number after the colon. In our case we will take the passenger-full image with the latest tag. We will use this feature to manage deployed versions of our app.

EXPOSE 80

Opens port 80 of the container which will be launched from the new image.

ENV APP_HOME /app

Defines $APP_HOME variable to ‘/app’. We choose this path because the phusion/passenger image has an app user with UID 9999 and home directory /home/app. Therefore our application should be placed inside /home/app.

RUN mkdir $APP_HOME
WORKDIR $APP_HOME

Creates and sets /app directory as the working directory. This means that from this directory all RUN, ADD, etc commands will be executed from this directory.

RUN bash -lc 'rvm --default use ruby-2.3'

Sets ruby version to 2.3 (this is available because the passenger-full image has several pre-installed versions of ruby with RVM).

RUN rm -f /etc/service/nginx/down

Enables Nginx and Passenger.

RUN rm /etc/nginx/sites-enabled/default

This is needed to remove the Nginx site as default (“Welcome to Nginx” page)

ADD webapp.conf /etc/nginx/sites-enabled/webapp.conf
ADD rails-env.conf /etc/nginx/main.d/rails-env.conf

These two files in our configuration should contain passenger settings and rails environment variables which we will be able to use while starting a container. The general idea is to have an environment independent container which can be run with any settings. Here is how the settings may look:

/webapp.conf

server{
    listen 80;
    server_name _;
    root /app/public;

    passenger_enabled on;
    passenger_user app;

    passenger_min_instances 2;

    passenger_max_request_queue_size 500;

}
passenger_max_pool_size 4;

/rails-env.conf

env RAILS_ENV;
env SECRET_KEY_BASE;

ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install

Obviously we will need to run bundle install for each deployment, but there is a trick- simply add Gemfile and Gemfile.lock separately. This isn’t necessary but it allows you to make Docker “watch” these files and cache inside intermediate images while running the first build. All subsequent builds will therefore be much faster unless new gems are added.

ADD . /app
RUN chown -R app:app /app

Lastly, copy the application directory into the /app directory within the image and give the app user ownership of the directory.

And we’re done! Now let’s build an image and run our first container:

$ docker build -t simple-app

Once the image is ready it is possible to confirm it by running:

$ docker images

And finally, we just need to run the container:

$ docker run -d -m 2G \ 
     -e PASSENGER_APP_ENV=development \
     -e RAILS_ENV=development \
     -e SECRET_KEY_BASE='0097ee3b2496e3b6cdfddbc0f96aa78b4d8ea153047ef1b00eb31e51fc15a5e3ad0fdec380fb880ffbfdaff95bbbc12c267cd34dae1f760bc8e2c0e7b478b5c2' \ 
     --restart="always" \
     --name simple-app \
     -p 3000:80 simple-app

Here the -d option stands for daemon, -m – is the amount of memory which will be dedicated to the container. and the -e options are the  environment variables which we declared in rails-env.conf. PASSENGER_APP_ENV is a variable defined by default in passenger image.

To confirm that our container is up and running execute

$ docker ps

This will list all running containers and we should be able to see simple-app. Now we can access the Passenger server of our app in the same way http://localhost:3000As we published the container’s port 80 to the host’s port 3000 with the -p option, you should see the same page as you do while running the app locally.

And that’s it :). The process above looks pretty much the same for any staging or production environment setup. Once a container is built it can be run anywhere.

Good to know

Here are a few tips and links which may be useful:

1. If you want to run a container from another image, firstly stop and remove the current one like this:

$ docker rm simple-app
$ docker stop simple-app

2. If you have any problems with running a container, you can check its logs with:

$ docker logs simple-app

3. It is also possible to access the terminal of a container by:

docker exec -it simple-app /bin/bash

And then inside check Nginx logs in /var/log/nginx/error.log or access.log

4. If you are going to run a container inside a private network with access to other services, it is possible by giving it access to servers hosts file by adding a volume:

-v /etc/resolv.conf:/etc/resolv.conf

5. It is possible to tag each container which can be used as a version in each deployment:

docker build -t simple-app:1
docker run (...) simple-app:1

Additional information about the phusion passenger image can be found on the official github page. For example the passenger-full image has already included many ruby versions along with redis, node, etc.

Conclusion

So, as we can see, Docker-ising a Rails app generally isn’t such a long and complicated process. What are main benefits of using Docker though?

  1. It solves scalability problems, running another container is a matter of one command.
  2. It keeps deployments easy. The deployment process is turned into just building a new image.
  3. It is fast to revert if needed. Simply run the image with a previous tag/version.
  4. It fits perfectly into the micro services approach.
  5. It is environment independent and can be used for the fast setup of any application locally for development purposes.

In this article, we haven’t covered many cases with e.g. connecting an app to external services or running a service inside a container, but I hope that the information described above can become a solid background in helping you to understand how Rails can be integrated with Docker to become a part of any micro services structure.


Maksym Karganov

Full Stack Ruby on Rails developer at Travactory, Scrum Master
Other interests: astronomy, tenis, electric cars, android

2 thoughts on “How Docker Can Simplify Your Life

  • linuxman1

    Thank you, the tutorial is really useful!

    Reply
  • Serguei

    You have to put a period in the end of the command “docker build -t simple-app” like that: “docker build -t simple-app .”

    Reply

Leave a comment to linuxman1 Cancel reply

Your email address will not be published. Required fields are marked *