Configuring our Rails app

7 minute read

One of the central philosophies of Ruby on Rails is convention above configuration which means that Rails does not demand you to (and in some cases allow you to) add a whole lot of configuration according to your needs. Instead, it comes with great defaults baked in which most people would find useful. The framework is made of a number of gems that are so tied up together that you do not have to fiddle around trying to figure out how to configure what.

However, there are still certain areas where you need to configure a Rails app before pushing it into production. Let’s take up a few of those considerations.

Environments and Secrets

It is part of common wisdom (and one of the factors of a 12-factor app) that you should keep the parts of your configuration which vary between production, development, test and other deploys in environment variables. Here are a few areas where you can use them.

Database username and password

If your app stores data in a database (and it most probably does), then you are connecting your app with the database using a pair of username and password. If your server gets hacked (no one likes getting hacked and we all take precautions but there is always a chance with continuous vulnerability disclosures), then the attacker would not be able to figure those out by reading config files. For the same reason, you keep the database credentials in environment. It’s not uncommon to come across database.yml which look more or less like:

default: &default
  adapter: <%= ENV.fetch("DB_ADAPTER_NAME") %>
  encoding: unicode
  database: <%= ENV.fetch("DB_DBNAME") %>
  username: <%= ENV.fetch("DB_USERNAME") %>
  password: <%= ENV.fetch("DB_PASSWORD") %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: '127.0.0.1'

development:
  <<: *default

test:
  <<: *default

production:
  <<: *default

With such a configuration, all we need to do is to make sure that when starting up Rails, those environment variables are populated before the Rails engine boots.

There are mainly two ways of doing it:

  1. Supply the environment variables from the CLI before running rails. If you have ever seen someone typing or recommending something like: RAILS_ENV=production rails server then that’s how you declare environment variables in a line and boot rails up without letting the variables leak anywhere.
  2. Keep the environment variables in a file and make Rails read the values during boot procedure. One of the most popular ways to doing this is to create a YAML file with all the values and add a piece of code in application.rb to make Rails read the file. Since loading application.rb is part of the boot procedure, the environment variables are loaded.

A great way is to mix them both: you can run something like: ENV_YML=/secrets/env.yml rails server where /secrets/env.yml contains all the variable values and the location of this file is fetched when booting rails (in application.rb) from an environment variable. This keeps everything secure and deployment manageable without having to alter the deployment script itself.

Running multiple Rails apps on one machine

You might have a need (for the sake of simplicity or saving money) to run multiple Rails apps on the same machine. While you can, for sure configure them to use different databases, there are two other things that you need to consider:

  1. You need to make sure that you are running the application server for your app on different ports. In most cases, it is written in the config file of the app server (in case of puma, the file is located at config/puma.rb within your Rails app).
  2. Your nginx server is configured properly to send the requests at the correct ports to avoid mixups.
  3. Your apps do not share a database (ok, if you are an expert at doing something like that, it might not be a valid thing for you).

Configuring puma (or whatever application server you are using)

Most application servers (like Puma, Unicorn, WEBrick etc.) are configurable around a number of parameters. Since we are using Puma as our application server, we are going to talk about Puma. However, these points apply to other application servers as well. It is important to understand some of these parameters and their effects, which should tell you how to configure these and how important they are

Which IPs are being listened to?

You want your application to be available to everyone (unless its some kind of intranet application) and you might think that for that to happen you need to configure puma to listen for connections coming from any IP address. This is not true.

When you are developing your application, you would be connecting to the app from your local machine (127.0.0.1) and when you have deployed your app, puma will receive incoming connections forwarded from nginx which basically means anyone connecting to your app is being proxied via localhost (127.0.0.1) via nginx.

To put things in perspective, you only need to make puma listen on 127.0.0.1. When you set puma to listen only for connections coming from localhost, you circumvent the need to block port 3000 in your firewall (you should still do that by all means). Anyone from the outside world would not be able to get a response from your Rails app unless they are hitting the nginx endpoint.

Which port to listen on?

Port 3000 is the default port on which Puma listens. However, this can be changed. There are primarily two reasons why you might want to change that:

  1. When you do not want anyone to be able to guess which port your application server is running on.
  2. When you have something else running on port 3000. This might happen when you have another Rails (or NodeJS) app running on port 3000 already.

Where is the PID file?

One of the final steps of deploying a Rails app is to restart puma. Puma cannot restart successfully unless you kill the old instance of puma because it listens on the specified port. Unless the old instance is dead, the new instance will fail to launch.

Puma comes with a command known as pumactl which can restart puma given the PID (Process ID) of the running instance. pumactl can be given a configuration file where you can specify configuration parameters to be supplied to puma. One of those parameters is a filename called pidfile into which it will write the PID of the puma process that is launched. When restarting puma, we can specify the pidfile to pumactl and it will automatically kill the old instance against the PID written in that file and launch a new instance of puma and write the new PID in the pidfile.

The right environment!

By default the puma configuration file in your Rails app tries to read an environment variable called RAILS_ENV. If the value is set, it will use the environment set in the variable, if not, it will use the development environment. A deployed Rails application however would typically need to run the production environment.

Other configuration variables

There are a number of other variables which you can set with puma such as number of threads to run, clustering etc. There is a good chance that your Rails app will work just fine without having to change most of those parameters. However, if you have a large server (say 16 cores and 64+ GB of RAM), then you might need to expand the horizons and configure puma to utilize the hardware a bit more.

Configuration files for this guide

We talked about an environment file for storing the environment variables which can be read during Rails boot process. We also talked about various configuration directives which can be used with puma.

We are going to use both.

The environment file

The environment file will be called local_env.yml which will be copied from the shared/config directory to the current release directory on each deployment. Whenever you want to change the environment variable during your app’s bootstrap process, you would need to edit this file by hand on the server as well.

Given our earlier example, we can set the contents of this file to:

DB_ADAPTER_NAME: 'postgresql'
DB_DBNAME: 'abcd'
DB_USERNAME: 'jklm'
DB_PASSWORD: 'xyza'
RAILS_MAX_THREADS: 4

This is just an example for the file. Please make sure that you are writing all the required values in the file.

NOTE: For this file to work, you would have to place the file at config/local_env.yml and add the following code in your application.rb file:

config.before_configuration do
  env_file = File.join(Rails.root, 'config', 'local_env.yml')
  if File.exist?(env_file)
    YAML.load(File.open(env_file)).each do |key, value|
      ENV[key.to_s] = value
    end
  end
end

The puma configuration file

We will call the puma configuration file as puma_production_config.rb. This file too will reside in the shared/config directory. However, since we do not plan to change the app restart every now and then, we will let this file be where it is. The contents of this file look like this:

#!/usr/bin/env puma

environment 'production'
daemonize
pidfile '/home/ubuntu/mysite/pids/puma.pid'
bind 'tcp://127.0.0.1:3000'

In this configuration file, we are asking puma to use the production environment with the pidfile located at /home/ubuntu/mysite/pids/puma.pid (it’s created by the deployment script if it is not present), bind it to port 3000 and listen for connections from 127.0.0.1 only (which is localhost; we expect to receive connections from nginx alone, as described earlier) and daemonize the process so that puma starts running in the background and the script can continue to execute and end (very useful for calling the deployment process remotely via SSH).

Updated:

Leave a comment