Deploy a Rails application in production with Capistrano and Phusion Passenger

  • apache
  • capistrano
  • rails

Publié le 2022-09-16

Intéressé pour créer un API avec Ruby on Rails? Jette un coup d'œil à mon livre: API on Rails 6. Tu peux télécharger une version gratuite au format PDF sur Github. Si tu aimes mon travail tu peux acheter un version payante sur Leanpub.

Deploy a Rails application in production with Capistrano and Phusion Passenger

Recently, I deployed a new brand new Rails application in my small VPS: https://killer.rsseau.fr . Doing so is alway a complex exercice because it requires to take care of many things. In this post, I will retrace everything I did.

In my case, I have the following stack:

If you have a slightly different stack, the workflow could be different but the global logic should be very similar.

As a deployment automation, I'll use Capistrano. Capistrano will do many thing under the hood for us like:

  1. makes an SSH connection to the server
  2. clone the last version of your Git repository on the server
  3. run bundle install to get new packages, run rails db:migrate for you, build assets, etc..
  4. create some symlink for shared item (ex: production.sqlite, master.key, uploaded files, etc..)

It requires some work to setup properly but, trust me, it worth it. Once setup, one command is sufficient to make a release on production.

NOTE: This guide is a note for myself, I may improve it in the future.

Setup the server

Setup /var/www

We'll start to create a folder for you project inside /var/www. This folder contains all yours websites sources for Apache.

sudo mkdir /var/www/killer

As Capistrano and Apache need access to this folder, we also need to make it writeable by your current user. So we'll change the owner of the folder to debian (my user):

sudo chown debian /var/www/killer

Then we'll create the shared folder. Capistrano will use this folder and symlink the content for every release. I'll also create shared/config and shared/config/credentials. We'll use those folder right after:

mkdir -p /var/www/killer/shared/
mkdir -p /var/www/killer/shared/config/credentials
mkdir -p /var/www/killer/shared/db

In my situation, I want to put the master.key, production.key (who are not committed on the repository). I'll do do using scp

# on local env
scp config/master.key user@server:/var/www/killer/shared/config/master.key
scp config/credentials/production.key user@server:/var/www/killer/shared/config/credentials/production.key
scp db/production.sqlite user@server:/var/www/killer/shared/db/production.sqlite

I also need to create an empty SQlite database. This database will be shared to every release. Let's do it:

sqlite3 /var/www/killer/shared/db/production.sqlite
sqlite> SELECT 1;
sqlite> .exit;

NOTE: we need to execute at least one query to create the file. That's why I did SELECT 1;.

Install Ruby

Now we need to install the same Ruby version than your Ruby on Rails project on the server. The easiest way to do so is use a Ruby version manager.

In Ruby world, there is two version manager for Ruby: Rbenv & RVM. I will use the last one.

So, if you didn't install RVM, just click on this link and follow steps. Once you did it, install the right version using:

rvm install 3.1.2

And we are good for now.

Setup Capistrano

Let the server appart for now and go back on your project.

We'll install Capistrano and some necessary plugins (those may not be necessary in you case):

bundle add capistrano --group development
bundle add capistrano-rvm --group development

Now we can install Capistrano on your project with this command:

cap install

At this point, you'll see some new files in your project.

Capfile

The first file is Capfile, this file is responsible to load plugins of Capistrano. You just need to add some requires

# Capfile
# ...
require "capistrano/deploy"
require 'capistrano/rails'
require "capistrano/bundler"
require "capistrano/rvm"

deploy.rb

Now we want to edit a bit the config/deploy.rb. This file contains configuration about all deployments.

We'll start to add basics informations about out project

# config/deploy.rb
# ...
# Global information about your repo
set :application, "killer-game"
set :repo_url, "git@github.com:madeindjs/killer-game.git"
set :branch, :main
# where to deploy and RVM conf
set :deploy_to, "/var/www/killer"
set :rvm_ruby_version, '3.1.2'

The last "complex" part is to list every linked files. Be aware because those files need to exist in the server share folder (Hopefully, we created them in the previous section).

# config/deploy.rb
# ...
# every shared file we need
append :linked_files, 'config/master.key', 'config/credentials/production.key', 'db/production.sqlite'

production.rb

Then last thing is to update config/deploy/production.rb. This file will contains informations about you production server. So we need to update server directive:

server "vps-1bf61d44.vps.ovh.net", user: "debian", roles: %w{app db web}

My case is really simple, I don't use an SSH key or any specific role for this server. But if you want to, Capistrano have much more settings.

Try it out

At this point, everything is ready.

We need to push all changes to the repository before going further because Capistrano will clone the repository and run the configuration from there.

git add .
git commit -m "Setup Capistrano"
git push origin

Once you did it, you'll be able to deploy the project:

cap production deploy

At this point, you should see some changes on the server:

  • a folder in /var/www/killer/releases/$timestamp, containing the Git repository with shared files as symlinks
  • a symlink in /var/www/killer/current who points to /var/www/killer/releases/$timestamp

Setup web server

Let's go back again on the server side to configure the Web server properly.

Install Phusion Passenger

As I said, Passenger is one way to run Ruby code on Apache. There is other way but I do like Passenger because it's an Open Source software.

Also, Passenger have a really great documentation. To Install it, you just need to follow this guide: https://www.phusionpassenger.com/library/install/apache/install/oss/ .

NOTE: I prefer to avoid copy/paste the tutorial from Passenger because he may change.

Setup Apache HTTP server

Now you setup Passenger, the hardest part remains: setup Apache. Hopefully, I have a template for you.

So create a new file killer.rsseau.fr.conf in /etc/apache2/sites-available/

sudo vim /etc/apache2/sites-available/killer.rsseau.fr.conf

... and paste the following content:

ServerName DefaultServer
DocumentRoot /var/www
PassengerDefaultRuby /home/debian/.rvm/wrappers/ruby-3.1.2/ruby

<VirtualHost *:80>
  # RAILS CONF

  # tell to Passenger which Ruby binary he need to use
  PassengerRuby /home/debian/.rvm/wrappers/ruby-3.1.2/ruby
  # setup rails environment
  RailsEnv production

  # APACHE CONF

  # basic configuration of your webserver
  ServerName killer.rsseau.fr
  ServerAlias www.killer.rsseau.fr
  ServerAdmin alexandre@rsseau.fr
  # setup HTTP2 (not mandatory but better)
  Protocols h2 http/1.1
  # setup log, one file per day (not mandatory but better)
  ErrorLog ${APACHE_LOG_DIR}/killer_fr_error.log
  CustomLog "|/usr/bin/rotatelogs -f /var/log/apache2/killer.access.%Y-%m-%d.log 86400" combined

  # FOLDER CONF

  # serve you current version of the project
  DocumentRoot /var/www/killer/current/public/
  <Directory "/var/www/killer/current">
    Options FollowSymLinks
    Allow From All
    Require all granted
    Options -Indexes
  </Directory>

  # use apache to serve static files to improve perf
  Alias / "/var/www/killer/current/public/"
  <Directory "/var/www/killer/current/public/">
          Options FollowSymLinks
  </Directory>
</VirtualHost>

After that, you can activate your configuration using a2ensite

sudo a2ensite killer.rsseau.fr.conf

And finally restart Apache to load the new configuration

sudo /etc/init.d/apache2 restart

To test your new application, you can try to navigate on your website using your browser or using cURL like an hacker:

curl http://killer.rsseau.fr

Voilà!

Troubleshooting

At this point, shit may happens. So you need to to watch log to see your error. Log may appears on many files:

  • /var/log/apache2/error.log
  • /var/log/apache2/killer_fr_error.log
  • /var/www/killer/current/log/production.log

If recommend to run this snippet to watch all them at once:

tail -f /var/log/apache2/error.log /var/log/apache2/killer_fr_error.log /var/www/killer/current/log/production.log

Conclusion

Nowadays, deploy a Rails application is "easy".

Lot's of developer use a tool like Heroku to quickly deploy a Rails application. There is nothing bad with that. But I don't want to depend on an external service for my application. Also, when you know how to deploy a Rails application yourself, it can be quicker than do it with Heroku. Do it yourself is a good way to improve you DevOps skills and save money. I host many websites on 3€ per month VPS server where Heroku start plan cost $7 per month (and grow fast).

But don't get me wrong, Heroku solutions have some pros. It provides a quick way to make your application scalable: you just have to pay more. Also, if you host your application yourself, you'll need to monitor logs of your web server, backup the database, upgrade Apache HTTP server regularly... Heroku can handle that for you.

It's up to you to choose the right answer.

You can go further in self hosting with these topics:

  1. setup HTTPS using Certbot
  2. use an another database server (even if sqlite may be sufficient)
  3. do a backup using a CRONtab

Articles liés