How I deploy Rails apps
In various mailing lists I read a lot of threads about deploying a Rails app. I want to contribute to the topic with this post, where I’ll describe how I’m now deploying my rails apps in a VPS (actually it’s not a virtual but a physical server, but it’s the same..).
In the past I used Pushion Passenger but it was a very young project and when Unicorn showed up, I felt in love :) I wrote a similar post some years ago, the idea is the same, but the structure is now more solid.
The tools I’m now using are:
- Unicorn as Rack HTTP server
- Nginx as proxy server
- Supervise (part of Daemontools) to monitor the unicorn app
- Capistrano to manage the deploy
- Rbenv to manage the ruby environment
The server’s o.s. is Ubuntu 12.04 LTS.
Rbenv
To install rbenv and ruby-build:
sudo apt-get install build-essential zlib1g-dev openssl libopenssl-ruby1.9.1 libssl-dev libruby1.9.1 libreadline-dev git-core
git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
exec $SHELL -l
mkdir -p ~/.rbenv/plugins
cd ~/.rbenv/plugins
git clone git://github.com/sstephenson/ruby-build.git
rbenv install 2.0.0-p247
rbenv rehash
rbenv global 2.0.0-p247
rbenv local 2.0.0-p247
Just check if everything went ok:
$ ruby -v
ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-linux]
Read this post to switch to Rbenv if you’re using RVM
Capistrano
Create the required folder in the server:
mkdir ~/apps
Now configure your app to be deployed:
cd ~/my_app_path
echo "gem 'capistrano'" >> Gemfile
bundle install
capify .
edit the Capfile file if you need, then edit config/deploy.rb. This is a working example:
require "bundler/capistrano"
require "capistrano-rbenv"
set :rbenv_ruby_version, "2.0.0-p247"
set :user, "server_username"
set :application, "my_app"
set :deploy_to, "/home/#{user}/apps/#{application}"
set :deploy_via, :remote_cache
set :use_sudo, false
set :scm, :git
set :repository, "your_app_git_repo"
default_run_options[:pty] = true
set :ssh_options, { forward_agent: true }
server "my_server.my_domain", :web, :app, :db, primary: true
set :branch, "master"
set :rails_env, "production"
after "deploy", "deploy:cleanup" # keep only the last 5 releases
# Daemontools start/stop
namespace :deploy do
%w[start stop restart].each do |command|
desc "#{command} unicorn server"
task command, roles: :app, except: {no_release: true} do
if command == "start"
sudo "/usr/bin/svc -u /etc/service/my_app"
elsif command == "stop"
sudo "/usr/bin/svc -d /etc/service/my_app"
else
sudo "/usr/bin/svc -t /etc/service/my_app"
end
end
end
task :setup_config, roles: :app do
run "mkdir -p #{shared_path}/config"
end
after "deploy:setup", "deploy:setup_config"
task :symlink_config, roles: :app do
run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml"
end
after "deploy:finalize_update", "deploy:symlink_config"
desc "Make sure local git is in sync with remote."
task :check_revision, roles: :web do
unless `git rev-parse HEAD` == `git rev-parse origin/#{branch}`
puts "WARNING: HEAD is not the same as origin/#{branch}"
puts "Run `git push` to sync changes."
exit
end
end
before "deploy", "deploy:check_revision"
end
You can create the required folders with:
cap deploy:setup
Log in to the server and check the ~/apps/my_app/shared folder. Add these folders if they don’t exist:
cd ~/apps/my_app/shared
mkdir config logs pids sockets
in the config folder create a database.yml file with the rails production environment configurations.
Unicorn
Add the unicorn gem to the rails app:
cd ~/my_app_path
echo "gem 'unicorn'" >> Gemfile
bundle install
Add the unicorn configuration in the shared/config/unicorn.rb file (in the server):
worker_processes 2
working_directory "/home/my_user/apps/my_app/current" # available in 0.94.0+
listen "/home/my_user/apps/my_app/shared/sockets/my_app.sock", :backlog => 64
timeout 30
pid "/home/my_user/apps/my_app/shared/pids/unicorn.pid"
stderr_path "/home/my_user/apps/my_app/shared/log/unicorn.stderr.log"
stdout_path "/home/my_user/apps/my_app/shared/log/unicorn.stdout.log"
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
GC.copy_on_write_friendly = true
before_fork do |server, worker|
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
end
after_fork do |server, worker|
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
end
To launch unicorn I create the ~/service folder. There I create a folder for each project. So:
mkdir -p ~/service/my_app
Then the required files.
~/service/my_app/run (must be executable)
#!/bin/bash
exec su - my_user -c '/home/my_user/service/load_my_app.sh bundle exec unicorn_rails -E production -c /home/my_user/apps/my_app/shared/config/unicorn.rb'
# If you want to launch unicorn manually use te line below instead of the line above (use sudo!). Useful for debugging
# exec su - my_user -c '/home/my_user/service/load_my_app.sh bundle exec unicorn_rails -E production -l /home/my_user/apps/my_app/shared/sockets/my_app.sock'
~/service/load_my_app.sh
#!/bin/bash
export RAILS_ENV="production"
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
cd /home/my_user/apps/my_app/current/
exec $@
As pointed in the comment, you can use the run file to test the app, just modify the file then launch it as root:
cd ~/service/my_app
sudo ./run
You’ll see the familiar unicorn startup process, then it will listen for connections in the given socket.
That’s it, now jump to supervise
Daemontools
Install the required packages:
sudo apt-get install daemontools daemontools-run
After this command you’ll have the svc executable. Before using it, create the symbolic link in the /etc/service folder:
cd /etc/service
sudo ln -s /home/my_user/service/my_app
Supervise automatically launches, at server sturtup, the run executable in the folders present in /etc/service/
To manually startup the app, use svc:
sudo svc -u /etc/service/my_app
This is the same command used by capistrano during deploy (se configuration above).
Nginx
If everything went as expected, the rails app is running and listening for connections in the unix socket at /home/my_user/apps/my_app/shared/sockets/my_app.sock. Now configure Nginx to use that socket.
/etc/nginx/sites-available/www.my_app.my_domain
upstream backend_my_app {
server unix:/home/my_user/apps/my_app/shared/sockets/my_app.sock fail_timeout=0;
}
server {
listen [::]:80;
client_max_body_size 4G;
keepalive_timeout 5;
try_files $uri/index.html $uri.html $uri @app;
root /home/my_user/apps/my_app/current/public/;
index index.html index.htm;
server_name my_app.my_domain www.my_app.my_domain;
location @app {
gzip_static on;
proxy_pass http://backend_my_app;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
root /home/my_user/apps/my_app/current/public/;
index index.html index.htm;
}
location ~* ^/font.+\.(svg|ttf|woff|eot)$ {
root /home/my_user/apps/my_app/current/public/;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /var/www/nginx-default;
}
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}
Symlink this file in /etc/nginx/sites-enabled/ and restart nginx, your app should be online.
When you’ll deploy a new version of the app, Capistrano will require the sudo password to send a TERM signal to supervise, which will restart the rails app.
That’s it, it seems a lot of configuration (and maybe is) but it works great and there are very little differences between the projects, so CTRL-C+CTRL-V works great! :)