How I deploy a Rails app using Docker

This blog post shows how I deploy a Ruby on Rails app
that runs in a Docker container on a
DigitalOcean server. To keep it simple, I’m going to explain a very manual process on simply getting an app deployed inside a Docker
container.

In this post I’ll show you:

  • How I installed Docker on a server
  • The Dockerfile for my Rails app
  • Building with bundled gems from Gemfile
  • Building with compiled assets
  • Running my app with Docker
  • Docker environment variables for database.yml

Let’s get started with the installation on a server.

Installing Docker on my server

First, I boot a new Ubuntu 14.04 server on DigitalOcean and install Docker:

workstation $ ssh root@178.62.232.206  
server $ apt-get install docker.io  
server $ docker -v  
Docker version 1.0.1, build 990021a  

The Dockerfile and nginx.conf

Now I’m going to build a Docker image from my Rails app. It happens that Jeroen
blogged about this last week:
How I build a Docker image for my Rails app. I am using his blog post as a base for the next steps.

I’ll be building the Docker image on the same server that I want to host my app. I build it on that server because I want my application to be private, so using the public Docker registry is not an option. I could set up my private Docker registry but then I would have to maintain it, and I don’t want to do that at the moment. In this post, I’m looking at the simplest way to use Docker to host my app.

In my Rails project intercity-website, I added the following Dockerfile and Nginx config:

Dockerfile

FROM phusion/passenger-ruby21  
MAINTAINER Firmhouse "hello@firmhouse.com"

ENV HOME /root  
ENV RAILS_ENV production

CMD ["/sbin/my_init"]

RUN rm -f /etc/service/nginx/down  
RUN rm /etc/nginx/sites-enabled/default  
ADD nginx.conf /etc/nginx/sites-enabled/intercity_website.conf

ADD . /home/app/intercity_website  
WORKDIR /home/app/intercity_website  
RUN chown -R app:app /home/app/intercity_website  
RUN sudo -u app bundle install --deployment  
RUN sudo -u app RAILS_ENV=production rake assets:precompile

RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*  

As you can see this Dockerfile uses the phusion/passenger-ruby21 base image. It adds the Nginx config, adds the application code, runs bundler to install the gems and precompiles the assets.

nginx.conf

# This is the server block that serves our application.
server {  
  server_name intercityup.com;
  root /home/app/intercity_website/public;

  passenger_enabled on;
  passenger_user app;
  passenger_ruby /usr/bin/ruby2.1;
}

# This is the server block that redirects www to non-www.
server {  
  server_name www.intercityup.com;
  return 301 $scheme://intercityup.com$request_uri;
}

Building the app container image

I committed these files to my repository. Now I’m going to upload it to my server and build the container:

my_workstation $ git archive -o app.tar.gz --prefix=app/ master  
my_workstation $ scp app.tar.gz root@178.62.232.206:  
my_workstation $ ssh root@ 178.62.232.206  
server $ tar zxvf app.tar.gz  
server $ docker build --tag="intercity-website" app/  

This command produces a lot of output and results in a Docker image. The first
time I run that docker build command it took a few minutes. That’s because
Docker needs to download the phusion/passenger-ruby21 base image. It only does
this once. When downloading the base image is done it will continue with my own
Dockerfile.

The docker images command now shows me my image:

server $ docker images  
REPOSITORY                 TAG                 IMAGE ID            CREATED             VIRTUAL SIZE  
intercity-website          latest              629f05f42915        3 minutes ago       1.011 GB  

Running the container for the first time

Time to run my app for the first time. Here we go:

server $ docker run --rm -p 80:80 intercity-website  

This command starts my container, produces some output that it is starting things and finally will result in a line:

[ 2014-09-23 11:23:11.9005 113/7fb22942b780 agents/Watchdog/Main.cpp:728 ]: All Phusion Passenger agents started!

Now let’s see if my app is running correctly. Let’s try with curl:

server $ curl -H "Host: intercityup.com" http://localhost/  
<!DOCTYPE html>  
<html>  
<head>  
  <title>We're sorry, but something went wrong (500)</title>
...

Oops! Apparently something is wrong. On examination of the application log file
inside the running container (which I accessed using
the docker-bash tool by Phusion) it seems that I don’t have a database yet. So I’m going to install MySQL on my server first.

Installing the database

I’ll use the standard MySQL server available in Ubuntu 14.04:

server $ apt-get install mysql-server  

After the installation where I set my root password, I can now create the app database:

server $ mysql -u root -p  
mysql> create database intercity_website_production;  
Query OK, 1 row affected (0.00 sec)  
mysql> grant all on intercity_website_production.* to 'intercity' identified by 'rwztBtRW6cFx9C';  
Query OK, 0 rows affected (0.00 sec)  

After this I modified /etc/mysql/my.cnf and changed the bind-address setting
from 127.0.0.1 to my public IP address, 178.62.232.206. This way the Rails app inside my Docker container can use it:

In /etc/mysql/my.cnf I updated the line to this:

bind-address        = 178.62.232.206  

And restarted MySQL:

server $ /etc/init.d/mysql restart  

Using ENV variables to configure the database

I’m going to use ENV variables to have my container use these mysql credentials.
To do this, I need to do two things: 1) Prepare my database.yml file inside the repository to use ENV vars. And 2) configure Nginx to pass these variables into my Passenger process.

Here is my new database.yml, prepared for environment variables:

production:  
  adapter: mysql2
  host: <%= ENV['APP_DB_HOST'] %>
  port: <%= ENV['APP_DB_PORT'] || "3306" %>
  database: <%= ENV['APP_DB_DATABASE'] %>
  username: <%= ENV['APP_DB_USERNAME'] %>
  password: <%= ENV['APP_DB_PASSWORD'] %>

To make these ENV variables work for my Rails app, I need to add a custom Nginx configuration files. This is because Nginx flushes all variables from the environment, except the ones you define.

In my Rails application code, I add another Nginx configuration file called
rails-env.conf:

env APP_DB_HOST;  
env APP_DB_PORT;  
env APP_DB_DATABASE;  
env APP_DB_USERNAME;  
env APP_DB_PASSWORD;  

And I modify my Dockerfile, so it adds the rails_env configuration file when building my container:

FROM phusion/passenger-ruby21  
MAINTAINER Firmhouse "hello@firmhouse.com"

ENV HOME /root  
ENV RAILS_ENV production

CMD ["/sbin/my_init"]

RUN rm -f /etc/service/nginx/down  
RUN rm /etc/nginx/sites-enabled/default  
ADD nginx.conf /etc/nginx/sites-enabled/intercity_website.conf  
# Add the rails-env configuration file
ADD rails-env.conf /etc/nginx/main.d/rails-env.conf

ADD . /home/app/intercity_website  
WORKDIR /home/app/intercity_website  
RUN chown -R app:app /home/app/intercity_website  
RUN sudo -u app bundle install --deployment  
RUN sudo -u app RAILS_ENV=production rake assets:precompile

RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

EXPOSE 80  

Building the image with ENV var support

I committed the new nginx ENV var configuration. Now I’m going to build a new version of my container:

workstation $ git archive -o app.tar.gz --prefix=app/ master  
workstation $ scp app.tar.gz root@178.62.232.206:  
workstation $ ssh root@178.62.232.206  
server $ tar zxvf app.tar.gz  
server $ docker build --tag="intercity-website" app/  

Running rake with ENV vars

After building my container on my server, I can set up the database. In the
following command, I use ENV variables to pass my database connection
information for running the rake task to set up my database. Note that I added
the -u app argument to the command. That argument makes sure the rake
db:setup
task is run as our app user inside the container.

server $ docker run --rm -e "RAILS_ENV=production" -e "APP_DB_HOST=178.62.232.206" -e "APP_DB_DATABASE=intercity_website_production" -e "APP_DB_USERNAME=intercity" -e "APP_DB_PASSWORD=rwztBtRW6cFx9C" -e "APP_DB_PORT=3306" -u app intercity-website rake db:setup

intercity_website_production already exists  
-- create_table("invite_requests", {:force=>true})
   -> 0.0438s
-- initialize_schema_migrations_table()
   -> 0.1085s

Awesome. It worked!

Running the app container with ENV vars

Now I can run the container with the same environment variables as the previous command and try to access it via my browser to see if it works now:

server $ docker run --rm -p 80:80 -e "RAILS_ENV=production" -e "APP_DB_HOST=178.62.232.206" -e "APP_DB_DATABASE=intercity_website_production" -e "APP_DB_USERNAME=intercity" -e "APP_DB_PASSWORD=rwztBtRW6cFx9C" -e "APP_DB_PORT=3306" intercity-website  

When I browse to http://178.62.232.206, I see my Rails app that is connected to the database, and I see that it has compiled the correct assets. Victory!

Conclusion

This concludes this post where I:

  1. Installed Docker on my server
  2. Set up a Dockerfile and built the container image
  3. Configured my database settings by using environment variables

Issues to solve and next steps

I still have questions that need answering. I and the other Intercity developers will write about them. Here are some of the issues that plan to solve:

  • How do I automate deployments? Maybe a tool like Capistrano?
  • What do I need to get zero downtime? When I stop and start a container to run the new app version, there will be connections dropped.
  • Where am I going to store the ENV vars for each of the apps I deploy on a server?
  • How do I speed up building the container? Do I need to run bundler and rake assets:precompile on every deploy?

I hope you enjoyed this post. Please offer advice or ask questions if you have them! Thank you very much for reading.