Recently we upgraded several of our applications to a newer version of Ruby which was relatively simple to do in our local development machines. However, we ran into complications once we started deploying the updated applications to our development and productions servers. The problems that we ran into highlighted issues in the way we had configured our applications and Passenger on the servers. This blog post elaborates on the final configurations that we arrived to (at least for now) and explains the rationale for the settings that worked for us.
Our setup
We deploy our applications using a “system account” (e.g. appuser
) so that execution permissions and file ownership are not tied to the account of the developer doing the deployment.
We use Apache as our web server and Phusion Passenger as the application server to handle our Ruby applications.
And last but not least, we use Bundler to manage gems in our Ruby applications.
Application-level configuration
We perform all the steps to deploy a new version of our applications with the “system account” for the application (e.g. appuser
.)
Since sometimes we have more than one version of Ruby in our servers we use chruby
to switch between versions on the server when we are logged in as the appuser
. However, we have learned that is better not to select a particular version of Ruby as part this user’s bash profile. Executing ruby -v
as this user upon login will typical show the version that came with the operating system (e.g. “ruby 1.8.7”).
By leaving the system Ruby as the default we are forced to select the proper version of Ruby that we want on each application, this has the advantage that the configuration for each application is explicit on what version of Ruby it needs. This also makes applications less likely to break when we install a newer version of Ruby on the server. This is particularly useful in our development server where we have many Ruby applications running and each of them might be using a different version of Ruby.
If we want to do something for a particular application (say install gems or run a rake task) then we switch to the version of Ruby (via chruby
) that we need for the application before executing the required commands.
We have also found useful to configure Bundler to install application gems inside the application folder rather than in a global folder. We do this via Bundler --path
parameter. The only gem that we install globally (i.e. in GEM_HOME) is bundler
.
A typical deployment script looks more or less like this.
Login to the remote server:
$ ssh our-production-machine
Switch to our system account on the remote server (notice that it references the Ruby that came with the operating system):
$ su - appuser $ ruby -v # => ruby 1.8.7 (2013-06-27 patchlevel 374) [x86_64-linux] $ which ruby # => /usr/bin/ruby
Activate the version of Ruby that we want for this app (notice that it references the Ruby that we installed):
$ source /opt/local/chruby/share/chruby/chruby.sh $ chruby ruby-2.3.6 $ ruby -v # => ruby 2.3.6p384 (2017-12-14 revision 61254) [x86_64-linux] $ which ruby # => ~/rubies/ruby-2.3.6/bin/ruby $ env | grep GEM # => GEM_HOME=/opt/local/.gem/ruby/2.3.6 # => GEM_ROOT=/opt/local/rubies/ruby-2.3.6/lib/ruby/gems/2.3.0 # => GEM_PATH=/opt/local/.gem/ruby/2.3.6:/opt/local/rubies/ruby-2.3.6/lib/ruby/gems/2.3.0
Install bundler (this is only needed the first time, notice how it is installed in GEM_HOME):
$ gem install bundler $ gem list bundler -d # => Installed at: /opt/local/.gem/ruby/2.3.6
Install the rest of the app, its gems, and execute some rake tasks (notice that Bundler will indicate that gems are being installed locally to ./vendor/bundle
):
$ cd /path/to/appOne $ git pull $ RAILS_ENV=production bundle install --path vendor/bundle # => Bundled gems are installed into `./vendor/bundle` $ RAILS_ENV=production bundle exec rake assets:precompile
Passenger configuration
Our default passenger configuration is rather bare-bones and indicates only a few settings. For example our /etc/httpd/conf.d/passenger.conf
looks more or less like this:
LoadModule passenger_module /opt/local/.gem/gems/passenger-5.1.12/buildout/apache2/mod_passenger.so <IfModule mod_passenger.c> PassengerRoot /opt/local/.gem/gems/passenger-5.1.12 PassengerUser appuser PassengerStartTimeout 300 </IfModule> Include /path/to/appOne/http/project_passenger.conf Include /path/to/appTwo/http/project_passenger.conf
Notice that there are no specific Ruby settings indicated above. The Ruby specific settings are indicated on the individual project_passenger.conf
files for each application.
If we look at the passenger config for one of the apps (say /path/to/appOne/http/project_passenger.conf
) it would look more or less like this:
<Location /appOne> PassengerBaseURI /appOne PassengerRuby /opt/local/rubies/ruby-2.3.6/bin/ruby PassengerAppRoot /path/to/appOne/ SetEnv GEM_PATH /opt/local/.gem/ruby/2.3.6/ </Location>
Notice that this configuration indicates both the path to the Ruby version that we want for this application (PassengerRuby
) and also where to find (global) gems for this application (GEM_PATH
).
The value for PassengerRuby
matches the path that which ruby
returned above (/opt/local/rubies/ruby-2.3.6/bin/ruby
) and clearly indicates that we are using version 2.3.6 for this application.
The GEM_PATH
settings is very important since this is what allows Passenger to find bundler
when loading our application. Not setting this value results in the application not loading and Apache logging the following error:
Could not spawn process for application /path/to/AppOne: An error occurred while starting up the preloader. Error ID: dd0dcbd4 Error details saved to: /tmp/passenger-error-3OKItz.html Message from application: cannot load such file -- bundler/setup (LoadError) /opt/local/rubies/ruby-2.3.6/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require' /opt/local/rubies/ruby-2.3.6/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require'
Notice that we set the GEM_PATH
value to the path returned by gem list bundler -d
above. This is a bit tricky since if you are looking closely we are setting GEM_PATH
to the value that GEM_HOME
reported above (/opt/local/.gem/ruby/2.3.6/
). I suspect we could have set GEM_PATH
to /opt/local/.gem/ruby/2.3.6:/opt/local/rubies/ruby-2.3.6/lib/ruby/gems/2.3.0
to match the GEM_PATH
above but we didn’t try that.
UPDATE: The folks at Phusion recommend setting GEM_HOME as well (even if Passenger does not need it) because some gems might need it.
— Hector Correa & Joe Mancino