Apache + Mongrel + Rails

THIS PAGE IS OLD AND NO LONGER RELEVANT

Apache

Install apache:

# apt-get install apache2

Make it so /etc/init.d/apache2 will start apache:

codetitle. /etc/defaults/apache2

NO_START=0

Enable 443 so we can do SSL:

codetitle. /etc/apache2/ports.conf

Listen 80
Listen 443

Enable the needed apache modules:

# a2enmod rewrite
# a2enmod proxy
# a2enmod proxy_http
# a2enmod proxy_balancer
# a2enmod ssl
# a2enmod headers

Set up the basic VirtualHost sections, requiring SSL connection:

codetitle. /etc/apache2/conf.d/we.riseup.net

<VirtualHost we.riseup.net:80>
  ServerName we.riseup.net
  RedirectMatch (.*)$ https://we.riseup.net$1
</VirtualHost>

<VirtualHost we.riseup.net:443>
  SSLEngine on
  SSLCertificateKeyFile /etc/certs/we.riseup.net/key.pem
  SSLCertificateFile /etc/certs/we.riseup.net/cert.pem
  RequestHeader set X_FORWARDED_PROTO 'https'

  ServerName we.riseup.net

  DocumentRoot /usr/apps/crabgrass/current/public
  <Directory "/usr/apps/crabgrass/current/public">
    Options FollowSymLinks
    AllowOverride None
    Order allow,deny
    Allow from all
  </Directory>
</VirtualHost>

The X_FORWARDED_PROTO is needed when using mongrel in order for rails to think that it is really serving https.

Test the apache configuration:

# apache2ctl configtest

If that works, re/start apache:

/etc/init.d/apache2 stop
/etc/init.d/apache2 start

If everything worked, you should be able to browse to static files like /javascripts/prototype.js.

Mongrel

Mongrel is mini web server written in ruby. It is designed to be a wrapper around ruby applications to give them a long lived process to call home. Instead of using the archaic cgi interface, mongrel communications with apache over plain old http. All requests for static files will go to apache, and requests for the web application will get passed on to one of three mongrel processes, which will in turn pass the request on to our rails web application. We could set up mongrel to be our web server instead of apache, but apache is better at serving static files and allows us the possibility of setting up many backend mongrel/rails processes (possibly on other servers).

Install mongrel:

 
# aptitude install ruby1.8-dev build-essential
# gem install mongrel -y --no-rdoc --no-ri
# aptitude remove build-essential

Create a generic setup for a rails application:

codetitle. /etc/apache2/rails.conf

RewriteEngine On

# Check for maintenance file and redirect all requests
#  ( this is for use with Capistrano's disable_web task )
RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
RewriteCond %{SCRIPT_FILENAME} !maintenance.html
RewriteRule ^.*$ /system/maintenance.html [L]

# Rewrite index to check for static
RewriteRule ^/$ /index.html [QSA] 

# Rewrite to check for Rails cached page
RewriteRule ^([^.]+)$ $1.html [QSA]

# Redirect all non-static requests to cluster
RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
RewriteRule ^/(.*)$ balancer://mongrel_cluster%{REQUEST_URI} [P,QSA,L]

To activate this configuration for a particular VirtualHost, add Include /etc/apache2/rails.conf.

This configuration will let static files be served from disk, and all other requests will be sent to mongrel_cluster. What is mongrel_cluster? Lets define it:

codetitle. /etc/apache2/conf.d/mongrel-cluster

<Proxy balancer://mongrel_cluster>
  Order allow,deny
  Allow from all
  BalancerMember http://127.0.0.1:8000
  BalancerMember http://127.0.0.1:8001
  BalancerMember http://127.0.0.1:8002
</Proxy>

What have we done? When a request comes in that is not a static file, it is passed to balancer::mongrel_cluster. We have defined this to then proxy the connection to localhost on one of three ports where we have our mongrel mini-web-server running. By default, if you have mongrel installed, then starting the rails spawner will start three mongrel processes listening on 8000, 8001, and 8002.

To start, restart, stop, and get the status of the mongrel processes:

crabgrass@we% /usr/apps/crabgrass/current/script/process/spawner
crabgrass@we% /usr/apps/crabgrass/current/script/process/reaper
crabgrass@we% /usr/apps/crabgrass/current/script/process/reaper -a kill
crabgrass@we% /usr/apps/crabgrass/current/script/process/inspector

Mongrel can run as whatever user you want. Typically, your rails application will have its own user that is different from www-data. Mongrel should be started and stopped using that user. In our case, the user crabgrass owns all crabgrass web application files.

Now that mongrel is running, restart apache and everything should be working.

Debugging mod_rewrite:

codetitle. /etc/apache2/rails.conf

RewriteLog /var/log/apache2/rewrite.log
RewriteLogLevel 9 
# touch /var/log/apache2/rewrite.log
# /etc/init.d/apache2 restart

Apache, compressed

The module mod_deflate will automatically send text files in a compressed format to web browsers that support it. This will often reduce the size of a file by 50-80%.

Enable the deflate module:

# a2enmod deflate

Create a generic configuration for deflate:

codetitle. /etc/apache2/deflate.conf

AddOutputFilterByType DEFLATE application/x-javascript text/html text/plain text/css text/javascript text/xml
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html

Now we can just add Include /etc/apache2/deflate.conf to any VirtualHost or Location section we want to serve compressed files.

If you want to debug deflate, you can add this too:

codetitle. /etc/apache2/deflate.conf

DeflateFilterNote Input input_info
DeflateFilterNote Output output_info
DeflateFilterNote Ratio ratio_info
LogFormat '"%r" %{output_info}n/%{input_info}n (%{ratio_info}n%%)' deflate
CustomLog /var/log/apache2/deflate.log deflate
# touch /var/log/apache2/rewrite.log
# /etc/init.d/apache2 restart

Apache, cachable

The web browser will do its darnedest to cache static content when it can. However, it could use help. In our case, we have a lot of small avatar files with unique URLs: if the image changes, then the avatar will be different. So we would like to cache images for a very long time. The module mod_expire lets you specify how long certain content types should be cached for.

Enable mod_expire

# a2enmod expire

Create a generic configuration for mod_expire:

codetitle. /etc/apache2/expire.conf

<IfModule mod_expires.c>
  # turn on the module for this directory
  ExpiresActive on

  # cache common graphics for 3 days
  ExpiresByType image/jpg "access plus 3 days" 
  ExpiresByType image/gif "access plus 3 days" 
  ExpiresByType image/jpeg "access plus 3 days" 
  ExpiresByType image/png "access plus 3 days" 

  # cache css and javascript forever
  ExpiresByType text/javascript "access plus 10 years" 
  ExpiresByType application/x-javascript "access plus 10 years" 
  ExpiresByType text/css "access plus 10 years" 

  ExpiresDefault "access plus 24 hours" 
</IfModule>

To use this mod_expire configuration, put Include /etc/apache2/expire.conf in a Directory section in your apache config.

Caching forever for js and css is good in our case, because these assets will get a new url if they change.

Allow non-ssl for assets

Firefox will not cache anything on disk that came via ssl, but we would really really like firefox to disk cache the css and js.

So, instead of just redirecting all traffic from http to https, we do it selectively:

RewriteEngine On
RewriteCond %{REQUEST_URI} !^/bundles/
RewriteRule ^.*$ https://%{SERVER_NAME}%{REQUEST_URI} [R=permanent,L]

This will redirect anything that is not /bundles/ to https. The way this particular rails application is written, /bundles/ will get handled by rails the first time, but subsequent loads will be statically cached.

This is what it looks like in the VirtualHost:

<VirtualHost we.riseup.net:80>
  ServerName we.riseup.net
  DocumentRoot /usr/apps/crabgrass/current/public
  <Directory "/usr/apps/crabgrass/current/public">
    Options FollowSymLinks
    AllowOverride None
    Order allow,deny
    Allow from all
    Include /etc/apache2/expire.conf
  </Directory>

  RewriteEngine On
  RewriteCond %{REQUEST_URI} !^/bundles/
  RewriteRule ^.*$ https://%{SERVER_NAME}%{REQUEST_URI} [R=permanent,L]

  Include /etc/apache2/rails.conf
  Include /etc/apache2/deflate.conf
</VirtualHost>

The order matters: the ssl redirect must come before the rails.conf.

Putting it all together

codetitle. /etc/apache/conf.d/we.riseup.net

<VirtualHost we.riseup.net:80>
  ServerName we.riseup.net
  DocumentRoot /usr/apps/crabgrass/current/public
  <Directory "/usr/apps/crabgrass/current/public">
    Options FollowSymLinks
    AllowOverride None
    Order allow,deny
    Allow from all
    Include /etc/apache2/expire.conf
  </Directory>

  RewriteEngine On
  RewriteCond %{REQUEST_URI} !^/bundles/
  RewriteRule ^.*$ https://%{SERVER_NAME}%{REQUEST_URI} [R=permanent,L]

  Include /etc/apache2/rails.conf
  Include /etc/apache2/deflate.conf
</VirtualHost>

<VirtualHost we.riseup.net:443>
  SSLEngine on
  SSLCertificateKeyFile /etc/certs/we.riseup.net/key.pem
  SSLCertificateFile /etc/certs/we.riseup.net/cert.pem
  RequestHeader set X_FORWARDED_PROTO 'https'

  ServerName we.riseup.net

  DocumentRoot /usr/apps/crabgrass/current/public
  <Directory "/usr/apps/crabgrass/current/public">
    Options FollowSymLinks
    AllowOverride None
    Order allow,deny
    Allow from all
    Include /etc/apache2/expire.conf
  </Directory>

  Include /etc/apache2/rails.conf
  Include /etc/apache2/deflate.conf
</VirtualHost>

codetitle. /etc/apache2/rails.conf

RewriteEngine On

# Check for maintenance file and redirect all requests
#  ( this is for use with Capistrano's disable_web task )
RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
RewriteCond %{SCRIPT_FILENAME} !maintenance.html
RewriteRule ^.*$ /system/maintenance.html [L]

# Rewrite index to check for static
RewriteRule ^/$ /index.html [QSA] 

# Rewrite to check for Rails cached page
RewriteRule ^([^.]+)$ $1.html [QSA]

# Redirect all non-static requests to cluster
RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
RewriteRule ^/(.*)$ balancer://mongrel_cluster%{REQUEST_URI} [P,QSA,L]

codetitle. /etc/apache2/conf.d/mongrel-cluster

<Proxy balancer://mongrel_cluster>
  Order allow,deny
  Allow from all
  BalancerMember http://127.0.0.1:8000
  BalancerMember http://127.0.0.1:8001
  BalancerMember http://127.0.0.1:8002
</Proxy>

codetitle. /etc/apache2/deflate.conf

AddOutputFilterByType DEFLATE application/x-javascript text/html text/plain text/css text/javascript text/xml
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html

codetitle. /etc/apache2/expire.conf

<IfModule mod_expires.c>
  # turn on the module for this directory
  ExpiresActive on

  # cache common graphics for 3 days
  ExpiresByType image/jpg "access plus 3 days" 
  ExpiresByType image/gif "access plus 3 days" 
  ExpiresByType image/jpeg "access plus 3 days" 
  ExpiresByType image/png "access plus 3 days" 

  # cache css and javascript forever
  ExpiresByType text/javascript "access plus 10 years" 
  ExpiresByType application/x-javascript "access plus 10 years" 
  ExpiresByType text/css "access plus 10 years" 

  ExpiresDefault "access plus 24 hours" 
</IfModule>