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:
- Developer boxes - usually running in development mode
- Pre-production environment (on the actual production server, or a box with the same exact setup) - for internal review
- Production environment - live application
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
- Add a new environment in
conf/environments/ - 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:
- Install and configure Phusion passenger
- Configure Capistrano to work with passenger
- Create a new environment for your Rails application, and configure passenger to use this
- Configure Capistrano to deploy to several targets
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>
Comments
Christian
26. January, 21:49
John
29. January, 07:12
Denis Hennessy
(http://hennessynet.com)
30. January, 15:47
Ed Spencer
(http://edspencer.net)
30. January, 16:54
I like to use something like this in deploy.rb:
set(:deploy_to) {"/var/web/rails/#{application}/#{stage}"}
Which picks up the deploy directory automatically. The lambda means you can define the stage after this line and it'll still work.
Andy Stewart
(http://airbladesoftware.com)
30. January, 17:25
Sam Figueroa
(http://samuelfigueroa.com)
30. January, 19:21
Christian
31. January, 01:00
@Ed: great tip, I wasn't aware of set accepting a block. Great stuff.
Chris
31. January, 05:14
jeroen
31. January, 11:37
but what about assets referenced from css files? Those won't expire even after a new version has been deployed. Right?
Christian
31. January, 13:08
Juicer is a command line tool for combining and minifying CSS and JavaScript - it also brings the cache buster thing inside CSS files. Once 0.2 is out (in a week or so) I'll start working on a Rails plugin using it that will do these things live (with caching of course) instead of on the command line.
Michael Buckbee
(http://www.buzzwordcompliant.net)
31. January, 18:20
Does that negatively effect application performance?
Thanks,
Mike
Christian
2. February, 09:11
http://developer.yahoo.com/performance/rules.html#etags
jeroen
(http://lbi.lostboys.nl)
2. February, 10:48
One very simple thing you can do with assets referenced from css files is put them in a seperate folder and exclude that folder from far future exipery dates.
I was hoping you could do something with the referer. Like if referer.end_width?('css') do not use far future expiry date. But I don't think that possible in Apache.
Osh
(http://myownpirateradio.com)
6. February, 11:30
Here's an addition to deploy/staging.rb I found essential:
set :rails_env, "staging"
This forces migrations to run in the correct environment. On my setup, without this line, cap deploy:migrations affects the production database.
Christian
8. February, 19:04
Millisami
(http://millisami.info)
26. June, 14:25
Have you added the CSS cache buster feature and the rails plugin? coz I'm curious to use this for caching.
Comments are closed