How I build a Docker image for my Rails app

You have probably heard of the new cool kid in town, Docker. The problem with the fact that it is a new kid is that the documentation about certain tasks is pretty limited. This includes the ways you can deploy your rails app using Docker. In this blog post I’ll give you some pointers that can help you in your further quest of deploying your rails apps with docker.

There are a few things that I won’t cover, simply because I’m not sure how they should be done yet. Those things are:

  • Databases
  • Migrations
  • Assets

Since that list contains a few super important items for rails apps, you will
wonder what I will cover, well I will help you with creating your own Dockerfile
that you can use to run your app. This Dockerfile is based on the work of the
Passenger Phusion guys.

I assume you have a working Docker environment, I’ll be using
Boot2docker in this post, since I’m working on osx, and there is no native docker support yet.

When I started looking at Docker, my first reaction was, wow! that’s easy, lets
do this! But when I started working on getting my rails app into the Docker
containers, I hit a wall. When going over the internet most people were using
just rails s in their containers, and that was just not going to work for me. I had a few demands:

  • It should use Passenger
  • It should use Nginx
  • It should be fast

So with those things in mind I started looking at some good base images, since
building them from the ground would not be an option for me. So I came across
the base images provided by the Phusion team. They had fixed a few basic things,
like SSH, a correct init proces, Cron daemon, Runit for service supervision and
much more (learn more).

The files

First thing I would need was a Dockerfile, this is the one I’ve come up with, in
the code you find comments in format === # ===, they map to the text below the code:

Dockerfile

# === 1 ===
FROM phusion/passenger-ruby21:0.9.12  
MAINTAINER Jeroen van Baarsen "jeroen@firmhouse.com"

# Set correct environment variables.
ENV HOME /root

# Use baseimage-docker's init system.
CMD ["/sbin/my_init"]

# === 2 ===
# Start Nginx / Passenger
RUN rm -f /etc/service/nginx/down

# === 3 ====
# Remove the default site
RUN rm /etc/nginx/sites-enabled/default

# Add the nginx info
ADD nginx.conf /etc/nginx/sites-enabled/webapp.conf

# === 4 ===
# Prepare folders
RUN mkdir /home/app/webapp

# === 5 ===
# Run Bundle in a cache efficient way
WORKDIR /tmp  
ADD Gemfile /tmp/  
ADD Gemfile.lock /tmp/  
RUN bundle install

# === 6 ===
# Add the rails app
ADD . /home/app/webapp

# Clean up APT when done.
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*  

And we needed the file to configure nginx

nginx.conf

server {  
listen 80;  
server_name example.com;  
root /home/app/webapp/public;

passenger_enabled on;  
passenger_user app;

passenger_ruby /usr/bin/ruby2.1;  
}

Lets take a closer look at all the single components:

1 The header

This is the header, every Dockerfile has one, the FROM is telling what base
image Docker should use, the MAINTAINER is for telling who created this particular Dockerfile

2 Start nginx

By default nginx is not running, you have to tell Docker to enable it. You do this via this command:

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

3 Configure nginx

Since we want to be able to get to this app via every domain thats pointing to the server, we remove the default nginx config, we also copy our nginx config file to the sites-enabled folder.

4 Making sure all the folders are created

We want to be sure that the correct folders exists. Our app only lives in
/home/app/webapp so for now thats the only folder we have to create.

5 Install gems in an efficient way

One of my goals was to make things fast, the way we can achieve this, is by making sure we can cache Docker layers. Of of the most time consuming things is bundle install, so we want to be sure to cache those layers. Thats why we add the
Gemfile and Gemfile.lock to the /tmp folder, and run bundle install from there. So when the Gemfile and Gemfile.lock did not change, the bundle install command will be skipped as well.

6 Adding the source code

Now we have all the part in place, we can add the source code of our app. This
is done by adding the complete folder to the docker image, and place it in
/home/app/webapp/

Building the image

Now we have all the code that is needed to build the actual image. To do this
run the command

$ docker build -t intercity/base .

You have to replace the intercity/base part with the name you want to give the image.

This will trigger the image build process, that looks like this:

...
Step 7 : ADD nginx.conf /etc/nginx/sites-enabled/webapp.conf  
 ---> 6764b3e5675d
Removing intermediate container ba9caaceb506  
Step 8 : RUN mkdir /home/app/webapp  
 ---> Running in d4e4ab62d133
 ---> ce38997f3513
Removing intermediate container d4e4ab62d133  
Step 9 : WORKDIR /tmp  
 ---> Running in d04e97c1d392
 ---> 1ed74bc61712
Removing intermediate container d04e97c1d392  
Step 10 : ADD Gemfile      /tmp/  
 ---> 7baa4f6a33d1
Removing intermediate container 0756eabe80b5  
Step 11 : ADD Gemfile.lock /tmp/  
 ---> 28a5e47d7831
Removing intermediate container 54aeec7598df  
Step 12 : RUN bundle install  
 ---> Running in a64a896edc98
...

After the command is done, you can check if the image is present by running

$ docker images

You should now see a list of all the images that are available on your system:

$ docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED             VIRTUAL SIZE  
intercity/base             latest              ed83cd251ff2        44 minutes ago      994.4 MB  
phusion/passenger-ruby21   0.9.12              45891671c71f        3 weeks ago         960.8 MB  

Starting your container

Now we have build our container, we want to be able to access it via the web! To
do this run the following command:

$ docker run -d -p 80:80 intercity/base

Make sure you replace the intercity/base part with your image name. This
command will return a hash, that is the ID of your container. When you want to interact with the container, you need that hash.

To see if the container is running, open your browser and go to the IP of the
host. If you’re running Boot2docker you can get the
IP via the following command:

$ boot2docker ip

Stopping your container

When you want to stop the container, you first need to know the container ID,
you can retrieve it by running

$ docker ps

This will return all the running containers. Copy the container ID you want to stop, and run

$ docker stop container_id

Conclusion

As I said in the intro, im pretty much a beginnen with Docker, and this are just
some things that I struggled with, and I hope this will save you some time. When
I figured out the parts about assets and databases, I’ll write another post
about it! You can find all the files, including an example app ready for
deployment on
github.com/intercity/rails-docker-example If you have any feedback on this, please contact me on
jeroen@firmhouse.com or on twitter @jvanbaarsen