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:
- Sets the path to the rails application.
- Configures unicorn options (worker_processes, timeout, etc).
- Defines where the socket should be created (unicorn.sock).
- Configures the logs directory (unicorn.stderr.log and unicorn.stdout.log).
- 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!