Check out my newest tutorial for deploying the same stack onto Ubuntu 20.04 here.
In this tutorial, I’ll be explaining how to deploy a Ruby on Rails project. The stack I’ll be using is:
Ubuntu 18.04 LTS
Ruby, version 2.3.1
Rails, version 4.2.5.1
RVM
Nginx
Puma
Capistrano
NodeJS, version 8.12.0 LTS
A small aside: I'm writing this article because of the difficulty finding a tutorial that uses this specific stack with Puma
on Ubuntu 18.04
.
Two popular alternatives to Puma
are Unicorn
and Passenger
.
There is a great article from the team at Scout that breaks down the differences between the three, but the gist is:
This tutorial assumes you have already setup a web server with Ubuntu 18.04
with Nginx
, RVM
, Ruby
, and Rails
.
Digital Ocean has a great One-Click LEMP stack that can be deployed in just a few clicks. They also have a great tutorial for installing RVM, Ruby, and Rails.
If you enjoy this tutorial and don't have a Digital Ocean account already, consider using this referral link. You get $10 and I get $10. It's a win-win.
If you use a MySQL database with Rails (rails new PROJECT_NAME -d mysql
), you'll need to run sudo apt-get install libmysqlclient-dev
on your Production
machine or you'll run into bundle errors.
Special thanks to Didier for catching a minor typo in the deploy.rb
file.
If you happen to find a typo, please let me know!
With all that out of the way, let’s begin.
This deploy process pulls from a repository to keep code in order. In this particular case, we’re using GitHub. To access GitHub smoothly, we’re going to setup SSH keys. This allows us to deploy securely without having to type our password each time.
Terminal: Production
ssh -T git@github.com
It’ll prompt you if you approve the fingerprint, then tell you that permissions are denied. This is because we don’t have the publickey setup yet.
First, generate a new SSH key:
Terminal: Production
ssh-keygen -t rsa
Follow the default prompts, leaving the password blank. You should generate a new SSH key at ~/.ssh/id_rsa.pub
.
Follow these instructions to add the newly created key to your GitHub project.
Now if you re-run the ssh -T git@github.com
command, you should see it succeed.
Capistrano is used to automate deployments. Once setup, rolling out deployments is painless and easy with a single terminal command. The hardest part is just setting it up.
To begin, let’s add Capistrano
to our project. We’ll do that by adding the gem to our Gemfile
.
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 install the newly added gems with:
Terminal: Development
bundle
Next, let’s use Capistrano
to create the necessary config files.
Terminal: Development
cap install
This creates:
Capfile
, used to load some pre-defined scripts (selecting the right version of Ruby, pre-compiling assets, installing new gems, and more).config/deploy.rb
, which holds configuration and environment variablesThis also creates a config/deploy
directory, but we don’t use it in this tutorial. They hold config files that are for environment-specific configurations (staging and deployment).
First, let’s update our Capfile
. Replace the contents with the following:
Capfile
# Load DSL and Setup Up Stages
require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/rails'
require 'capistrano/bundler'
require 'capistrano/rvm'
require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git
require 'capistrano/puma'
install_plugin Capistrano::Puma
# Loads custom tasks from `lib/capistrano/tasks' if you have any defined.
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
Next, replace the contents of config/deploy.rb
with the following, changing the parts where appropriate. Specifically:
root
for security reasons)'my-rails-app'
instead of 'my rails app'
).config/deploy.rb
# Change these
server '[PRODUCTION_IP]', port: 22, roles: [:web, :app, :db], primary: true
set :repo_url, '[YOUR GIT SSH ADDRESS: git@example.com:username/appname.git]'
set :application, '[APP_NAME]'
set :user, '[USER_NAME]'
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.access.log"
set :puma_error_log, "#{release_path}/log/puma.error.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{bin 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
desc 'Restart application'
task :restart do
on roles(:app), in: :sequence, wait: 5 do
invoke 'puma:restart'
end
end
before :starting, :check_revision
after :finishing, :compile_assets
after :finishing, :cleanup
after :finishing, :restart
end
# ps aux | grep puma # Get puma pid
# kill -s SIGUSR2 pid # Restart puma
# kill -s SIGTERM pid # Stop puma
A reader reported receiving the error Net::SSH::AuthenticationFailed
. Their solution was to edit the line set :ssh_options
above and remove the .pub
extension so that it's simply ...%w(~/.ssh/id_rsa) }
.
Now we’ll setup the Nginx
config files. This will allow us to communicate with Puma
.
Nginx
is a web sevrver, where Puma
is an application server. Nginx
intercepts the incoming web traffic, and passes it to Puma
which works with our Rails
project. A more detailed explanation can be found here and here.
Create a new file config/nginx.conf
and populate it with the following:
config/nginx.conf
upstream puma {
server unix:///home/[USER_NAME]/apps/[APP_NAME]/shared/tmp/sockets/[APP_NAME]-puma.sock;
}
server {
listen 80 default_server deferred;
# If you're planning on using SSL (which you should), you can also go ahead and fill out the following server_name variable:
# server_name example.com;
# Don't forget to update these, too
root /home/[USER_NAME]/apps/[APP_NAME]/current/public;
access_log /home/[USER_NAME]/apps/[APP_NAME]/current/log/nginx.access.log;
error_log /home/[USER_NAME]/apps/[APP_NAME]/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;
}
You’re all set with configuration! Let’s make your first deployment.
As I mentioned earlier, we’ll be deploying not from your local dev environment, but from the project’s repository. So to begin, commit your project to your GitHub repo (the same one we specified earlier). Then, on your developer environment’s terminal:
Terminal: Development
cap production deploy:initial
The initial
option will create and configure the Production
server for deployments. As the name implies, it only needs to be included this first time.
This will take a few minutes to run. If you run into any issues, it’s typically because:
database.yml
is mis-configuredIf everything is deploying properly, let’s finish setting up Nginx
.
Finally, we just need to tell Nginx to use our nginx.conf
file we created earlier. On the Production
machine, swap the config files by:
Terminal: Production
sudo rm /etc/nginx/sites-enabled/default # Remove old config file
sudo ln -nfs "/home/[USER_NAME]/apps/[APP_NAME]/current/config/nginx.conf" "/etc/nginx/sites-enabled/[APP_NAME]" # Create a symlink to our new one
Test to make sure the new config file is working:
Terminal: Production
nginx -t
If there’s an error, double-check your nginx.conf
file. Otherwise, restart Nginx
to have the new config file take effect:
Terminal: Production
sudo service nginx restart
Refresh your browser and you should see your new site!
If you get a "An unhandled lowlevel error occurred. The application logs may have details." message, it's most likely due to not setting the SECRET_KEY_BASE
.
You can set fix this by running rake secret
and copying the alphanumeric string into the Production option config/secrets.yml
. More info here.
This might not be best practice, but for whatever reason I always have issues setting this global variable, so I just manually hard-code it.
You’re all done! Now, anytime you want to push an update, all you have to do is commit your changes to your repo, then run:
Terminal: Development
cap production deploy
🎉 Happy deployments! 🎉