Rails deployment step by step. Deployment process with commands to a fresh server.
Author: Sergey Kuznetsov kuznecov.sg@gmail.com
Published: April 2013
License: CC BY-SA (Creative Commons Attribution-ShareAlike)
Special note: This tutorial is based on capistrano 2.x, not 3.x
24.04.2013: updated nginx config to serve pre-gzipped assets static files and to set proper headers for them.
05.06.2013: updated commands for adding init.d script to autostart (thanks to Andrey Samsonov)
27.06.2013: small update to /etc/init.d unicorn script
04.09.2013: content of Capfile - we should explicitly load assets cap-tasks
22.09.2013: added section ‘logrotate config’
Buzz-words:
- Ubuntu 12.04 LTS
- System-wide RVM
- Postgresql 9.1
- Nginx
- Ruby 1.9.3
- Rails 3.2
- Capistrano 2.15.5
Time from start to complete is about 1-1.5 hours.
user@server
is a prefix for commands runned on the server side by user, dev
on opposite on a developers machine.
OS installation
I will not describe how to install your system (it’s pretty much straight forward), but I have to point out on the next thing:
Do not use encryption for home folders (or you will have some kind of problems with ssh-keys).
Server updates right after OS installation
dev:~ $ ssh server
user@server:~$ sudo -i
root@server:~# apt-get update
root@server:~# apt-get dist-upgrade
root@server:~# shutdown -r now
SSH with public keys
dev:~ $ scp .ssh/id_dsa.pub server:~
dev:~ $ ssh server
Password: ...
user@server:~$ mkdir ~/.ssh
user@server:~$ chmod 700 ~/.ssh
user@server:~$ mv id_dsa.pub ~/.ssh/authorized_keys
user@server:~$ chmod 600 ~/.ssh/authorized_keys
The next time you log in to server it will not ask your password.
RVM install
I prefer to use system-wide installation of RVM.
Something got there https://www.digitalocean.com/community/articles/how-to-install-ruby-on-rails-on-ubuntu-12-04-lts-precise-pangolin-with-rvm
dev:~ $ ssh server
user@server:~$ sudo apt-get -y install curl build-essential openssl libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt-dev autoconf libc6-dev libgdbm-dev ncurses-dev automake libtool bison subversion pkg-config libffi-dev
user@server:~$ \curl -L https://get.rvm.io | sudo bash -s stable
user@server:~$ sudo usermod -a -G rvm user
user@server:~$ sudo usermod -a -G rvm root
user@server:~$ exit
dev:~ $ ssh server
user@server:~$ rvm install 1.9.3
user@server:~$ rvm use 1.9.3 --default
Node.js install
Something got there https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager
We need node.js for asset compilation purposes (there are other options, but node.js is much more quickiest way to compile assets).
user@server:~$ sudo apt-get install python-software-properties python
user@server:~$ sudo add-apt-repository ppa:chris-lea/node.js
user@server:~$ sudo apt-get update
user@server:~$ sudo apt-get install nodejs
Postgresql install
user@server:~$ sudo apt-get -y install postgresql libpq-dev
Nginx config
user@server:~$ sudo apt-get install nginx
user@server:~$ sudo vim /etc/nginx/sites-available/com.example
Contents of /etc/nginx/sites-available/com.example
:
upstream com_example_unicorn {
server unix:/var/www/com.example/shared/system/unicorn.sock fail_timeout=0;
}
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/com.example-access.log;
error_log /var/log/nginx/com.example-error.log;
keepalive_timeout 5;
root /var/www/com.example/current/public;
try_files $uri/index.html $uri.html $uri @app;
location @app {
proxy_set_header X-Forwarded_for $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://com_example_unicorn;
}
location ~ ^/(assets)/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
error_page 500 502 503 504 /500.html;
location = /500.html {
root /var/www/com.example/current/public;
}
}
user@server:~$ sudo ln -s /etc/nginx/sites-available/com.example /etc/nginx/sites-enabled/com.example
user@server:~$ sudo vim /etc/nginx/nginx.conf
Uncomment this line in /etc/nginx/nginx.conf
:
#server_names_hash_bucket_size 64;
Now we able to restart nginx:
user@server:~$ sudo /etc/init.d/nginx restart
init.d shell script
We must be able to start/stop/restart our Rails application in easy way, and it must autostart after system reboot (like other daemons).
user@server:~$ sudo vim /etc/init.d/unicorn_com.example
Contents of /etc/init.d/unicorn_com.example
(EUSER must be the same as main server user):
#!/bin/bash
### BEGIN INIT INFO
# Provides: unicorn
# Required-Start: $local_fs $remote_fs $network $syslog
# Required-Stop: $local_fs $remote_fs $network $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: starts the unicorn web server
# Description: starts unicorn
### END INIT INFO
# Add to auto-start on server start
# sudo update-rc.d unicorn_com.example defaults
# Remove from auto-start
# sudo update-rc.d -f unicorn_com.example remove
RVM_ENV="ruby-1.9.3-p392@com-example"
APP_NAME="com.example"
APP_PATH="/var/www/$APP_NAME"
EUSER="user"
RVM="source /usr/local/rvm/environments/$RVM_ENV"
CMD="$RVM && cd $APP_PATH/current && bundle exec unicorn"
CMD_OPTS="-c $APP_PATH/current/config/unicorn.rb -E production -D"
PID="$APP_PATH/shared/pids/unicorn.pid"
NAME=`basename $0`
DESC="Unicorn app for $APP_NAME"
case "$1" in
start)
echo -n "Starting $DESC: "
su - $EUSER -c "$CMD $CMD_OPTS"
echo "$NAME."
;;
stop)
echo -n "Stopping $DESC: "
if [ -f $PID ] && [ -e /proc/$(cat $PID) ]
then
kill -QUIT `cat $PID`
echo "$NAME."
else
echo "Unable to get $PID file, or process is down"
fi
;;
restart)
echo -n "Restarting $DESC: "
if [ -f $PID ] && [ -e /proc/$(cat $PID) ]
then
kill -USR2 `cat $PID`
else
su - $EUSER -c "$CMD $CMD_OPTS"
fi
echo "$NAME."
;;
*)
echo "Usage: $NAME {start|stop|restart}" >&2
exit 1
;;
esac
exit 0
Then run this commands:
user@server:~$ sudo chmod 755 /etc/init.d/unicorn_com.example
user@server:~$ sudo update-rc.d unicorn_com.example defaults
user@server:~$ sudo visudo
Add this line to the end of sudoers file:
user ALL=(ALL) NOPASSWD: /etc/init.d/unicorn_com.example
Save it and close.
Unicorn config
Here is an example of unicorn.rb config:
APP_PATH = '/var/www/com.example'
rails_env = ENV['RAILS_ENV'] || 'development'
worker_processes 5
working_directory APP_PATH + '/current'
preload_app true
timeout 60
listen APP_PATH + '/shared/system/unicorn.sock', backlog: 64
pid APP_PATH + '/shared/pids/unicorn.pid'
stderr_path APP_PATH + '/shared/log/unicorn.stderr.log'
stdout_path APP_PATH + '/shared/log/unicorn.stdout.log'
before_exec do |server|
ENV['BUNDLE_GEMFILE'] = APP_PATH + '/current/Gemfile'
end
before_fork do |server, worker|
defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
old_pid = "#{server.config[:pid]}.oldbin"
if File.exists?(old_pid) && server.pid != old_pid
begin
Process.kill(:QUIT, File.read(old_pid).to_i)
rescue
# someone else did our job for us
end
end
end
after_fork do |server, worker|
defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end
Almost done. It’s time to deploy our Rails application.
Capistrano
Ensure that you have included capistrano gem to your Gemfile:
group :development do
# ...
gem 'capistrano', '~> 2.15.5'
gem 'capistrano_colors'
gem 'rvm-capistrano'
# ...
end
Capify project and update deploy.rb as you need:
dev:~/com.example $ capify .
dev:~/com.example $ vim config/deploy.rb
Content of ~/com.example/config/deploy.rb
:
require 'bundler/capistrano'
require 'rvm/capistrano'
set :rvm_type, :system
set :rvm_ruby_string, 'ruby-1.9.3-p392@com-example'
set :application, 'com.example'
set :scm, :git
set :repository, 'git@github.com:user/com-example.git'
set :branch, 'master'
set :use_sudo, false
set :user, 'user' # username on the server
set :deploy_to, "/var/www/#{application}"
set :keep_releases, 5
set :ssh_options, { forward_agent: true }
server 'example.com', :app, :web, :db, primary: true
# to use new assets approach
set :normalize_asset_timestamps, false
# unicorn related
set :unicorn_conf, "#{deploy_to}/current/config/unicorn.rb"
set :unicorn_pid, "#{deploy_to}/shared/pids/unicorn.pid"
namespace :deploy do
after 'deploy', 'deploy:cleanup'
desc 'Zero-downtime restart of Unicorn'
task :restart do
run "sudo /etc/init.d/unicorn_#{application} restart"
end
task :start do
run "sudo /etc/init.d/unicorn_#{application} start"
end
task :stop do
run "sudo /etc/init.d/unicorn_#{application} stop"
end
end
We need to uncomment line with loading assets tasks in a ~/com.example/Capfile
, so it will be like this one:
load 'deploy'
load 'deploy/assets'
load 'config/deploy'
Predeploying
dev:~/com.example $ ssh server
user@server:~$ sudo mkdir -p /var/www/com.example
user@server:~$ sudo chown user:user /var/www/com.example
user@server:~$ rvm use 1.9.3-p392@com-example --create
Database
Here is example of config/database.yml
file:
development:
adapter: postgresql
encoding: unicode
database: com_example_development
pool: 5
test:
adapter: postgresql
encoding: unicode
database: com_example_test
pool: 5
production:
adapter: postgresql
encoding: unicode
database: com_example_production
host: 127.0.0.1
pool: 5
username: user
password: myPassword
Use your own production database name from database.yml. user
in CREATE USER
must be the same as your main user from server.
user@server:~$ sudo -i
root@server:~# su - postgres
postgres@server:~$ psql template1
template1=# CREATE USER user WITH PASSWORD 'myPassword';
template1=# CREATE DATABASE com_example_production;
template1=# GRANT ALL PRIVILEGES ON DATABASE com_example_production TO user;
template1=# \q
postgres@server:~$ exit
root@server:~# exit
Manually confirm repository key
dev:~/com.example $ ssh server
user@server:~$ git clone git@github.com:user/com-example.git
Cloning into 'com-example'...
The authenticity of host 'github.com (207.223.240.181)' can't be established.
RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40.
Are you sure you want to continue connecting (yes/no)? yes
Run migrations (in the future you’ll be able to run migrations by cap deploy:migrate
command from development machine)
user@server:~$ cd com-example
user@server:~/com-example$ rvm use 1.9.3-p392@com-example
user@server:~/com-example$ bundle install
user@server:~/com-example$ RAILS_ENV=production rake db:migrate
Deploying
dev:~/com.example $ cap deploy:setup
dev:~/com.example $ cap deploy
We are done. Now you are able to use the next workflow:
dev:~/com.example $ # ... some changes to project
dev:~/com.example $ git add .
dev:~/com.example $ git commit -am "Commit message"
dev:~/com.example $ git push
dev:~/com.example $ cap deploy
logrotate config
We should be ready for incoming traffic. And we should aware of huge accessing log files of our app. So we can config a logrotate program with some simple actions. Here is the steps:
user@server:~$ sudo vim /etc/logrotate.d/unicorn_com.example
Here is the sample content of that file:
/var/www/com.example/shared/log/*.log {
weekly
missingok
rotate 30
compress
delaycompress
notifempty
copytruncate
size=100M
}
That’s all!
Now logrotate which runs on a daily basis, will rotate our app log-files if they become bigger than 100Mb. Description of other options you can find here http://linuxcommand.org/man_pages/logrotate8.html