Setting up a Rails environment on a VPS with rvm, nginx, Puma (with jungles), PostgreSQL and deploy via Capistrano

It's the last day of the year again, and it feels like a good tradition to write a nice post on this very particular day.

And since a new year could mean a new life, I wanted to refresh a bit the small infrastructure holding together this blog and all of the web apps I need to deploy.

This website was hosted on a small VPS running on an outdated Ubuntu server, running Passenger (Open source) with it's embedded nginx and configurations.

For security reasons I wanted to reinstall everything from scratch, updating Ubuntu and at the same time migrate to Puma, that enables many features that the open source version of Phusion Passenger doesn't provide.

So, remember to backup your database and important files, hit that reinstall VPS button and follow this guide.

Log in as root, then update your system:

sudo apt update
sudo apt upgrade

Then add a new user that you will use to deploy your apps:

adduser deploy
gpasswd -a deploy sudo

It's then better to disallow root ssh login editing the ssh config file:

sudo nano /etc/ssh/sshd_config

Search for, and edit like so:

# Authentication:
PermitRootLogin no
sudo service ssh restart

Install nginx: on Ubuntu is as simple as:

sudo apt install nginx

Install rvm: go to the secure install page and follow the instruction. In my case, these worked out beautifully:

# Install mpapis public key (might need `gpg2` and or `sudo`)
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB

# Download the installer
\curl -O https://raw.githubusercontent.com/rvm/rvm/master/binscripts/rvm-installer
\curl -O https://raw.githubusercontent.com/rvm/rvm/master/binscripts/rvm-installer.asc

# Verify the installer signature (might need `gpg2`), and if it validates...
gpg --verify rvm-installer.asc &&

# Run the installer
bash rvm-installer stable

You could install rvm using apt, just remember this will not install it in users folder: this means that in your Capistrano deploy you need to add something like:

set :rvm_type, :system

If you use node and npm, it's better to use a version manager like nvm.

Follow it's instruction for installation, but it's basically a one-liner:

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash

Then install your preferred ruby (and bundler) and node binaries.

In my case:

nvm install 8.9.3
rvm install 2.4.2
gem install bundle

Now, we need PostgreSQL:

sudo apt-get install postgresql postgresql-contrib libpq-dev

Then create it's user:

sudo -u postgres createuser -s your_pg_user
sudo -u postgres psql
postgres=# \password your_pg_user
postgres=# \q

Create the required directories for your apps.

On you deploy user home folder, you can:

mkdir apps
mkdir apps/your_rails_app
mkdir apps/your_rails_app/shared
mkdir apps/your_rails_app/shared/config
touch apps/your_rails_app/shared/config/database.yml

And you want to paste in something like this:

production:
  database: your_rails_app_production
  adapter: postgresql
  username: your_pg_user
  password: your_pg_user_password
  host: localhost
  encoding: unicode
  port: 5432
  pool: 5

This is also the right time to place inside the shared folder every other file you may need to keep between releases.

Now, head into your rails app, and make sure you are using these gems:

gem 'capistrano', '~> 3.10.0'
gem 'capistrano-rails', '~> 1.1.6'
gem 'capistrano3-puma', '~> 3.1.1'
gem 'capistrano-rvm'
gem 'capistrano-nvm'

And set up as you need your deploy:

set :application, 'your_rails_app'
set :repo_url, 'git@github.com:you/your_rails_app.git'
set :deploy_to, '/home/deploy/apps/your_rails_app'

set :rvm_ruby_version, proc { `cat .ruby-version`.chomp }

set :linked_files, fetch(:linked_files, []).push('config/database.yml')

set :linked_dirs, fetch(:linked_dirs, []).push('public/system', 'tmp/pids', 'tmp/sockets', 'tmp/cache', 'log')

set :nvm_type, :user
set :nvm_node, proc { 'v' + `cat .nvmrc`.chomp }
set :nvm_map_bins, %w{node npm}

namespace :deploy do
  before :compile_assets, :clear_cache do
    on roles(:web), in: :groups, limit: 3, wait: 10 do
      within release_path do
        execute :npm, "install"
      end
    end
  end
end

A couple of things to note: we are keeping the node and ruby versions from the .nvmrc and .ruby-version files respectively: we just need to add the letter 'v' when reading from the file as the node versions folder are prepended by it.

Remember to add 'tmp/pids', 'tmp/sockets', 'log' as linked dirs, as required by puma (and also other dirs and files you may need).

I also added execution of the npm install command to install any dependencies there.

We are now getting closer and closer to the actual deploy: the gem capistrano3-puma provides useful rake tasks to upload many configurations:

bundle exec cap production puma:config

Will upload the puma server configuration on the server, by default in shared/puma.rb

bundle exec cap production puma:nginx_config

Is going to install your site within nginx. This won't probably be ok as is, so be ready to edit it by going into ssh and editing /etc/nginx/sites-enabled/your_rails_app_production.

Now, since we want to have multiple Rails apps running on the same server, we need a tool called Puma Jungle. These are basically init.d or upstart configuration files that will run Puma as service and handle multiple apps at once.

Once again, the puma capistrano gem will help us and (at least on Ubuntu), can upload the right code with the right search paths with a single command:

bundle exec cap production puma:jungle:setup

We just need to check the puma.conf file on the server, and also add the folder of your puma.rb file like so:

nano /etc/puma.conf
/home/deploy/apps/your_rails_app/current,deploy,/home/deploy/apps/your_rails_app/shared/puma.rb

Now, if everything was done correctly, on your local machine you can just:

bundle exec cap production deploy

And manage your jungle with:

bundle exec cap production puma:jungle:start
bundle exec cap production puma:jungle:stop
bundle exec cap production puma:jungle:restart
bundle exec cap production puma:jungle:status

Hope this was helpful!