Running Rails 2.3 using CGI with Rack

One of the new features of Rails 2.3 is full Rack support, and the removal of all the CGI-specific code that has been with us since the beginning of time. While this is generally a Good Thing, because Rack is a cleaner way to support different ways of deploying and running your Rails app, one unfortunate side-effect of this is that there's no longer a dispatch.cgi in your shiny new Rails apps which allow you to run your app in CGI mode.

This article discusses why you might want to run your Rails app as a CGI (and why, in general, you really, really don't), a little bit about how Rack works, and finally how you can get your dispatch.cgi back.

Why CGI is teh suck

In short, CGI isn't a good fit for running a Rails application because Rails takes quite some time to start up. This isn't a problem when you're running it in an app server, since this startup only happens once for a huge number of requests, and you can start new instances in the background without slowing down your users. However, a CGI starts up for every single request, so you're chewing a second (or sometimes more) for every request. On even a moderately busy site, you can end up with a lot of concurrent Rails CGI processes starting up and running, which puts a high load on your webserver and generally sucks.

This problem isn't unique to Rails, either -- any web application with significant startup time will suffer similarly with CGI. It affects Python, Perl, and other Ruby web frameworks similarly. Even PHP applications (yes, even those running in mod_php) suffer from it to some extent, which is the reason why PHP bytecode caches are so popular. The worst sufferers are Java frameworks, where startup times there can be up to a minute(!), which is why they usually are only available as an appserver, and CGI is just Not Spoken Of.

Why CGI is still useful

Given that a Rails app will run very slowly and consume more CPU for every request, why would we ever want to run it as a CGI? To answer that, consider what you're trading when you run an appserver: memory. Every appserver permanently consumes a chunk of memory, which is basically unavailable for any other process to use. If each of those applications is only going to be accessed infrequently, and page load times aren't significant (the apps are demos, or for client testing, or are otherwise really low volume production apps), then the cost in memory of having an appserver running for each one all the time might not be worth it, and a CGI would be more effective.

Nice Rack, how does it work?

So, what is this Rack thing that everyone's talking about? To put it simply, it's a glue layer to let web servers and Ruby-based web applications easily talk to each other. A web server simply needs a way to talk to the Rack dispatcher (called a Handler), and it can suddenly run any Rack compliant web application. Conversely, all a web application framework needs to do is provide a way for Rack to provide request data to the application (called an Adapter), and any Rack compliant webserver can run that framework.

If this sounds an awful lot like CGI, FastCGI, SCGI, WSGI, and any number of other existing ways of making a web server talk to a web application, that's probably because it is. If you're wondering why someone came up with yet another way to make a web server talk to a web application, you won't find enlightenment here. I have no idea, either.

The Meat and Three Veg of Rails/CGI

On to the main event: how to make Rails 2.3 run as a CGI. This requires a bit of rake work on your app, and then some poking of Apache to do the right thing.

The Meat

New Rails 2.3 apps don't have a dispatch.cgi any more, which makes codeteddy sad. However, according to the Rails 2.3 release notes, it's easy to get it back:

  • Various files in /public that deal with CGI and FCGI dispatching are no longer generated in every Rails application by default (you can still get them if you need them by adding --with-dispatches when you run the rails command, or add them later with rake rails:generate_dispatchers).

The Three Veg

We now need to tell apache to run the dispatch.cgi script when appropriate. Because we also serve static content from the rails app, we need to make sure dispatch.cgi isn't triggered when a normal file is accessed.

This piece of apache config lives inside a <VirtualHost> directive, which I've included for completeness. As mentioned, dispatch.cgi lives in the public directory of the Rails application, which is where static content also resides.

# Rails apps in CGI mode, we've made a separate vhost to keep things clean
<VirtualHost *>
        ServerName testapp1.despairworks.com:80

        ServerAdmin webmaster@despairworks.com
        ErrorLog logs/error_logs/despairworks_log

        # Static content lives here, so we root the vhost at this directory
        DocumentRoot /home/zetsubou/testapp1/public

        # We need to enable CGI execution here for dispatch.cgi
        AddHandler cgi-script .cgi
        <Directory /home/zetsubou/testapp1/public>
                Options +ExecCGI
        </Directory>

        RewriteEngine on

        # A "down for maintenance" handling page
        # This is a really cool trick you can use - if system/maintenance.html exists,
        # it will get served instead of going to the Rails application
        RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
        RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
        RewriteRule ^.*$ /system/maintenance.html [L]

        # This is where the real magic happens
        # The RewriteCond ensures that if a piece of static content exists,
        # it will get served normally. All other requests are assumed to be for the Rails app,
        # and will get rewritten to run the dispatch.cgi script, with the URI appended
        RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
        RewriteRule ^/(.*)$ /dispatch.cgi/$1 [QSA,L]
</VirtualHost>