Multi-staging environment for Rails using Capistrano and mod_rails

I've finally gotten around to checking out mod_rails. As it turns out, it was every bit as simple and impressing as I had suspected. Here's how I set up a multi-staging environment using mod_rails and Capistrano.

Phusion passenger (aka mod_rails)

Setting up passenger is dead simple, as explained on Phusion's installation page:

sudo gem install passenger
sudo passenger-install-apache2-module

If you're on Ubuntu 8.04+ or Debian, you can also install passenger through apt, using Brightbox' deb package.

The passenger-install-apache2-module wizard takes you through installing passenger. It does not, however, give you everything you need in order for Apache to load it. For this you'll have to create two files:

echo "PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-2.0.6
PassengerRuby /usr/bin/ruby1.8" > /etc/apache2/mods-available/mod_rails.conf

echo "LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-2.0.6/ext/apache2/mod_passenger.so" > /etc/apache2/mods-available/mod_rails.load

Make sure you adjust the paths to your system and gem versions. Now we need to make them available at run time:

sudo ln -s /etc/apache2/mods-available/mod_rails.* /etc/apache2/mods-enabled/.
sudo /etc/init.d/apache2 restart

Now you should be good to go. Let's deploy a site to verify that passenger is in fact up and running.

Deploying

Deploying is easy:

<VirtualHost *>
    DocumentRoot /var/www/mysite.com/app/public
    ServerName mysite.com
</VirtualHost>

Really, that's all that's needed. Whenever you need to restart your application you can just touch /var/www/mysite.com/app/tmp/restart.txt and passenger reloads it for you.

Using Capistrano with passenger

Using Capistrano with passenger requires the deploy:start, deploy:stop and deploy:restart tasks to be redefined. Start and stop makes no sense with passenger, a restart is the only action we can actually perform:

namespace :deploy do
  desc "Restarting mod_rails with restart.txt"
  task :restart, :roles => :app, :except => { :no_release => true } do
    run "touch #{current_path}/tmp/restart.txt"
  end

  [:start, :stop].each do |t|
    desc "#{t} task is a no-op with mod_rails"
    task t, :roles => :app do ; end
  end
end

Tasks lifted from Capistrano Deply with Git and Passenger.

Multi-staging

Deploying on passenger makes it easy to install several applications, or several instances of the same application. I find it very helpful to multi-stage my application. In the simplest case this means the following places:

The pre-production environment is where your client and other non-developer team-mates can access the application in its current stable form. This environment should act like the production environment to emulate as close as possible the final version, but give you a secluded area to verify that it's in fact ready for primetime.

If you are to run tests on this environment, you may want to use another database, so as to not corrupt your live data. You may also want the application to display some information about itself, like revision number, deploy date, data which may help testers give you a more detailed report when something fails. Thus, you cannot actually use production mode, even if you'd like a production-like environment. The solution is to create a new mode for your Rails application, let's call it staging.

Setting up a new Rails environment

Here's how you can add an environment to your rails application

  1. Add a new environment in conf/environments/
  2. Add a new database configuration (optional)

Since our new environment should match the production environment we can start by copying the production environment:

cp config/environments/production.rb config/environments/staging.rb

Let's say we'd like our staging environment to have full stack traces. Edit config/environments/staging.rb and change the following:

config.action_controller.consider_all_requests_local = false

to

config.action_controller.consider_all_requests_local = true

Now, let's edit config/database.yml. If you don't need a separate database for your staging environment, you can just add this:

staging:
  production

If you want a separate database for the staging area, set one up, just like the others.

Now you can verify that your new environment is working by firing up the console:

script/console staging

If this works, then you're good to go!

Staging and passenger

By default, passenger will run your application in production mode. You can easily change this by setting the RailsEnv directive in your virtual host configuration:

<VirtualHost *>
    DocumentRoot /var/www/mysite.com/app/public
    ServerName mysite.com
    RailsEnv staging
</VirtualHost>

Remember to restart Apache when you change the virtual host configuration.

Deploying to staging server with Capistrano

Capistrano still doesn't know about your multiple targets. As Jamis Buck explained in his blog some time ago, it's easy to fix. First:

sudo gem install capistrano-ext

Then, in your config/deploy.rb, add the following lines:

set :stages, %w(staging production)
set :default_stage, "production"
require 'capistrano/ext/multistage'

Now the most magical thing happens. Your capistrano configuration still works, and cap deploy still does the same thing. However, you can now also add deployment configuration for more targets. For our staging environment, simply add the file config/deploy/staging.rb. In this file you can override any settings from config/deploy.rb.

If your staging environment resides on the same box as your production environment, then all you need to add is another target and specify which rails environment to run rake tasks in:

set :deploy_to, "/var/www/#{application}/staging"
set :rails_env, "staging"

Now you can run cap staging deploy, and Capistrano will deploy your application to the staging server. If you add config/deploy/production.rb then cap production deploy will deploy your production site. Very nice!

In the long run, you'll probably deploy to your staging server more often than your production server. To fix this just change the default stage (second line):

set :default_stage, "staging"

In conclusion

So, in this article I showed you how to:

To round things off I though I'd show you some real life configuration files.

config/deploy.rb

set :stages, %w(staging production)
set :default_stage, "staging"
require 'capistrano/ext/multistage'

set :application, "myapp.com"

# Use Git source control
set :scm, :git
set :repository, "git@github.com:cjohansen/myapp.com.git"
# Deploy from master branch by default
set :branch, "master"
set :deploy_via, :remote_cache
set :scm_verbose, true

set :user, 'christian'
ssh_options[:forward_agent] = true
default_run_options[:pty] = true

role :app, "myapp.com"
role :web, "myapp.com"
role :db,  "myapp.com", :primary => true

namespace :deploy do
  desc "Restarting mod_rails with restart.txt"
  task :restart, :roles => :app, :except => { :no_release => true } do
    run "touch #{current_path}/tmp/restart.txt"
  end

  [:start, :stop].each do |t|
    desc "#{t} task is a no-op with mod_rails"
    task t, :roles => :app do ; end
  end
end

# Avoid keeping the database.yml configuration in git.
task :copy_database_yml, :roles => :app do
  db_config = "/var/www/#{application}/conf/database.yml"
  run "cp #{db_config} #{release_path}/config/database.yml"
end

config/deploy/staging.rb:

set :deploy_to, "/var/www/#{application}/staging"
set :rails_env, "staging"

config/deploy/production.rb:

set :deploy_to, "/var/www/#{application}/app"
# Deploy to production site only from stable branch
set :branch, "stable"

Virtual host configuration for staging server

<VirtualHost *>
    <Directory /var/www/myapp.com/app/current/public>
        Options FollowSymLinks
        AllowOverride None
        Order allow,deny
        Allow from All
    </Directory>

    DocumentRoot /var/www/myapp.com/staging/current/public
    ServerName dev.myapp.com

    RailsEnv staging

    ErrorLog /var/www/myapp.com/log/staging_error.log
    CustomLog /var/www/myapp.com/log/staging_access.log combined

    # Gzip/Deflate
    # http://fluxura.com/2006/5/19/apache-for-static-and-mongrel-for-rails-with-mod_deflate-and-capistrano-support
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/javascript text/css application/x-javascript
    BrowserMatch ^Mozilla/4 gzip-only-text/html
    BrowserMatch ^Mozilla/4\.0[678] no-gzip
    BrowserMatch \bMSIE !no-gzip !gzip-only-text/html

    # No Etags
    FileETag None

    RewriteEngine On

    # Check for maintenance file and redirect all requests
    RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
    RewriteCond %{SCRIPT_FILENAME} !maintenance.html
    RewriteRule ^.*$ /system/maintenance.html [L]

    # Static cache
    RewriteCond %{REQUEST_METHOD} !^POST$
    RewriteCond /var/www/mysite.com/staging/current/tmp/cache/static$1/index.html -f
    RewriteRule ^(.*)$ /var/www/mysite.com/staging/current/tmp/cache/static$1/index.html [L]

    <Location />
        AuthName "Staging area is password protected"
        AuthType Basic
        AuthUserFile /var/www/myapp.com/conf/passwords
        Require valid-user
    </Location>
</VirtualHost>

Virtual host configuration for production server

<VirtualHost *>
    <Directory /var/www/myapp.com/app/current/public>
        Options FollowSymLinks
        AllowOverride None
        Order allow,deny
        Allow from All
    </Directory>

    DocumentRoot /var/www/myapp.com/app/current/public
    ServerName myapp.com
    # Make sure dev.myapp.com vhost file is seen by Apache before this one
    ServerAlias *.myapp.com

    ErrorLog /var/www/myapp.com/log/error.log
    CustomLog /var/www/myapp.com/log/access.log combined

    # Gzip/Deflate
    # http://fluxura.com/2006/5/19/apache-for-static-and-mongrel-for-rails-with-mod_deflate-and-capistrano-support
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/javascript text/css application/x-javascript
    BrowserMatch ^Mozilla/4 gzip-only-text/html
    BrowserMatch ^Mozilla/4\.0[678] no-gzip
    BrowserMatch \bMSIE !no-gzip !gzip-only-text/html

    # Far future expires date
    <FilesMatch "\.(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)$">
        Header set Expires "Thu, 15 Apr 2010 20:00:00 GMT"
    </FilesMatch>

    # No Etags
    FileETag None

    RewriteEngine On

    # Rewrite from www.myapp.com to myapp.com
    RewriteCond %{HTTP_HOST} ^w+\.myapp\.com
    RewriteRule /?(.*) http://myapp.com/$1 [R=301,L]

    # Check for maintenance file and redirect all requests
    RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
    RewriteCond %{SCRIPT_FILENAME} !maintenance.html
    RewriteRule ^.*$ /system/maintenance.html [L]

    # Static cache
    RewriteCond %{REQUEST_METHOD} !^POST$
    RewriteCond /var/www/mysite.com/app/current/tmp/cache/static$1/index.html -f
    RewriteRule ^(.*)$ /var/www/mysite.com/app/current/tmp/cache/static$1/index.html [L]
</VirtualHost>

Published 26. January 2009 in rails, ruby, apache og capistrano.