Ruby on Rails deployment

From Smith family

Jump to: navigation, search

Various different ways to deploy a Ruby on Rails application. Assumes the server is set up and the databases are created.

Contents

Bare bones deployment: Apache2 and mod_ruby, application at root of virtual host

This is a very basic installation simply to check that Rails is running properly. See some config files for Apache and some more Apache2 config files.

  • Create a directory to contain the application
root@server:~# mkdir /var/www/project.domain.tld
root@server:~# cd /var/www/project.domain.tld
  • Download the Rails app from the Suvbersion repository
root@server:/var/www/project.domain.tld# svn checkout svn://svn.server.tld/path/to/app/
Note that this will insert the last directory into the checked-out path, so the Rails root will be in /var/www/project.domain.tld/app/
  • Update config/database.yml
production:
  adapter: mysql
  encoding: utf8
  database: project_production
  pool: 5
  username: project
  password: SecretPassword
  socket: /var/run/mysqld/mysqld.sock
  • Uncomment this line in config/environment.yml
ENV['RAILS_ENV'] ||= 'production'
  • Create the database and its user
root@server:~# mysql -u root -p
mysql> create database project_production;
mysql> create user 'project'@'localhost' identified by 'SecretPassword';
mysql> grant all on project_production.* to 'project'@'localhost';
  • Run the database migration to create the tables and populate them
root@server:/var/www/project.domain.tld# rake db:migrate RAILS_ENV=production
  • Create the Apache2 configuration file, /etc/apache2/sites-available/project.domain.tld
<VirtualHost *:80>
       ServerAdmin webmaster@localhost

       DocumentRoot /var/www/project./deployment-20090209/public
       ServerName depot.njae.me.uk

       <Directory />
               Options FollowSymLinks
               AllowOverride None
       </Directory>

       <Directory /var/www/>
               Options FollowSymLinks MultiViews
               AllowOverride None
               Order allow,deny
               allow from all
       </Directory>

       <Directory /var/www/depot.njae.me.uk/deployment-20090209/public>
               # General Apache options
               Options +FollowSymLinks +ExecCGI
               AllowOverride None
               Order allow,deny
               allow from all

               #AddHandler fastcgi-script .fcgi
              AddHandler cgi-script .cgi
       </Directory>

       RewriteEngine On
       # RewriteRule ^$ index.html [QSA]
       RewriteRule ^([^.]+)$ $1.html [QSA]
       RewriteCond %{REQUEST_FILENAME} !-f
       RewriteRule ^(.*)$ dispatch.cgi [QSA,L]

       # In case Rails experiences terminal errors
       ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"

       # this not only blocks access to .svn directories, but makes it appear
       # as though they aren't even there, not just that they are forbidden
       <DirectoryMatch "^/.*/\.svn/">
           ErrorDocument 403 /404.html
           Order allow,deny
           Deny from all
           Satisfy All
       </DirectoryMatch>

       ErrorLog /var/log/apache2/error.log

       LogLevel warn

       CustomLog /var/log/apache2/access.log combined
       ServerSignature On
</VirtualHost>
  • Enable the site and reload it into Apache
root@server:~# a2ensite project.domain.tld
root@server:~# /etc/init.d/apache2 reload

Initial deployment: Capistrano, Apache2, mod_ruby, application at root of virtual host

Source instructions, Git + Passenger

This assumes the app will be deployed to the root of a virtual host all of its own.

  • Copy the project to a stable branch, for deployment.
user@desktop:~/project$ svn copy -m "Creating stable branch" http://svn.domain.tld/svn/repo/project/trunk http://svn.domain.tld/svn/repo/project/branches/stable
(note the absence of trailing '/' characters in the SVN command)
  • Capify the project:
user@desktop:~/project$ capify . 
  • Modify project/config/deploy.rb:
set :application, "project"
set :repository,  "http://svn.domain.tld/svn/repo/#{application}/branches/stable"

set :deploy_to, "/var/www/project.domain.tld/"

set :scm, :subversion
set :scm_username, 'developer_user'

role :app, "project.domain.tld"
role :web, "project.domain.tld"
role :db,  "project.domain.tld", :primary => true

# Copy the database.yml file across, as it's not kept in the SVN repository
after "deploy:update_code" , :configure_database
desc "copy database.yml into the current release path"
task :configure_database, :roles => :app do
  db_config = "#{deploy_to}/config/database.yml"
  run "cp #{db_config} #{release_path}/config/database.yml"
end

# As we're not running any other server processes (FastCGI, Mongrel, etc.), 
# we don't need to start and stop them.
namespace :deploy do
  [:start, :stop, :restart].each do |t|
    desc "#{t} task is a no-op without other server processes"
    task t, :roles => :app do ; end
  end
end
Note that we instruct Capistrano to copy database.yml into the right place, as it's not in the repository. Also, as we're not running any other server processes (FastCGI, Mongrel, etc.), we don't need to start and stop them.
  • Create the production database user's password and store it in the production stanza in database.yml
  • On the server, create the /var/www/project.domain.tld directory, and make it writeable by the user:
root@server:~# mkdir /var/www/project.domain.tld
root@server:~# chown -R user:user /var/www/project.domain.tld
  • Copy across the database.yml file
 user@desktop:~/project$ ssh user@server 'mkdir /var/www/project.domain.tld/config'
 user@desktop:~/project$ scp config/database.yml \
                             user@server:/var/www/project.domain.tld/config/database.yml                                
  • Create the production database on the server
root@server:~# mysql -u root -p
mysql> create database project_production;
mysql> grant all on project_production.* to 'project'@'localhost' identified by 'password';
mysql> quit;
  • On the server, create the /etc/apache2/sites-available/project.domain.tld file:
<VirtualHost *:80>
       ServerAdmin webmaster@localhost

       DocumentRoot /var/www/project.domain.tld/current/public
       ServerName project.domain.tld

       SetEnv RAILS_ENV production

       <Directory />
               Options FollowSymLinks
               AllowOverride None
       </Directory>

       <Directory /var/www/>
               Options FollowSymLinks MultiViews
               AllowOverride None
               Order allow,deny
               allow from all
       </Directory>

       <Directory /var/www/project.domain.tld/current/public>
               AllowOverride None
               Order allow,deny
               allow from all

               # General Apache options
               #AddHandler fastcgi-script .fcgi
               AddHandler cgi-script .cgi
               #AddHandler fcgid-script .fcgi
               Options +FollowSymLinks +ExecCGI
       </Directory>

       RewriteEngine On

       # If the maintenance page exists, rewrite all requests to that page
       RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
       RewriteCond %{SCRIPT_FILENAME} !maintenance.html
       RewriteRule ^.*$ /system/maintenance.html [L]

       RewriteRule ^([^.]+)$ $1.html [QSA]
       RewriteCond %{REQUEST_FILENAME} !-f
       RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
       # RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]

       ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"
 
       # this not only blocks access to .svn directories, but makes it appear
       # as though they aren't even there, not just that they are forbidden
       <DirectoryMatch "^/.*/\.svn/">
           ErrorDocument 403 /404.html
           Order allow,deny
           Deny from all
           Satisfy All
       </DirectoryMatch>

       ErrorLog /var/log/apache2/error.log

       # Possible values include: debug, info, notice, warn, error, crit,
       # alert, emerg.
       LogLevel warn

       CustomLog /var/log/apache2/access.log combined
       ServerSignature On
</VirtualHost>
Note the SetEnv directive to ensure this is a production system.
  • Set up and check the deployment
user@desktop:~/project$ cap deploy:setup
user@desktop:~/project$ cap deploy:check
Fix any reported errors
  • Enable the site and restart Apache
root@server:~# /etc/init.d/a2ensite project.domain.tld
root@server:~# /etc/init.d/apache2 reload
  • Make the first deployment
user@desktop:~/project$ cap deploy:migirations

Apache2 and Mongrel cluster, deployed with Capistrano, application at root of virtual host

Get Mongrel running

  • For testing, open ports in the firewall to allow direct access to the Mongrel cluster. Modify /etc/iptables.rules to include these lines near the end:
## Temporarily allow Mongrel clusters across the LAN
iptables -A INPUT  -i $IFACE -p tcp -s $LAN --dport 3000 -j ACCEPT
iptables -A INPUT  -i $IFACE -p tcp -s $LAN --dport 3001 -j ACCEPT
iptables -A INPUT  -i $IFACE -p tcp -s $LAN --dport 3002 -j ACCEPT
and restart the firewall
root@server:~# /etc/init.d/iptables restart
  • Deploy the application to the server, for instance using Capistrano (as above).
  • On the server, cd to the root of the Rails application and set up the Mongrel cluster
user@server:~$ cd /var/www/project.domain.tld/current
user@server:current$ mongrel_rails cluster::configure -e production -p 3000 -N 3
Writing configuration file to config/mongrel_cluster.yml.
  • Modify the file config/mongrel_cluster.yml
---
cwd : /var/www/depot.njae.me.uk/current
log_file: log/mongrel.log
port: "3000"
environment: production
pid_file: tmp/pids/mongrel.pid
servers: 3
docroot: public
user: bob
group: bob
the cwd, docroot, user, and group elements are new. Bob is the name of a non-privileged user.
  • Start the cluster
user@server:current$ mongrel_rails cluster::start
starting port 3000
starting port 3001
starting port 3002
  • Test the Mongrel cluster by pointing a web browser at server.domain.tld:3000, server.domain.tld:3001, and server.domain.tld:3002

Make Apache a proxy for Mongrel

Now to configure Apache to act as a load-balancing proxy

  • Enable the modules Apache will need
root@server:~# a2enmod proxy_balancer
root@server:~# a2enmod proxy_http
root@server:~# a2enmod rewrite
  • Edit the /etc/apache2/sites-enabled/project.domain.tld file
<VirtualHost *:80>
       ServerAdmin webmaster@localhost

       DocumentRoot /var/www/project.domain.tld/current/public
       ServerName project.domain.tld

       SetEnv RAILS_ENV production

       <Directory />
               Options FollowSymLinks
               AllowOverride None
       </Directory>

       <Directory /var/www/>
               Options FollowSymLinks MultiViews
               AllowOverride None
               Order allow,deny
               allow from all
       </Directory>

       <Directory /var/www/depot.njae.me.uk/current/public>
               # General Apache options
               Options +FollowSymLinks +ExecCGI
               AllowOverride None
               Order allow,deny
               allow from all
       </Directory>

       <Proxy *>
               Order allow,deny
               Allow from all
       </Proxy>

       <Proxy balancer://mongrel_cluster>
           BalancerMember http://127.0.0.1:3000
           BalancerMember http://127.0.0.1:3001
           BalancerMember http://127.0.0.1:3002
       </Proxy>

       RewriteEngine On

       # If the system maintenance page exists, serve that instead of any other page
       RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
       RewriteCond %{SCRIPT_FILENAME} !maintenance.html
       RewriteRule ^.*$ /system/maintenance.html [L]

       # Rewrite rule to check for the index page: Apache serves this directly
       RewriteRule ^/$ /index.html [QSA]

       # Rewrite rule for static pages: Apache serves these direclty
       RewriteRule ^([^.]+)$ $1.html [QSA]

       # If no other rules match, pass the request to the Mongrel cluster
       # Redirect all non-static requests to cluster
       RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
       RewriteRule ^/(.*)$ balancer://mongrel_cluster%{REQUEST_URI} [P,QSA,L]

       ## Deflate served pages to improve speed over the network
       #AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/javascript text/css
       #BrowserMatch ^Mozilla/4 gzip-only-text/html
       #BrowserMatch ^Mozilla/4\.0[678] no-gzip
       #BrowserMatch \\bMSIE !no-gzip !gzip-only-text/html

       ## Uncomment for deflate debugging
       #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/project_deflate.log deflate

       # In case Rails experiences terminal errors
       # Instead of displaying this message you can supply a file here which will be rendered instead
       ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"

       # this not only blocks access to .svn directories, but makes it appear
       # as though they aren't even there, not just that they are forbidden
       <DirectoryMatch "^/.*/\.svn/">
           ErrorDocument 403 /404.html
           Order allow,deny
           Deny from all
           Satisfy All
       </DirectoryMatch>

       ErrorLog /var/log/apache2/error.log

       # Possible values include: debug, info, notice, warn, error, crit,
       # alert, emerg.
       LogLevel warn

       CustomLog /var/log/apache2/access.log combined
       ServerSignature On
</VirtualHost>
(if you use the Deflate module, remember to enable it (a2enmod deflate) and create an empty log file (touch /var/log/apache2/project_deflate.log) before restarting Apache.
  • Reload the Apache configuration
root@server:~#  /etc/init.d/apache2 force-reload
  • Close the Mongrel ports to the outside world: delete these lines from /etc/iptables.rules:
## Temporarily allow Mongrel clusters across the LAN
iptables -A INPUT  -i $IFACE -p tcp -s $LAN --dport 3000 -j ACCEPT
iptables -A INPUT  -i $IFACE -p tcp -s $LAN --dport 3001 -j ACCEPT
iptables -A INPUT  -i $IFACE -p tcp -s $LAN --dport 3002 -j ACCEPT
and restart the firewall
root@server:~# /etc/init.d/iptables restart

Update the project's capfile

See the Deploying Rails Applications book for details on using Monit to start and stop Mongrel clusters. If you're running Mongrel cluster as a service outside Monit, you'll need to include some modifications to config/deploy.rb. Create the custom start, stop, and restart tasks as shown below:

# Custom tasks for starting and restarting Mongrel cluster
namespace :deploy do
  desc "start the mongrel cluster"
  task :start, :roles => :app do
    sudo "/usr/bin/mongrel_cluster_ctl start"
  end

  desc "stop the mongrel cluster"
  task :stop, :roles => :app do
    sudo "/usr/bin/mongrel_cluster_ctl stop"
  end

  desc "restart the mongrel cluster"
  task :restart, :roles => :app do
    sudo "/usr/bin/mongrel_cluster_ctl restart"
  end
end

Make Mongrel run as a service

  • Create the file /etc/mongrel/project.conf
# The user and group which run Mongrel
user: bob
group: bob

# The location of the Rails application and the environment to run in
cwd: /var/www/depot.njae.me.uk/current
environment: production

# The number of Mongrels in the cluster
servers: 3

# The starting port
port: "3000"

# The IP addresses allowed to connect to Mongrel
address: 127.0.0.1

# The location of the process ID files relative to the directory above
pid_file: tmp/pids/mongrel.pid
  • Stop the existing Mongrel cluster and restart it with this new config file
user@server:current$ mongrel_rails cluster::start
user@server:current$ mongrel_cluster_ctl start
  • Remove the file config/mongrel_cluster.yml
  • Check you can still use the application. This checks that the config file is correct.
  • Stop the Mongrel cluster:
user@server:current$ mongrel_cluster_ctl stop
  • Copy the Mongrel init.d script across
root@server:~# cp /usr/lib/ruby/gems/1.8/gems/mongrel_cluster-1.0.5/resources/mongrel_cluster /etc/init.d/
  • Modify the /etc/init.d/mongrel_cluster script to remove the reference to the non-existent mongrel user. Replace the reference to one to root. Chanage the lines near the top of the script to look like this:
# USER=mongrel
USER=root
  • Set up the service and start it
root@server:~# chmod +x /etc/init.d/mongrel_cluster
root@server:~# update-rc.d mongrel_cluster defaults

If you want to stop the Mongrel cluster running as a service, remove the init scripts:

root@server:~# rm /etc/init.d/mongrel_cluster
root@server:~# update-rc.d mongrel_cluster remove

The Deploying Rails Applications book goes into more detail about using Monit to monitor the Mongrel cluster and restart Mongrel instances if any of them get into trouble. I've not got round to doing that yet. I will do if I decide to use Mongrel as my main production server.

Apache2 and Phusion Passenger, deployed with Capistrano, application at root of virtual host

Passenger is an application server for Rails that is run via Apache. The Passenger application server spawns a new process when it's needed and keeps it running for a while. After about 10 minutes without use, the application server process terminates to save system resources. When the next Rails request comes along, Apache spawns a new Passenger process to handle it.

The nice thing is that the configuration is just about non-existent.

This assumes the app will be deployed to the root of a virtual host all of its own.

  • Copy the project to a stable branch, for deployment.
user@desktop:~/project$ svn copy -m "Creating stable branch" http://svn.domain.tld/svn/repo/project/trunk http://svn.domain.tld/svn/repo/project/branches/stable
(note the absence of trailing '/' characters in the SVN command)
  • Capify the project:
user@desktop:~/project$ capify . 
  • On the server, install the Passenger gem and install it:
root@desktop:~# gem install passenger
root@desktop:~# passenger-install-apache2-module   
  • Add these lines to /etc/apache2/httpd.conf
# Load the Passenger module for Rails applications
LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-2.0.6/ext/apache2/mod_passenger.so
PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-2.0.6
PassengerRuby /usr/bin/ruby1.8
  • On the server, create the /var/www/project.domain.tld directory, and make it writeable by the user:
root@server:~# mkdir /var/www/project.domain.tld
root@server:~# chown -R user:user /var/www/project.domain.tld
Note that Passenger runs as the user who owns the config/environment.rb file, so ensure that user has adequate rights to this bit of the file system.
  • Copy across the database.yml file
user@desktop:~/project$ ssh user@server 'mkdir /var/www/project.domain.tld/config'
user@desktop:~/project$ scp config/database.yml \
                            user@server:/var/www/project.domain.tld/config/database.yml                                
  • Create the production database on the server
root@server:~# mysql -u root -p
mysql> create database project_production;
mysql> grant all on project_production.* to 'project'@'localhost' identified by 'password';
mysql> quit;
  • Create the virtual host configuration file, /etc/apache2/sites-available/project.domain.tld containing just this:
  <VirtualHost *:80>
     ServerName project.domain.tld
     DocumentRoot /var/www/project.domain.tld/current/public

     RewriteEngine On
     # If the system maintenance page exists, serve that instead of any other page
     RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
     RewriteCond %{SCRIPT_FILENAME} !maintenance.html
     RewriteRule ^.*$ /system/maintenance.html [L]

     # In case Rails experiences terminal errors
     # Instead of displaying this message you can supply a file here which will be rendered instead
     ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"

     # this not only blocks access to .svn directories, but makes it appear
     # as though they aren't even there, not just that they are forbidden
     <DirectoryMatch "^/.*/\.svn/">
       ErrorDocument 403 /404.html
       Order allow,deny
       Deny from all
       Satisfy All
     </DirectoryMatch>

     ErrorLog /var/log/apache2/error.log

     # Possible values include: debug, info, notice, warn, error, crit,
     # alert, emerg.
     LogLevel warn

     CustomLog /var/log/apache2/access.log combined
     ServerSignature On
</VirtualHost>
  • Enable the site and restart Apache
root@server:~# /etc/init.d/a2ensite project.domain.tld
root@server:~# /etc/init.d/apache2 reload
  • Set up and check the deployment
user@desktop:~/project$ cap deploy:setup
user@desktop:~/project$ cap deploy:check
  • For deployment, you need to modify the Capistrano deployment recipe, project/config/deploy.rb. Update the :start. :stop. and :restart tasks to be as shown:
set :application, "project"
set :repository,  "http://svn.domain.tld/svn/repo/#{application}/branches/stable"

set :deploy_to, "/var/www/project.domain.tld/"

set :scm, :subversion
set :scm_username, 'developer_user'

role :app, "project.domain.tld"
role :web, "project.domain.tld"
role :db,  "project.domain.tld", :primary => true

# Copy the database.yml file across, as it's not kept in the SVN repository
after "deploy:update_code" , :configure_database
desc "copy database.yml into the current release path"
task :configure_database, :roles => :app do
  db_config = "#{deploy_to}/config/database.yml"
  run "cp #{db_config} #{release_path}/config/database.yml"
end

namespace :deploy do
  task :start, :roles => :app do
  end

  task :stop, :roles => :app do
  end

  desc "Restart Application"
  task :restart, :roles => :app do
    run "touch #{release_path}/tmp/restart.txt"
  end
end
  • Make the first deployment
user@desktop:~/project$ cap deploy:migirations

Subsequent deployments with Capistrano from a stable branch

Assume a bunch of enhancements have been made in the trunk. These need to be copied to the stable branch in Subversion and redeployed.

  • Check out a working copy of the stable branch
user@desktop:~$ mkdir -p /path/to/stable/branch/working/copy
user@desktop:~$ cd /path/to/stable/branch/working/copy
user@desktop:copy$ svn checkout uri://repository/app/branches/stable
user@desktop:copy$ cd stable
  • Find the latest revision number of the stable branch
user@desktop:stable$ svn log
note revision number of last commit. Assume its revision 30.
  • Find the latest revision number of the trunk. Either look at the repository, or update the trunk and look at the log
user@desktop:trunk$ svn update
user@desktop:trunk$ svn log
Note the different directory: do this in a different console. Assume the trunk is at revision 45.
  • Merge the changed from the trunk into the stable branch
user@desktop:stable$ svn merge -r 30:45 uri://repository/app/trunk
  • Commit the updated stable branch to the repository
user@desktop:stable$ svn commit -m "Merged updates from trunk into stable branch"                                                     
  • Redeploy the application
user@desktop:stable$ cap deploy:migrations

See also

Personal tools