Setup and deploy Rails app with Capistrano
2019.09 update: RVM installation commands
When I was a new Ruby developer, deploying a Rails app seems to be an enormous task (and I don't talk about Heroku here). Capistrano used to be a life-saver for me back then, but at that time it was still a matter of copy and paste.
Recently I have chance to use Capistrano again after a long time using Heroku. With all the tutorials I found it still seems enormous as it used to be. So I decide to write a complete guide to deploy a Rails app with Capistrano with various envinronments and settings.
The flow
Regardless of remote host OS (Ubuntu/Centos), application servers (Puma/Unicorn/Passenger) and HTTP servers (Nginx/Apache), the flow of setting up server and deploy Rails app stay the same as follow:
- Create deploy account
- Create deploy account with sudo privilege
- Generate SSH keys and add it to Github repo deployment key
- Allow SSH from local machine
- Install Ruby on Rails & dependencies
- Capistrano
- with Puma
- with Unicorn [next month]
- with Passinger/Raptor [next month]
- Install and config nginx
More advance stuffs: (will be added in future)
- Custom Capistrano tasks
- Complex Capistrano config for multiple servers deployment
1. Create deploy account on remote server
It is a good practice not to use root
account as deployment account.
Usually we create a dedicated user for deployment purpose, often named deploy
with sudo access.
Create deploy account with sudo privilege
As root
On Ubuntu
adduser deploy
gpasswd -a deploy sudo
On Centos
adduser deploy
# Set password for deploy user
passwd deploy
usermod -aG wheel deploy
Generate SSH keys and add it to Github repo deployment key
Now login to the new user you've created:
su - deploy
On Ubuntu
Generate new SSH key pair:
ssh-keygen
You will be ask to enter key name and passphase, just skip them.
Shake hand with Github / Bitbucket:
ssh -T [email protected]
ssh -T [email protected]
You will likely get a Permission denied (publickey)
message. It is because you haven't add deploy
public key to Github / Bitbucket deployment key yet.
To show deploy
public key, run:
cat ~/.ssh/id_rsa.pub
Copy this key to Github / Bitbucket repo follow instruction:
Now shake hand again (e.g. Github):
ssh -T [email protected]
> Hi github-username/repo-name! You've successfully authenticated, but GitHub does not provide shell access.
If you get the message above, then everything is fine.
Allow SSH from local machine
Assume you are going to run Capistrano command from your local machine, you need to add your local machine public key to list of authorized keys on remote server.
Open this file from deploy
and paste your local machine public key and save it
vi ~/.ssh/authorized_keys
To make sure this work, try to run this from your local machine:
ssh deploy@server-ip
Install Ruby on Rails & dependencies
We should use a ruby version manager to install and manage multiple Ruby versions.
There are 2 popular choices: RVM and rbenv. This article will use RVM for this purpose.
From deploy
user:
On Ubuntu
# Update package index
sudo apt-get update
# Install curl
sudo apt-get install curl
# install RVM
gpg2 --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://get.rvm.io | bash -s stable
echo "source $HOME/.rvm/scripts/rvm" >> ~/.bash_profile
source ~/.rvm/scripts/rvm # activate RVM
rvm requirements # install dependencies
# Install Ruby 2.3.0 & Rails 4.2.6
rvm install 2.3.0
rvm use 2.3.0 --default
gem install rails -v '4.2.6' -V --no-ri --no-rdoc
gem install bundler -V --no-ri --no-rdoc
# Install git
sudo apt-get install git-core
# Install database engine based on your project
sudo apt-get install mysql-server
sudo apt-get install postgresql postgresql-contrib libpq-dev
# Install nodejs, your Rails app needs a JS runtime
sudo apt-get install nodejs
# Install yarn
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get install yarn
# Install nginx
sudo apt-get install nginx
# Others
sudo apt-get install redis-server
sudo apt-get install imagemagick
On Centos / Amazon Linux
# Update package index
sudo yum -y update
# Install curl
sudo yum install curl
# install RVM
gpg2 --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
curl -L get.rvm.io | bash -s stable
source ~/.rvm/rvm.sh # activate RVM
rvm requirements # install dependencies
# Install Ruby 2.3.0 & Rails 4.2.6
rvm install 2.3.0
rvm use 2.3.0 --default
gem install rails -v '4.2.6' -V --no-ri --no-rdoc
gem install bundler -V --no-ri --no-rdoc
# Install git
sudo yum install git
# Install database engine based on your
sudo yum install mysql-server
sudo yum install postgresql-server postgresql-contrib postgresql-devel
# Install nodejs, your Rails app needs a JS runtime
sudo yum -y install nodejs
Capistrano
With Puma
Add following gems to Gemfile
group :development do
gem 'capistrano', require: false
gem 'capistrano-rvm', require: false
gem 'capistrano-rails', require: false
gem 'capistrano-bundler', require: false
gem 'capistrano3-puma', require: false
end
gem 'puma'
then bundle those gems
bundle
then install Capistrano:
cap install
This will create:
Capfile
config/deploy.rb
: main config about deployment processconfig/deploy
: envinronment specific config (e.g.staging.rb
,production.rb
)
Change the content of Capfile
to:
# Load DSL and Setup Up Stages
require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/rails'
require 'capistrano/bundler'
require 'capistrano/rvm'
require 'capistrano/puma'
install_plugin Capistrano::Puma
require 'capistrano/scm/git'
install_plugin Capistrano::SCM::Git
# Loads custom tasks from `lib/capistrano/tasks' if you have any defined.
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
And change the content of config/deploy.rb
to:
# Change these
server 'your_server_ip', roles: [:web, :app, :db], primary: true
set :repo_url, '[email protected]:username/appname.git'
set :application, 'appname'
set :user, 'deploy'
set :puma_threads, [4, 16]
set :puma_workers, 0
# Don't change these unless you know what you're doing
set :pty, true
set :use_sudo, false
set :stage, :production
set :deploy_via, :remote_cache
set :deploy_to, "/home/#{fetch(:user)}/apps/#{fetch(:application)}"
set :puma_bind, "unix://#{shared_path}/tmp/sockets/#{fetch(:application)}-puma.sock"
set :puma_state, "#{shared_path}/tmp/pids/puma.state"
set :puma_pid, "#{shared_path}/tmp/pids/puma.pid"
set :puma_access_log, "#{release_path}/log/puma.error.log"
set :puma_error_log, "#{release_path}/log/puma.access.log"
set :ssh_options, { forward_agent: true, user: fetch(:user), keys: %w(~/.ssh/id_rsa.pub) }
set :puma_preload_app, true
set :puma_worker_timeout, nil
set :puma_init_active_record, true # Change to false when not using ActiveRecord
## Defaults:
# set :scm, :git
# set :branch, :master
# set :format, :pretty
# set :log_level, :debug
# set :keep_releases, 5
## Linked Files & Directories (Default None):
set :linked_files, %w{config/database.yml}
set :linked_dirs, %w{log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system}
namespace :puma do
desc 'Create Directories for Puma Pids and Socket'
task :make_dirs do
on roles(:app) do
execute "mkdir #{shared_path}/tmp/sockets -p"
execute "mkdir #{shared_path}/tmp/pids -p"
end
end
before :start, :make_dirs
end
namespace :deploy do
desc "Make sure local git is in sync with remote."
task :check_revision do
on roles(:app) do
unless `git rev-parse HEAD` == `git rev-parse origin/master`
puts "WARNING: HEAD is not the same as origin/master"
puts "Run `git push` to sync changes."
exit
end
end
end
desc 'Initial Deploy'
task :initial do
on roles(:app) do
before 'deploy:restart', 'puma:start'
invoke 'deploy'
end
end
before :starting, :check_revision
after :finishing, :compile_assets
after :finishing, :cleanup
end
For the first time, we need to run
cap production deploy:initial
From then on:
cap production deploy
Quick command to control Puma:
cap puma:restart # restart puma
cap puma:start # Start puma
cap puma:status # status puma
cap puma:stop # stop puma
For a full list of capistrano task, run:
cap -T
Config Nginx
Server block files are what specify the configuration of our separate sites and dictate how the Nginx web server will respond to various domain requests.
To begin, we will need to set up the directory that our server blocks will be stored in, as well as the directory that tells Nginx that a server block is ready to serve to visitors. The sites-available directory will keep all of our server block files, while the sites-enabled directory will hold symbolic links to server blocks that we want to publish. We can make both directories by typing:
sudo mkdir /etc/nginx/sites-available
sudo mkdir /etc/nginx/sites-enabled
nginx.conf
upstream puma {
server unix:///home/deploy/apps/appname/shared/tmp/sockets/appname-puma.sock;
}
server {
listen 80 default_server deferred;
# server_name example.com;
root /home/deploy/apps/appname/current/public;
access_log /home/deploy/apps/appname/current/log/nginx.access.log;
error_log /home/deploy/apps/appname/current/log/nginx.error.log info;
location ^~ /assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
try_files $uri/index.html $uri @puma;
location @puma {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://puma;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 10M;
keepalive_timeout 10;
}
Fix nginx permission denied:
The only wat I could make it works was to change this line in /etc/nginx/nginx.conf
from
user nginx;
# to
user root;
Extra tips
Add RAILS_ENV environment variable to server
Assume that we just setup our production server. Adding the following line to ~/.bashrc
export RAILS_ENV=production
or to etc/environment
RAILS_ENV=production
This will simplify Rails commands when you need to use.
# before RAILS_ENV=production rails c # after rails c
Quickly create PostgreSQL user and password
If we use PostgreSQL as DB for Rails app, the first deployment often come up with following error
PG::ConnectionBad FATAL: role “deploy” does not exist
The following command allow to quickly create a PostgreSQL user and password to use in database.yml
sudo -u postgres createuser -P -d -e deploy
See more at StackOverflow