Rails deployment: wouldn't it be great if it worked like this?
Unless you’ve been living under a rock (or if you never read Ruby on Rails blogs), you should know that there has been a lot of complaints about Rails deployment in the past few weeks. Heck, it’s not just the past few weeks, this issue has existed for far longer. Rails deployment is - so people say - too hard.
The first question that appeared in my head when I read all that, is: “what do they mean by ‘hard’?” I read their posts more carefully. I read the comments. I also asked around on my blog and the Ruby on Rails mailing list. As with most questions in life, there is not a single answer, but multiple.
How is Rails deployment typically done?
Before we dive into the answers, one should understand how Rails deployment currently works.
With PHP, it is straightforward:
- Upload the source files to a directory accessible by the web server.
- There is no step 2.
(I’ve conveniently left out the steps in which the user must edit config files and setup the database; this is application-dependent and is not a core part of PHP.)
PHP web servers typically run Apache, which has mod_php. The number of concurrent requests that a PHP application can handle, is equal to the number of worker threads/processes that Apache is running (see the Apache MPM documentation for more information).
There are several ways to deploy Rails. The most common one at the moment is as follows:
- Upload the Rails app to the web server.
- Configure the Mongrel application server on the web server (or configure a cluster of Mongrel, i.e. multiple Mongrel servers) and start it.
- Configure the web server (which is usually Lighttpd, Apache or Nginx) to proxy HTTP requests to Mongrel (or to one of the Mongrel instances in the cluster).
Since each Mongrel instance can only handle 1 HTTP request at the same time, the number of concurrent requests that your Rails application can handle, is equal to the number of Mongrel servers that you have running. This number is independent of the number of worker threads/processes that Apache is running.
This is not the only way though. It is possible bypass step 2 and 3, by running the Rails application as a CGI process. Of course, nobody does this because it’s insanely slow. On Dreamhost, it seems to take several seconds to load the Rails framework itself. If the web server has to start the Rails application from scratch every time there is an HTTP request, the website will grind to a halt. This is the reason why an application server is needed: it keeps the Rails application running.
There is another method: running a web server that supports FastCGI. FastCGI is like CGI, but it removes the startup overhead by keeping processes persistent. Lighttpd has FastCGI support by default. I’m not sure whether Nginx has FastCGI support by default, but it does have a FastCGI module. Apache has a (third party) FastCGI module as well.
Both Apache and Lighttpd support auto-starting FastCGI processes. That is, when an HTTP request is received, the web server will automatically start a FastCGI process if it hasn’t done that already. It can start more processes (up to a specified maximum) if it notices that the current number of processes can’t handle the current number of concurrent HTTP requests. It will automatically shutdown some processes after a certain idle period, to free memory. Shared hosts seem to typically use Apache with the FastCGI module.
However, there are several big catches with using FastCGI method. Apache’s FastCGI module has a very bad reputation. Many people have reported that it’s very unstable. FastCGI itself seems to be unmaintained in the past few years.
What are the complaints about Rails deployment?
These are the most important reasons why Rails deployment is either hard or perceived to be hard:
- Support on shared hosts. Rails apps don’t seem to play well on shared hosts like Dreamhost. Dreamhost said that their Rails app regularly make Apache choke, and uses too much system resources. Many people who uploaded their Rails app to a shared host find out that their app either doesn’t work, or is unstable.
- Deployment involves multiple steps. Many people seem to come from a PHP background, where they are used to the upload-and-go style of deployment. This is not possible with Rails, at least not in the same way as PHP. Setting up Mongrel is an extra step for them - they don’t want to think about application servers or clusters because they don’t have to with PHP.
- The need to do port administration if one runs Mongrel clusters. One has to remember which ports are reserved for which apps, in order to avoid conflicts in the future. As the number of hosted applications grows, this quickly becomes a serious pain in the ***.
- The installation of dependencies (gems). A J2EE developer pointed out that Java deployment is “easy” because it is the norm that all dependencies are bundled with the application itself. If a Ruby on Rails application depends on third party gems, those will have to be installed on the deployment server as well. Even though gems are easy to install (one command), the installation usually requires root privileges, something that is not always available.
- Perceived performance problems. It’s pretty well-known that Rails is not thread-safe and thus can handle only one request at the same time. Some people think that all Rails sites can only handle one request at the same time. This is of course not true: that’s the whole point of running a Mongrel cluster with multiple Mongrel instances: to be able to handle multiple concurrent requests. The number of Mongrel instances to run should be fine-tuned to the web server’s load. Nevertheless, the fact the Mongrel cluster has to be manually configured, gives people the (false) impression that PHP can (architecturally) handle more concurrent requests than Rails, even though the dispatching mechanism used by mod_php is conceptually not much different.
- Perceived “deployment risk” (whatever this is supposed to mean). Someone has pointed out that, because of the need to manually configure Mongrels, the “deployment risk” is higher than PHP.
As you can see, there are legitimate complaints (1-4) as well as superstition (5 and 6).
Point (1) is particularly interesting. Shared hosts are not able to run Mongrel clusters because they have to host so many websites. Suppose a shared hoster’s web server hosts 100 websites, and suppose that the typical Rails application uses 25 MB memory. Then they’ll need 2500 MB of memory if each website is reserved only 1 Mongrel instance. Ouch. So instead, shared hosts typically use Apache with FastCGI. This is the main source of stability problems that people are complaining about.
A common answer to this is that shared hosts should not use Apache, but use Lighttpd instead (Nginx doesn’t support auto-starting FastCGI processes so they can’t use that). Dreamhost’s answer to this is as follows:
Dreamhost has already standardized on Apache. A lot of their infrastructure depends on Apache. Switching to lighttpd is therefore non-trivial. Dreamhost also says that Apache is the defacto standard, and considers anything non-Apache as “bleeding edge technology”. I suspect that a lot of shared hosters are like this as well.
While Dreamhost’s response may come over like whining, Jon Gretar has formulated the point perfectly:
“Although I like using Litespeed [another web server which can run Rails with FastCGI] at home I just don’t have that luxury at work. Because I might have to use the same server to host a subversion repo for example. There is a reason Apache is loved by ISPs. Apache is a one size fits all solution. Apache can do just about everything. Except host Rails application.”
But even if they switch to Lighttpd, they might still run into memory problems. Ruby on Rails applications typically use more memory than PHP applications. Every Rails FastCGI process contains its own copy of the entire Rails framework as well as the application code itself. Rails applications therefore typically use at least 25 MB of memory, per process.
Another common answer is that people should use a VPS (Virtual Private Server) instead of a shared host. These days VPSses are very very cheap. However, many people are unwilling to purchase a VPS and would prefer to use a shared host for their small applications. They have a point - a VPS, while cheap, secure and powerful (you have root access), needs to be maintained. A shared host will allow one to delegate a lot of the maintenance burden to the hosting company.
Proposed solution
While I think most complaints about Rails deployment is exaggerated - it really isn’t as hard as people make it out to be - they do have a point. Why can’t Rails deployment be as easy as PHP, i.e. upload-and-forget?
To solve this problem, I propose a hypothetical Apache module, which I call mod_rails. It should follow the usual Rails conventions: Dont-Repeat-Yourself and as least configuration as possible.
- Configuration
mod_rails will need only one configuration option: RailsBaseURI, which specifies that the given URI is the base URI of a Rails application. For example, the Apache configuration might look like this:<VirtualHost *> ServerName www.foo.com DocumentRoot /home/webapps/foo/public RailsBaseURI / </VirtualHost>And this is all you have to do to make the Rails app, located in /home/webapps/foo, work. mod_rails will automatically manage Rails processes, just like FastCGI is supposed to, but with no additional configuration. The number of Rails processes is managed automatically, just like mod_php. There will be no need to think about the number of application servers that need to be started.
- Startup time
A lot of the Rails application startup time is to blame on the fact that the Rails framework has to be loaded. mod_rails can solve this by preloading the Rails framework. Since different applications may require different Rails versions, mod_rails can create multiple child processes, each which preload its own version of the Rails framework. mod_rails will then delegate the creation of Rails application processes to the preloader process which has the correct Rails version. - Memory efficiency
Preloading will also save a lot of memory. In modern operating systems, child processes shared most of its memory with the parent process. Thus, starting a Rails process - even for the first time - will be very fast. And the Rails application instances will use a lot less memory than with FastCGI. - Easily restart the Rails application
When the application code has changed, one needs to restart the Rails application before the changes have effect (unless of course when running in development mode, but we’re talking about production mode). Restarting the entire web server is out of the question: it would suddenly abort any downloads that the web server is handling at the moment.Ideally, we want mod_rails to restart the application automatically if it detects changes in the source. But checking all of the application’s source files on disk is expensive if we have to do it for every HTTP request, so that is out of the question. While this can be made very inexpensive by using a filesystem monitor daemon such as FAM or Gamin, we cannot rely on it being installed.
So instead, one could upload a file called “restart.txt” to the Rails application’s “tmp” folder. The file could be empty, its content is not important. If mod_rails detects such a file, it would remove the file and automatically restart the corresponding Rails application instances. If the file cannot be removed because of permission problems, then mod_rails would remember the file’s modification time, and only restart the Rails application again when it notices that restart.txt’s modification time has changed. The user only needs an FTP client to be able to restart the Rails application. Shared hosters will not even have to write special Rails restarting support for their control panels.
This is the easiest way I could think of, while still being low-overhead.
- Security
Shared hosts typically run all PHP scripts as the same user as the web server. So theoretically, customer A’s PHP script can peek into customer B’s files and steal his database password or other files. Not good. (Of course, storing sensitive data on a shared host is inherently unsafe, but still, we can do better than this.) This can be solved by running each customer’s PHP scripts as a different user. There are several ways to set this up, one of which is running PHP as FastCGI processes. Each customer will have its own pool of PHP FastCGI processes, serving only HTTP requests for that customer’s website.The alert reader will notice that this setup is similar to Rails in combination with Mongrel clusters. Indeed, they share the same problems. It wastes a lot more memory.
mod_rails can solve this by spawning Rails application instances via a setuid root program. The web server tells the spawner program to spawn a Rails application as a specific user. The spawner program will then check, through a whitelist of usernames, whether it is allowed to spawn a program as that user. It will then spawn the Rails application, and pass the Rails application’s communication channel back to the web server. The web server can then directly forward HTTP requests to the Rails application, which is now running as a different user than the web server. This can be combined with preloading, for maximum memory efficiency.
I’d like to hear what others think about this solution. Would this be easy and good enough? Can it be better?666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666
666666666666666666666666666666666666666666666666
6666666666666666666666666666666666666666
6666666666666666666666666666666666666666666666666666666666

James Stewart said,
January 28, 2008 @ 12:34 am
My main fear with a mod_rails would be that it would put the version of rails my app runs on into the hands of the hosting company. I always freeze rails (and any dependent gems — in most cases they can be packaged with the rails app and don’t need to be installed server-wide) into my app and regularly deploy apps requiring Edge features.
Just today I ran into a problem with a host offering mod_php. The supposedly easy deployment process quickly ran into a brick wall because various PHP extensions I needed weren’t compiled into their version of PHP. mod_php makes life easy for simple scripts, but I’ve never felt like it makes life that much easier for deploying complex apps, particularly when you take the juggling of compiled-in extensions and the existence of tools like capistrano into account.
That said, if a way were found to get around these issues for a mod_rails (or a mod_ruby) then it would be good to make hosting providers’ lives easier.
Andrew Kaspick said,
January 28, 2008 @ 1:17 am
I was thinking about all of this recently too. Anything to make Rails deployment easier would be very welcome. As you say, most people are using Apache, even for clients that are running existing applications in PHP and Apache, being able to easily deploy a Rails app to the same server without too much hassle would be ideal.
Arlen Cuss said,
January 28, 2008 @ 2:24 am
This sounds like an excellent idea! I think there must be neater ways to restart the server, but otherwise I wish someone would come along and write mod_rails. I have only a vague idea about Apache module architecture (after hacking apart some modules to compile), but possibly it’s not so far off.
bascule said,
January 28, 2008 @ 5:16 am
Sounds great, except: why not target some sort of abstract solution for hosting Ruby web applications, rather than being Rails specific? Something like mod_rack perhaps?
Andrew Cholakian said,
January 28, 2008 @ 6:57 am
An awesome idea, though like bascule said mod_rack would be better. I think something like this would also make sysadmins feel more safe about being able to manage servers running rails apps even if they don’t have ruby experience.
karmi said,
January 28, 2008 @ 11:13 am
”
Because the current Rails Way of deployment with versioned source code and Capistrano promotes much better and healthier development practices than PHP deployment in “upload-and-forget” style?
(If you’re looking for “a la PHP” Ruby framework, have a look on magnificent http://sinatra.rubyforge.org/)
Saimon Moore said,
January 28, 2008 @ 1:56 pm
Good idea, right the module for nginx first and make sure it does ‘fair’:http://brainspl.at/articles/2007/11/09/a-fair-proxy-balancer-for-nginx-and-mongrel proxying while you’re at it
Neil Wilson said,
January 28, 2008 @ 3:42 pm
Slight problem with your vision. ‘mod_rails’ is an Apache module. If you fork that then you will replicate the entire Apache process again - including the bits you don’t want. Add to that the Ruby interpreters annoying inability to handle Copy on write effectively and you’ll be up to Mongrel size in no time.
What you actually need is a mongrel cluster generator that forks the processes, and a Ruby interpreter that doesn’t expand cause memory replication. Then what you do is create a Unix user for every customer you have, and boot empty mongrel clusters on preallocated ports (the ones you’ve paid for) via the users own crontab. The mongrels run in user space, pointing at a blank Rails app sat in the users home directory. This is all created automagically via the Unix skeleton system. The web server proxies onto these ports for whaver domain it is managing. You then upload the Rails app into the home directory via capistrano in the normal fashion.
The solution is here http://accounts4free.rubyforge.org/svn/multi-tenant, but is in a pretty rough form at present and is probably out of date with regards to Rails 2, etc.
Hongli said,
January 28, 2008 @ 7:25 pm
Neil Wilson: You’re misunderstanding a few things, namely:
- Ruby’s current garbage collector is not copy-on-write friendly, this is correct. But I’ve already fixed this. See my previous blog articles in the Optimizing Rails category.
- Ruby’s copy-on-write unfriendliness only affects the Ruby object heap, not everything else. A garbage collection will *not* make all the Apache data pages dirty as well.
Neil Wilson said,
January 28, 2008 @ 8:30 pm
You’ll probably pull in all the Apache memory management routines, which you would have to use to get Ruby running in the shared space without running into something else. Otherwise you risk potentially destabalising the web server with your parent forking structure.
I don’t think there is much to be gained from sticking Rails within the Apache process. The real gain is CoW forking from the cluster start up processes with preloading, and again I wonder if it is worth creating the programming necessary to allow forking across users - with all the security problems that may turn up.
Fire up a mongrel/thin cluster using forking from user space using crontab @boot with the Rails app in the Unix user home directory. If you use thin as opposed to Mongrel you can use Unix sockets rather than ports and they have proper names! A crontab watch script can allow a restart on a file touch.
What I suggest is simple and can be done today with existing technology. You can use edge Rails and Gems - even a different Ruby interpreter if you change your path.
One commercial advantage of having multiple backend application servers running is that you can charge per port. The more concurrent ports you require for your app, the more it costs.
Hongli said,
January 28, 2008 @ 9:45 pm
Neil Wilson: mod_rails will not run the Rails app inside Apache’s process space. It is not mod_ruby. Rails apps crashing will have no effect on Apache other than returning an Internal Server Error back to the client.
Whether all this trouble is worth it, is something that the users should decide.
Dan Kubb said,
January 28, 2008 @ 10:11 pm
I know the thin server just added support for running via unix sockets. I wonder if this would solve problem #3.
Hongli said,
January 28, 2008 @ 10:15 pm
Dan Kubb: Yes it would, but instead of port administration you now have to do socket administration (though this is easier because there’s less chance on conflicts). It would be best if no administration is needed, at all.
Greg Lorriman said,
February 4, 2008 @ 11:42 pm
My own problems with with deployment where mostly due to the bad documentation on the mongrel site. It is confusing. I instead turned to my VPS service documentation for mongrel setup, and it was also inadequate. I ended up having to solve the problems myself, having never configured Apache before: it took a long time.
A secondary problem is the number of problems with gem updates/installs. Gems seems to confuse the functionality of ‘install’ and ‘update’. Even though I technically needed to update, actually I had to install. It took a long time to discover that.
Roger Pack said,
February 7, 2008 @ 11:21 pm
Have you looked into mod_ruby? I believe I once read that it can handle multiple rails guys going at once, but…I’d say the main problem with mod_rails is the huge RAM cost. Unless all the rails apps are trained to ‘play nice’ and ‘not take too long’ and ‘don’t override each other class methods’ you’ll need lots and lots of memory. Or I could be wrong. I don’t see an easy way out. Thoughts?