December 31, 2015

1557 words 8 mins read

Deploying Ruby on Rails applications with Apache

This time we’ll see how to deploy a rails application using apache and unicorn on ubuntu server!

As you may know deploying a Rails, Node or Django app is not as straight forward as deploying PHP applications, unless you’re using a new generation hosting service that has automated deploys from your repo to the server in just a few clicks. In this guide I’ll explain the entire process to deploy apps by hand. Let’s begin!

The basics

The way you deploy Rails, Node and Django apps is very similar, a web server is in the front line usually is Nginx and rarely is Apache (unless using PHP is more common than Nginx), this web server acts as a reverse proxy passing incoming requests to the app server, depending on the programming language this could be unicorn (commonly used for ruby web apps), gunicorn (commonly used for python web apps) and in the case of node directly to your app (usually you use an app manager).

However, when using the Apache web server that’s not the only way you can do it, you can use a module or extension like mod_passenger or mod_wsgi to support serving applications written in ruby or python respectively, however I don’t like to use them, I prefer the proxy setup since decouples the server from the application processing, but keep in mind that your needs may not be the same as mine, so it is better to explore all the alternatives.

Set up your server

We are going to use rbenv to manage ruby versions in the server, and the rbenv-vars plugin to expose environment variables to our rails application.

# Install rbenv
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ cd ~/.rbenv && src/configure && make -C src
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc

# Install rbenv-vars
$ cd ~/.rbenv/plugins
$ git clone https://github.com/sstephenson/rbenv-vars.git

If you’re having trouble with these commands you can always refer to the official documentation for a more in depth installation guide.

Deploying a Ruby On Rails app

First we need a Rails app, if you don’t have one go ahead and create one now:

$ rails new hello-world-rails

You should have a similar directory structure:

hello-world-rails
+ app/
+ bin/
+ config/
+ db/
+ lib/
+ log/
+ public/
+ test/
+ tmp/
+ vendor/
Gemfile
Gemfile.lock
config.ru
README.rdoc
Rakefile

We are going to use Unicorn as the app server, so add it to the Gemfile:

gem 'unicorn'

Save the file and run bundle install or just bundle on the terminal to install it:

$ bin/bundle

Now we have unicorn installed, great!. At this point we need to create the config file config/unicorn.rb with the following contents:

# Path to application
app_dir = File.expand_path("../..", __FILE__)
shared_dir = "#{app_dir}/shared"
working_directory app_dir

# Unicorn options
worker_processes 2
preload_app true
timeout 30

# Socket location
listen "#{shared_dir}/sockets/unicorn.sock", :backlog => 64

# Logging
stderr_path "#{shared_dir}/log/unicorn.stderr.log"
stdout_path "#{shared_dir}/log/unicorn.stdout.log"

# Master PID location
pid "#{shared_dir}/pids/unicorn.pid"

This file does five things:

  1. Sets the path to the rails application.
  2. Configures unicorn options (worker_processes, timeout, etc).
  3. Defines where the socket should be created (unicorn.sock).
  4. Configures the logs directory (unicorn.stderr.log and unicorn.stdout.log).
  5. Defines where the pid file should be created (unicorn.pid).

Now let’s create the directories where the unicorn sock and pid should be saved:

$ mkdir -p shared/pids shared/sockets shared/log

If you like to keep this folder structure in the repo as I do, then we can do it like this:

$ touch shared/pids/.gitkeep shared/sockets/.gitkeep shared/log/.gitkeep
$ git add shared/ && git commit -m "Add shared folder structure"

Now in the server clone the app and create the .rbenv-vars File:

RAILS_ENV=production
SECRET_KEY_BASE=cf46c25...

All the variables in this file will be exposed to your application, remember to use a strong SECRET_KEY_BASE for your application, you can generate one using:

$ bin/rake secret

Save the file and test if the variables are read:

$ rbenv vars

You should see the variables displayed, if not try restarting the session to reload the shell environment. Now it’s time to precompile the assets and migrate the database (in case we’re using one):

$ bin/rake assets:precompile  # May need nodejs installed
$ bin/rake db:create && bin/rake db:migrate  # Create and migrate database

At this point we can test the application by setting up the rails server, assuming our public IP is 105.28.27.90 we pass it to the --binding flag:

$ bin/rails server --binding=105.28.27.90

If we open up this URL (http://105.28.27.90:3000/) in our web browser we should see the application welcome page, if is not working then try to debug you app and fix it first before continuing with the guide (remember to install with bundle).

Stop the server by pressing Ctrl-c in the keyboard. Now it’s time to configure our unicorn script for running our app.

Setting up the init script

We need to create an init script to manage our application with unicorn as a daemon service, so we create the script in the /etc/init.d/ directory:

$ sudo vim /etc/init.d/unicorn_hello-world-rails

Add the following content:

#!/bin/sh

### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the unicorn app server
# Description:       starts unicorn using start-stop-daemon
### END INIT INFO

set -e

USAGE="Usage: $0 <start|stop|restart|upgrade|rotate|force-stop>"

# app settings
USER="nonrootuser"
APP_NAME="hello-world-rails"
APP_ROOT="/home/$USER/$APP_NAME"
ENV="production"

# environment settings
PATH="/home/$USER/.rbenv/shims:/home/$USER/.rbenv/bin:$PATH"
CMD="cd $APP_ROOT && bundle exec unicorn -c config/unicorn.rb -E $ENV -D"
PID="$APP_ROOT/shared/pids/unicorn.pid"
OLD_PID="$PID.oldbin"

# make sure the app exists
cd $APP_ROOT || exit 1

sig () {
    test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
    test -s $OLD_PID && kill -$1 `cat $OLD_PID`
}

case $1 in
start)
sig 0 && echo >&2 "Already running" && exit 0
echo "Starting $APP_NAME"
su - $USER -c "$CMD"
;;
stop)
echo "Stopping $APP_NAME"
sig QUIT && exit 0
echo >&2 "Not running"
;;
force-stop)
echo "Force stopping $APP_NAME"
sig TERM && exit 0
echo >&2 "Not running"
;;
restart|reload|upgrade)
sig USR2 && echo "reloaded $APP_NAME" && exit 0
echo >&2 "Couldn't reload, starting '$CMD' instead"
$CMD
;;
rotate)
sig USR1 && echo rotated logs OK && exit 0
echo >&2 "Couldn't rotate logs" && exit 1
;;
*)
echo >&2 $USAGE
exit 1
;;
esac

Don’t forget to modify this two lines and set the user running your app as well as the app name:

USER="nonrootuser"
APP_NAME="hello-world-rails"

Now this file will allow us to manage the application process, in order to use it let’s do the setup:

$ sudo chmod 755 /etc/init.d/unicorn_hello-world-rails
$ sudo update-rc.d unicorn_hello-world-rails defaults

Now start the app by running:

$ sudo service unicorn_hello-world-rails start

We can be sure the app is up and running if the file shared/sockets/unicorn.sock exists, as well as by using the ss command like this:

$ ss -lxn | grep unicorn

If we see the unicorn.sock as LISTEN then it’s all fun and games!

Configure the web server

We’re almost done! Let’s setup Apache (I would prefer Nginx though), add a virtualhost conf file for the site configuration:

$ vim /etc/apache2/sites-available/hello-world-rails.conf

Add the following to the file:

<VirtualHost *:80>

# Setup server name and aliases
ServerName yourdomain.com
ServerAlias yourdomain.com

# Setup public folder
DocumentRoot /path/to/my/hello-world-rails/public

# Setup web server logs
ErrorLog /path/to/my/logs/hello-world-rails_error_log
CustomLog /path/to/my/logs/hello-world-rails_access_log combined

# Setup reversed proxy to unicorn
ProxyPass / unix:/path/to/my/hello-world-rails/shared/sockets/unicorn.sock|http://yourdomain.com/
ProxyPassReverse / unix:/path/to/my/hello-world-rails/shared/sockets/unicorn.sock|http://yourdomain.com/
ProxyPreserveHost On

<Directory /path/to/my/hello-world-rails/public>
Options -Indexes +IncludesNOEXEC +SymLinksIfOwnerMatch +ExecCGI
allow from all
AllowOverride All Options=ExecCGI,Includes,IncludesNOEXEC,Indexes,MultiViews,SymLinksIfOwnerMatch
Require all granted
</Directory>

</VirtualHost>

Inspect the file and replace the paths and domains to match your setup, the important thing to understand is that Apache is listening on port 80 (default http port) and will forward all request matching / to unicorn via a UNIX socket. If instead you’ve configured unicorn to listen on a TCP port then your proxy definition should be more like this:

ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
ProxyPreserveHost On

Is important to notice how in this case our app server is running on localhost this hides our app server from external traffic protecting the load of the app, take this example:

# This is bad!
ProxyPass / http://105.28.27.90:3000/
ProxyPassReverse / http://105.28.27.90:3000/
ProxyPreserveHost On

In this case our app is publicly visible in http://105.28.27.90:3000 which is bad for production environments since a request can make it to our application without passing through the web server (Apache in our case) and in fact this will have trouble handling static files of our project. We should always make our app listen locally, that’s why here we used a UNIX socket file instead of choosing a TCP address and port.

Finally reload the server configuration to put changes into effect:

$ sudo service apache2 reload

In case you don’t have the mod_proxy and the mod_proxy_http enabled you’ll face some errors, if this is the case enable them first and restart the server afterwards.

$ sudo a2enmod proxy proxy_http
$ sudo service apache2 restart

If you have an error while restarting your web server try to use the configtest command, most of the times it will tell you what is wrong:

$ sudo apache2ctl configtest

And that’s it! View the app in your browser http://yourdomain.com/.

This is a lot of work!, if you’re interested in making this process a little bit easier and maintainable better use an automation deploying tool like capistrano, I hope to write in the future about it. Cheers!