How to Deploy a Ruby on Rails App

… or any Rack app, really

There are quite a few options available to deploy Ruby on Rails applications on various platforms. Be it a given datacenter supports nothing else or because we depend on one of its auth modules, Apache often is the first choice. Read on for the painless way to get this done.

Of course, we are happy to do the work for you and host your application on any infrastructure. Don't hesitate to contact us for a quick chat about your project.

All Ruby on Rails applications (framework version 3 and higher) are also Rack applications. Rack applications adhere to a specific interface when connecting to the web server and this allows the web server to be swapped as long as it can handle Rack applications. A few examples are Puma, WEBrick or Unicorn. Thus, all steps below apply to all Rack applications alike.

How do we recognize a Rack app? Its pretty easy: If there is a file called config.ru in the application’s root directory, its almost certainly a Rack app. A convention is to serve css, js, images, fonts etc. from a public directory. Also, most Ruby (and hence Rack) applications nowadays come with a Gemfile.

What you will need

  • A running Debian 11 (bullseye) server (other distributions should work equally well as long as they bring a recent Ruby version and systemd). You can use a spare computer and install Debian or install it in a VirtualBox. Also, cloud providers like Amazon, Hetzner or Vultr provide virtual machines with Debian pre-installed.
  • Some experience with editing files and running basic commands on a terminal, mainly to change configuration files and to restart services

The Example app

For this guide, we’ll work with the (very simple and not necessarily useful) clock app. Its directory structure looks essentially like this:

├── config.ru
└── public
    ├── index.html
    └── style.css

It doesn’t do much: The Rack app itself spits out the current time in a JSON response and the static index.html contains some JavaScript to periodically fetch the current time from the Rack app and insert it into the page.

Make sure the clock app is checked out at /var/apps/clock and ensure a suitable user and group. As root, do:

apt-get install git
useradd -m app
git clone https://github.com/wendig-ou/clock.git /var/apps/clock
chown -R app. /var/apps/clock

Overview

There are several ways to get this app running:

  • with rackup (comes with Rack, excellent for development workstations or automated tests, this uses WEBrick by default)
  • use a specific Rack web server such as Puma, Thin or Unicorn (a good choice for production when you want a lot of control)
  • to manage your application servers, use systemd
  • additionally use a web server such as Apache or nginx (these can serve the public directory and reverse proxy other requests to your app. They give access to a vast amount of modules for authentication, ssl, redirection, load balancing etc.)
  • use Phusion’s Passenger (good for zero-config production deployments, provides a Apache/nginx module to manage your Ruby web processes and also comes with a Rack server included)

Of course, there are many more things you could do: For example, you might want to include HAProxy or a caching server in the mix. For now, we will focus on the options mentioned above, to get you started and to cover the most common scenarios.

1 • Rackup

This is very simple and just requires that Ruby and the gem rack are installed. This is an excellent option for development and quick tests. As root, do:

apt-get install ruby ruby-rack

Now run the app as the app user (how?):

cd /var/apps/clock
rackup

# use e.g. 'rackup -o 0.0.0.0 -p 8080' to change host and port

This will make the app available at http://localhost:9292. Use ctrl-c to stop the process.

2 • Puma, Thin or Unicorn

This is also very straight forward. You need to install the respective gem (as root):

apt-get install ruby ruby-rack puma

… and then start the process with the app user, e.g.:

cd /var/apps/clock
puma

# use e.g. 'puma -b 0.0.0.0 -p 8080' to change host and port

This will make the app available at http://localhost:9292. Use ctrl-c to stop the process.

You might want to run this on a server. See the section about systemd to learn how to manage this process there.

3 • Systemd

When deploying your app with Puma, Rack, Thin or WEBrick, you will probably want to make this service run in the background and restart on boot or after crashes. Comes in systemd:

For configuration, we’ll create a file /etc/systemd/system/clock.service as root with the following content:

[Unit]
Description=a clock app

[Service]
User=app
Group=app
WorkingDirectory=/var/apps/clock
ExecStart=/usr/bin/rackup -o 0.0.0.0 -p 9292
Restart=on-failure

[Install]
WantedBy=multi-user.target

Then, we need to reload systemd, enable the unit (makes it start/restart at boot) and start it (starts it now). As root, do:

systemctl daemon-reload
systemctl enable clock.service
systemctl start clock.service

This will also make the app available at http://localhost:9292, but now systemd will take care that the app is restarted when it dies or started after a reboot.

4 • Add Apache

Until now, we used a Ruby process to respond to all incoming web request. That works and might be enough in many situations. It is usually more efficient, to deliver static files with a web server that is optimized for that task and only forward dynamic requests to your app. Also, some apps might simply not handle static files. In this case, you would probably just see an unstyled page without any JavaScript, fonts, icons etc.

We will configure Apache to reverse proxy requests to the Rack application which we started on port 9292 earlier, but only the requests that can’t be served with files from within the public directory. But first, let’s install Apache including some modules as root:

apt-get install apache2
a2enmod rewrite proxy proxy_http
systemctl restart apache2

Then, we create a config file in /etc/apache2/sites-available/001-clock.conf for this page with the following content:

<VirtualHost *:80>
  ServerName http://localhost
  DocumentRoot /var/apps/clock/public

  # allow the location to be accessed at all, this needs to be done because our
  # DocumentRoot isn't in /var/www
  <Location />
    Require all granted
  </Location>

  # serve requests with files from the public subdirectory if the requested url
  # refers to an existing file
  RewriteEngine on
  RewriteRule ^/?$ /index.html
  RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
  RewriteRule ^/(.*)$ http://127.0.0.1:9292%{REQUEST_URI} [P,QSA,L]
</VirtualHost>

Now, we need to activate this configuration as root (there is probably a default site on your server that you can deactivate):

a2ensite 001-clock
a2dissite 000-default
systemctl restart apache2

And the app should be available at http://localhost.

5 • Phusion Passenger

The guys at Phusion have developed an Apache module which takes care of managing the processes and the reverse proxying. So here, you don’t need to set up a systemd unit. To use it, install it as root:

apt-get install libapache2-mod-passenger

Now, like before, configure Apache with a config file in /etc/apache2/sites-available/001-clock.conf, but this time with the following content:

<VirtualHost *:80>
  ServerName http://localhost
  DocumentRoot /var/apps/clock/public

  <Location />
    Require all granted
  </Location>
</VirtualHost>

Then activate it as before:

a2ensite 001-clock
a2dissite 000-default
systemctl restart apache2

And the app should be available at http://localhost

RubyGems and your OS

Many OS package managers provide Ruby which includes RubyGems. If you decide to use this Ruby installation, I would also recommend that you use the OS package manager to install the gems (as we did above). In some cases, this isn’t possible because you require specific gem versions. But using gem install directly can easily create conflicts with the gems installed by your OS package manager. In such a situation, you can:

  • Use the OS Ruby to install your gems with a specific directory, for example, with bundle install --path /var/apps/clock/bundle. This requires that your gems (including puma, webrick etc.) are mentioned in the Gemfile.
  • Install a separate Ruby for your deployment. You can make use of rbenv or RVM to accomplish this. In that way, the installed gems for those Rubies are separate from the system gems. I recommend this, but, you have to configure passenger or your systemd services to use your dedicated Ruby installation.

Change User

Assuming you are logged in to your server as root, you can change the user for the current session (think impersonate) with su app. After you are done executing commands as this user, type exit or hit ctrl-d.