Thursday, May 29, 2008

Deploying Rails on Windows

Why would you want to deploy Rails on Windows? With tools like Capistrano, surely deploying on Linux is the obvious choice. Well there are (at least) two valid reasons to deploy Rails on Windows:

  1. You don't have a choice. Many companies are Windows shops and maintaining a Linux/OS X/other server is simply not an option. This situation is becoming less common as Linux gains popularity in the server room. However, depending on the scale of your Rails app, you may have to share a server with other services, and this reason still holds.
  2. You use Microsoft's SQL Server as your Rails database. This is probably not common for new Rails apps in completely new systems (where you have your choice of SQL servers), although SQL Server is still a good product and sensible choice. Rails connects to SQL Server through ADO (at least, this is the method suggested by AWDwR), which is only available on Windows.

I know there are many guides on running Rails, but I didn't find one that covered this particular configuration in detail: LightTPD <- SCGI -> Rails <- ADO -> SQL Server on Windows, running as services. This guide covers vanilla Windows XP/Windows Server 2003 to working web server. It assumes you have a Rails app working in a development environment (such as RadRails), connecting to an existing SQL Server, and committed to an existing SVN server.


Gather and Install Necessary Software

  1. Ruby (v 1.8.6) - Download the Ruby One-click Installer for Windows. Install to C:\Ruby\ and include Gems.
  2. LightTPD (v 1.4.18-1) - Download the pre-compiled LightTPD web server (.exe), part of the WLMP project. Install to C:\LightTPD\.
  3. Rails (v 2.0.2) - Install Rails via RubyGems. Open the command line and type:
    gem update --system
    gem install rails
  4. Zed Shaw's SCGI Rails Runner (v 0.4.3) - Download the gem from Shaw's blog (click the link labeled "gem"). Install .gem file using RubyGems; again, open the command line and type:
    gem install C:\path\to\scgi_rails-0.x.x.gem
    Replace the path in the above command with the location/file you downloaded the .gem to.
  5. Subversion (v 1.4.5) - Download the SVN Windows Installer. Install to the default location (C:\Program Files\Subversion); this will install the SVN command-line tools.
  6. Windows Server 2003 Resource Kit Tools - Download rktools.exe. Install to the default location (C:\Program Files\Windows Resource Kits\Tools).
  7. Ruby/DBI ADO (v 0.1.1) - Download the ruby-dbi package from RubyForge. Extract archive somewhere using your favorite tool (such as 7-zip). Install the package from the command line:
    cd C:\path\to\ruby-dbi\
    ruby setup.rb config --with=dbd_ado
    ruby setup.rb setup
    ruby setup.rb install
    Replace the path in the above command with the location where you extracted ruby-dbi.
  8. Rails SQL Server adapter gem. This step is optional if you freeze this gem into your application as described below.

I had to fix the SCGI Rails gem to work with Rails 2.0; you may not for later versions of the SCGI Rails Runner. Open the file C:\ruby\lib\ruby\gems\1.8\gems\scgi_rails-0.4.3\bin\scgi_service using your favorite Ruby editor (or just WordPad), and comment out line 36,

    ActiveRecord::Base.threaded_connections = false

so that it looks like this:

    #ActiveRecord::Base.threaded_connections = false

Apparently this method is no longer available in Rails 2.0.  I have not run into any issues removing this line, but if anyone has any suggestions for fixing it for Rails 2.0, let me know.

Note: Since we're using the command line a lot in this guide, it makes sense to leave the window open. At this point however, you should close all command line windows, because some of the installers add locations to your PATH variable, which only affects new command line windows.

Prepare Rails Application

This guide suggests a configuration to host multiple Rails applications from a single Lighty server. We create a main folder to contain all Rails apps; inside, we put a log folder for the Lighty logs, and a folder for each Rails application. In my particular setup, I use this as a way to run the same intranet Rails app in production and development modes for testing purposes before rolling out changes.

Open a command line and type:

cd C:\
mkdir webapps
cd webapps
mkdir log
svn export svn://your-svn-server/railsapp1/trunk railsapp1
cd railsapp1
scgi_ctrl config -S

Replace "your-svn-server" with the name of your svn server, and "railsapp1" with the name/svn path of your particular Rails application. Note: the last command will ask you for a password. I have no clue what this password is for, so I don't think it's too important. If you know what the password is for let me know.

The scgi_ctrl command creates a new file in your rails app at C:\webapps\railsapp1\config\scgi.yaml. For additional apps, you will need to alter this file. For example, to run the app in development mode, you would change the file to look like this:

--- 
:disable_signals: true
:env: development
:control_url: druby://127.0.0.1:8998
:config: config/scgi.yaml
:host: 127.0.0.1
:port: 9998
:password: LeaveThePasswordAlone
:logfile: log/scgi.log

The useful options are highlighted above. You must specify a different port for each Rails app to run on (LightTPD redirects requests to these ports). You may also change the env setting to a different environment, based on your needs.

Configure Lighty

Copy the folder C:\LightTPD\conf to C:\LightTPD\conf-backup so that you have the original configuration files for future reference. Open C:\LightTPD\conf\lighttpd-srv.conf in a text editor (again, Wordpad works). Edit the file to match the following:

# LightTPD Configuration file (RUN AS A SERVICE)
#
# Use it as a base for LightTPD 1.0.0 and above.
# This version is built for WLMP Project - http://wlmp.dtech.hu/
#
# $Id: lighttpd-srv.conf,v 1.0 2006/11/03 23:35:28 weigon Exp $

## where to send error-messages to
server.errorlog             = "C:/webapps/log/lighttpd-srv.error.log"

#### accesslog module
accesslog.filename          = "C:/webapps/log/lighttpd-srv.access.log"

## to help the rc.scripts
#server.pid-file            = "C:/LightTPD/logs/lighttpd-srv.pid"

#### include other configfiles
include "C:/LightTPD/conf/lighttpd-tag.conf"
include "C:/LightTPD/conf/lighttpd-inc.conf"

The important changes are highlighted above. Providing absolute paths becomes necessary when running Lighty as a service.

Next, open C:\LightTPD\conf\lighttpd-srv.conf in your text editor. Replace it with the following, and edit to suit your specific paths:

# LightTPD Configuration file (INCLUDE)
#
# Use it as a base for LightTPD 1.0.0 and above.
#
# This version is a stripped down version for use with SCGI, Rails, and 
# multiple hosts on ports.
#
# $Id: lighttpd-inc.conf,v 1.7 2004/11/03 22:26:05 weigon Exp $

############ Options you really have to take care of ####################

## Always has to be a default root
var.home = "C:/webapps"
server.document-root = var.home + "/railsapp1/public" 

## modules to load
server.modules              = (
                                "mod_access",
                                "mod_accesslog",
                                "mod_alias",
                                "mod_redirect",
                                "mod_rewrite",
                                "mod_scgi",
                                "mod_ssi",
                                "mod_status",
                               )
## App1 Configuration

var.app1 = var.home + "/railsapp1"

$SERVER["socket"] == ":80" {
    server.port                     = 80
    server.document-root            = var.app1 + "/public"
    server.upload-dirs              = ( var.app1 + "/tmp" ) 
    server.errorlog                 = var.home + "/log/app1-errors.log" 
    accesslog.filename              = var.home + "/log/app1-access.log" 
    static-file.exclude-extensions  = ( ".cgi", ".fcgi", ".scgi" )
    server.error-handler-404        = "/dispatch.scgi" 
    scgi.server = ( "dispatch.scgi" => ((
        "host" => "127.0.0.1",
        "port" => 9999,
        "check-local" => "disable" 
    )) )
}

## App2 Configuration

var.app2 = var.home + "/railsapp2" 

$SERVER["socket"] == ":3000" {
    server.port                     = 3000
    server.document-root            = var.app2 + "/public"
    server.upload-dirs              = ( var.app2 + "/tmp" ) 
    server.errorlog                 = var.home + "/log/app2-errors.log" 
    accesslog.filename              = var.home + "/log/app2-access.log" 
    static-file.exclude-extensions  = ( ".cgi", ".fcgi", ".scgi" )
    server.error-handler-404        = "/dispatch.scgi" 
    scgi.server = ( "dispatch.scgi" => ((
        "host" => "127.0.0.1",
        "port" => 9998,
        "check-local" => "disable" 
    )) )
}

## Applies to all Apps

## files to check for if .../ is requested
# index-file.names            = ( "index.html", "index.htm", "default.htm" )

# mimetype mapping
mimetype.assign             = (
  ".pdf"          =>      "application/pdf",
  ".sig"          =>      "application/pgp-signature",
  ".spl"          =>      "application/futuresplash",
  ".class"        =>      "application/octet-stream",
  ".ps"           =>      "application/postscript",
  ".torrent"      =>      "application/x-bittorrent",
  ".dvi"          =>      "application/x-dvi",
  ".gz"           =>      "application/x-gzip",
  ".pac"          =>      "application/x-ns-proxy-autoconfig",
  ".swf"          =>      "application/x-shockwave-flash",
  ".tar.gz"       =>      "application/x-tgz",
  ".tgz"          =>      "application/x-tgz",
  ".tar"          =>      "application/x-tar",
  ".zip"          =>      "application/zip",
  ".mp3"          =>      "audio/mpeg",
  ".m3u"          =>      "audio/x-mpegurl",
  ".wma"          =>      "audio/x-ms-wma",
  ".wax"          =>      "audio/x-ms-wax",
  ".ogg"          =>      "application/ogg",
  ".wav"          =>      "audio/x-wav",
  ".gif"          =>      "image/gif",
  ".jpg"          =>      "image/jpeg",
  ".jpeg"         =>      "image/jpeg",
  ".png"          =>      "image/png",
  ".xbm"          =>      "image/x-xbitmap",
  ".xpm"          =>      "image/x-xpixmap",
  ".xwd"          =>      "image/x-xwindowdump",
  ".css"          =>      "text/css",
  ".html"         =>      "text/html",
  ".htm"          =>      "text/html",
  ".js"           =>      "text/javascript",
  ".asc"          =>      "text/plain",
  ".c"            =>      "text/plain",
  ".cpp"          =>      "text/plain",
  ".log"          =>      "text/plain",
  ".conf"         =>      "text/plain",
  ".text"         =>      "text/plain",
  ".txt"          =>      "text/plain",
  ".dtd"          =>      "text/xml",
  ".xml"          =>      "text/xml",
  ".mpeg"         =>      "video/mpeg",
  ".mpg"          =>      "video/mpeg",
  ".mov"          =>      "video/quicktime",
  ".qt"           =>      "video/quicktime",
  ".avi"          =>      "video/x-msvideo",
  ".asf"          =>      "video/x-ms-asf",
  ".asx"          =>      "video/x-ms-asf",
  ".wmv"          =>      "video/x-ms-wmv",
  ".bz2"          =>      "application/x-bzip",
  ".tbz"          =>      "application/x-bzip-compressed-tar",
  ".tar.bz2"      =>      "application/x-bzip-compressed-tar"
 )

# Use the "Content-Type" extended attribute to obtain mime type if possible
mimetype.use-xattr          = "enable"

######### Options that are good to be but not neccesary to be changed #######

## enable debugging
#debug.log-request-header   = "enable"
#debug.log-response-header  = "enable"
#debug.log-request-handling = "enable"
#debug.log-file-not-found   = "enable"

#### SCGI module
scgi.debug                  = 0

#### status module
status.status-url           = "/server-status"
status.config-url           = "/server-config"

#### url handling modules (rewrite, redirect, access)
#url.rewrite                = ( "^/$"             => "/server-status" )
#url.redirect               = ( "^/wishlist/(.+)" => "http://www.123.org/$1" )
#### rewrite to pick up page cache
url.rewrite = ( "^([^.]+)$" => "$1.html" ) 

Again, portions that you will most likely need to change are highlighted above. Be sure to provide absolute paths.

Finally, with Lighty configured, we want to run that Rails app! Copy the following into a text editor and save it to C:\LightTPD\Start_LightTPD.bat:

@echo off
c:
cd C:\lighttpd\
echo Starting lighty...
START /B lighttpd.exe -f conf\lighttpd-srv.conf -m lib -D
PAUSE >NUL && EXIT

And save the following to C:\LightTPD\Start_RailsApp1.bat:

@echo off
c:
cd C:\webapps\railsapp1
echo Starting RailsApp1 SCGI_service...
START /B scgi_service
PAUSE >NUL && EXIT

As always, adjust to suit your particular Rails app. You will want to create a runner batch file for each Rails app you wish to run. (unless you run as services as described below)

Now just run each batch file. A command window should appear for each, print the "Starting x..." line, and stay open. Pop open a web browser, and type http://localhost/ in the address bar. If your Rails app comes up, congratulations! If not, you've got some troubleshooting to do. Check the command windows for any errors; you can also look at the logs in C:\webapps\log and C:\webapps\railsapp1\log for errors and additional info.

Setup LightTPD and Rails as Services

Once you have Lighty and Rails running from batch files, you may want to run them as services. Services have two major advantages: they keep running when you logout, and they can run with restricted security privileges.

We are going to use the srvany program that comes with the Windows Resource Kit to run both Lighty and Rails as services. srvany acts as a wrapper that allows regular applications to run as a Windows service. Open a command line and type the following:

instsrv lighttpd "C:\Program Files\Windows Resource Kits\Tools\srvany.exe"
instsrv railsapp1 "C:\Program Files\Windows Resource Kits\Tools\srvany.exe"

Create a single service for LightTPD, and one service for every Rails app you wish to run. Just change the name of the service (highlighted above).

This creates two new services, named "lighttpd" and "railsapp1" that both run srvany. Now we edit the registry to configure what srvany runs. You can do this manually (using the values listed below), or use a text editor to save the following to lighttpd.reg (doesn't matter where you put it):

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\lighttpd\Parameters]
"Application"="C:\\LightTPD\\LightTPD.exe"
"AppParameters"="-f \"C:\\LightTPD\\conf\\lighttpd-srv.conf\" -m \"C:\\LightTPD\\lib\" -D"

Save the following to railsapp1.reg (or name of your Rails app):

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\railsapp1\Parameters]
"Application"="C:\\ruby\\bin\\ruby.exe"
"AppParameters"="C:\\ruby\\bin\\scgi_service"
"AppDirectory"="C:\\webapps\\railsapp1"

Save a copy of the .reg file above for each Rails service, changing the registry path and AppDirectory value to point to your service name and Rails app location.

Finally, double-click each of these .reg files to add their contents to the registry.

By default, new services run under (or "log on as") the Local System account. This should be changed to a low-privilege account, in case the Rails environment or LightTPD is compromised. We want to run all of the services in this Guide under the Network Service account.

Open the Services dialog at Start > Control Panel > Admistrative Tools > Services. Find the service "lighttpd" that we created, and double-click it. Click the "Log On" tab at the top of the properties dialog, and click "This account:" radio. Type "NT AUTHORITY\NetworkService" into the account field, and clear the password and confirm fields. Click "OK" in the properties dialog. Repeat these steps for each Rails service you have created.

One last step is needed to get the services to run. Both LightTPD and the SCGI Rails Runner will crash if they don't have write permissions to their respective log directories. (As a bonus, when running as services under srvany, they crash silently, not even appearing to have stopped, and having no way to report the error) Using Explorer, navigate to C:\webapps\, right-click the log folder, and select "Properties". Click the "Security" tab, and click "Add...". Type "Network Service" into the object names field, and click "OK". Back in the Properties dialog, make sure "NETWORK SERVICE" is selected in the upper user name list, and check the box in the lower permissions list to allow writes (row "Write", column "Allow"). Click "OK" on the properties dialog. Now navigate to C:\webapps\railsapp1\, etc. and repeat for the log folder inside each Rails app.

Now to test the services. Make sure to close any open command windows that are running LightTPD and/or your Rails apps (such as the batch files created above). Start the services using the Services dialog (Start > Control Panel > Admistrative Tools > Services) or from the command line:

NET START lighttpd
NET START railsapp1

At this point, I'd recommend starting Lighty and just one Rails app, then checking if it is running by pointing a browser at http://localhost/. If it works, start up and test the rest of your Rails apps and call it a day! If you get a blank page, you will have to troubleshoot your configuration. The only good way to do that is to run cmd using srvany. One gotcha: Microsoft's solution uses Local System with the "interact with desktop" feature; however, this only works when you are logged directly into the machine, not over a Remote Desktop connection. Once you have cmd running as a service, try running your batch files from before and troubleshoot any errors reported.

Automating deployment

A couple tips to automate deployment:

  • Commit config\scgi.yaml to SVN, so that you don't have to run scgi_ctrl config when exporting future versions of your webapp. If you need multiple versions of scgi.yaml (say, for different environments), create a deploy folder in your Rails app, then create deploy\scgi-prod.yaml and deploy\scgi-dev.yaml; copy one of these to config\scgi.yaml at deployment.
  • Create a batch file to automate the process, and create a shortcut to said batch on your desktop. This gets you fairly close to one-click deployment (SVN commit, RDC to server, run batch). A sample deploy script might look like this (save in C:\webapps\):
    @echo off
    echo Stopping Service
    NET STOP railsapp1
    sleep 1
    echo Deleting old copy
    rd /s /q railsapp1
    echo Exporting new copy from SVN
    svn export svn://your-svn-server/railsapp1/trunk railsapp1
    echo Configuring deployment
    copy railsapp1\deploy\scgi-prod.yaml railsapp1\config\scgi.yaml
    echo Starting Service
    NET START railsapp1

5 comments:

Anonymous said...

thanks for this nice little trick.

Anonymous said...

I've followed the steps very carefully... Lighty and the SGCI_service start without issue, but I navigate to localhost:8080/ and I get a blank page...

If it helps, a ruby script/server works just fine...

Please let me know if anything jumps out at you!

Brian Hogan said...

I really encourage people not to try this approach. I recommend the approach I outlined in

http://www.pragprog.com/titles/fr_deploy/deploying-rails-applications

It shows a much better way to do this. SCGI hasnt been supported for over 3 years and it's buggy as hell.

Apache + mongrel + mongrel_service is all you need. The mongrel_service gem allows you to install a Rails application running under Mongrel as a win32 service. Start 2 or more of these on their own ports, then use Apache mod_proxy_balancer to load balance them. You can even set up a proxy behind IIS with a little rewrite magic.

Red M@ said...

Brian -

Thanks for the suggestion.

At the time that I wrote this up, this was the only suggested Windows deployment configuration on the Rails wiki. After playing with it, I realize that I'm not getting the load ballancing I was promised. Other than that, it has honestly worked fine running an internal app for a couple of years.

If I were going to redo our deployment, I'd probably take a serious look at moving to Linux and Passenger. The only issue there is figuring out how to get Linux/Rails to talk to SQL Server.

Unknown said...

Hi,

Interessant tutorial thanks for that ! I'm a french student and I just being involved in a new Ruby on Rails development way for a big project. I'm now well familiarized with Ruby on Rails and my application is initialized but unfortunately, the only computer I can use for that is running on windows. So I used Instant Rails for the development part but now I need to deploy the website on a Windows Apache server with MySQL, I searched a lot of informations on internet, but nothing helped me. Have you got informations about how I could do that ? Or something else ?

Thanks a lot in advance for any help !