Rails setup on Virtual/Private Server

Posted by chris olsen on December 30, 2007

A while back I posted about how to set rails up on Linode. While the instructions were in the right direction, I notices a couple of errors when I was deploying a recent application. Instead of updating the previous post I figured I would start from scratch, but this time leave out a lot of the small talk and just get down and dirty. The previous article, as well as this one, may seem to be specific to Linode, but the steps should be applicable to any virtual or private server.

This is written assuming that you have already signed up to Linode and have installed the Debian 4.0 distro as well as have your project stored within a subversion repository.

SSH into the server.

#linode machine
ssh root@ip_address

Server

Once logged into the server we need to ensure everything is up to date.

apt-get update
apt-get upgrade

Install some of the required libraries.

apt-get install ruby1.8-dev ruby ri irb rdoc libmysql-ruby mysql-server nginx subversion build-essential libopenssl-ruby1.8 sudo

Install RubyGems (Check here for the most recent release, although at the time of this writing 1.0.1 was out, but was causing me problems. I ended up sticking with v0.9.4).

wget http://rubyforge.org/frs/download.php/20989/rubygems-0.9.4.tgz
tar xzvf rubygems-0.9.4.tgz
cd rubygems-0.9.4
ruby setup.rb

Install Rails without all the documentation, it will save you a lot of time.

gem install rails --include-dependencies --no-rdoc --no-ri

Install mongrel and the mongrel cluster.

gem install mongrel -y
gem install mongrel_cluster -y

Create a new user (mongrel) that will be used to deploy the app.

useradd -m mongrel
su - mongrel
mkdir ~/.ssh
chmod 700 ~/.ssh
cat /root/my_id >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Grant permissions to the user, mongrel.

export VISUAL=vi
visudo

Add the following line just under where root is granted all permissions.

mongrel ALL=(ALL) NOPASSWD:ALL

Create the folder where the project will be deployed to and set the proper permissions.

mkdir /var/rails
chown root:mongrel /var/rails
chmod g+w /var/rails

Local Machine

If you don’ have an ssh key created perform the following:

ssh-keygen -t rsa
scp ~/.ssh/id_rsa.pub root@ip_address:~/my_id

Ensure Capistrano is installed.

cap --version

If you see some output like $ Capistrano v2.x.x then all is good if not run the following:

sudo gem install capistrano -y

Capify your project.

cd folder_of_your_project
capify .

Next we need to create a file named “spin” in the script folder.

vi script/spin

Then insert the following and save it

mongrel_rails cluster::start

Let’s now add the new file to the project and give make it executable.

svn add config/spin
svn propset svn:executable on config/spin

Before deploying we have to make sure that the repository is fully up to date, so let’s check on the status, add any files that files that have not yet been added, then commit.

svn status
svn add
svn ci -m "Ready to deploy"

There are some changes that have to be made to the nginx.conf file and Bryan was kind enough to provide them to us. The following code also contains updates that I inserted. I will talk more about these later.

set :application, "my_app_name"

set :deploy_to, "/var/rails/#{application}"
set :user, "mongrel"
set :runner, "mongrel"

role :app, "liXX-XXX.members.linode.com"
role :web, "liXX-XXX.members.linode.com"
role :db,  "liXX-XXX.members.linode.com", :primary => true

set :svn_user, ENV['svn_user'] || "your_svn_user_name"
set :svn_password, Proc.new { Capistrano::CLI.password_prompt('SVN Password: ') }
set :repository,
Proc.new { "--username #{svn_user} " +
           "--password #{svn_password} " +
           "--no-auth-cache " +
           "http://your_svn_repos_project_path" }

namespace :init do
  desc "create database.yml"
  task :database_yml do
    set :db_user, Capistrano::CLI.ui.ask("database user: ")
    set :db_pass, Capistrano::CLI.password_prompt("database password: ")
    database_configuration =<<-EOF
---
login: &login
  adapter: mysql
  database: #{application}
  host: localhost
  username: #{db_user}
  password: #{db_pass}

production:
  <<: *login

EOF

    run "mkdir -p #{shared_path}/config"
    put database_configuration, "#{shared_path}/config/database.yml"
  end

  desc "create mongrel_cluster.yml"
  task :mongrel_cluster_yml do
    mongrel_cluster_configuration = <<-EOF
---
user: mongrel
cwd: #{current_path}
log_file: #{current_path}/log/mongrel.log
port: "8000"
environment: production
group: mongrel
address: 127.0.0.1
pid_file: #{current_path}/tmp/pids/mongrel.pid
servers: 3
EOF

    run "mkdir -p #{shared_path}/config"
    put mongrel_cluster_configuration, "#{shared_path}/config/mongrel_cluster.yml"
  end
end

namespace :localize do
  desc "copy shared configurations to current"
  task :copy_shared_configurations, :roles => [:app] do
    %w[mongrel_cluster.yml database.yml].each do |f|
      run "ln -nsf #{shared_path}/config/#{f} #{current_path}/config/#{f}"
    end
  end

  desc "Sync the uploads to the new current project"
  task :sync_uploads, :roles => [:app] do
    run "mkdir -p #{shared_path}/uploads"
    run "chown #{user}:#{user} #{shared_path}/uploads/"
    run "ln -nsf #{shared_path}/uploads #{current_path}/public/uploads"
  end
end

namespace :deploy do
  desc "stop mongrel cluster"
  task :stop do
    run "cd #{current_path};mongrel_rails cluster::stop"
  end

  desc "restart mongrel cluster"
  task :restart do
    run "cd #{current_path};mongrel_rails cluster::restart"
  end

end

after "deploy:setup", "init:database_yml"
after "deploy:setup", "init:mongrel_cluster_yml"
after "deploy:symlink", "localize:copy_shared_configurations"
after "deploy:symlink", "localize:sync_uploads"

There are some changes that have to be made to the nginx.conf file and Brian was kind enough to provide them to us. To make things easier for every one the nginx.conf settings are shown below.

user mongrel mongrel;
worker_processes 6;

error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

events {
	worker_connections 1024;
}

http {
	include /etc/nginx/mime.types;
	default_type application/octect-stream;

	log_format main '$remote_addr - $remote_user [$time_local] $request '
	'"$status" $body_bytes_sent "$http_referer" '
	'"$http_user_agent" "$http_x_forwarded_for"';

	access_log /var/log/nginx/access.log;

	sendfile on;
	tcp_nopush on;
	tcp_nodelay off;
	gzip on;
	gzip_http_version 1.0;
	gzip_comp_level 2;
	gzip_proxied any;
	gzip_types text/plain text/html text/css application/x-javascript text/xml application.xml application/xml+rss text/javascript;

	# define your mongrel cluster here
	upstream mongrel_cluster {
		server 127.0.0.1:8000;
		server 127.0.0.1:8001;
		server 127.0.0.1:8002;
	}

	server {
		listen 80;
		client_max_body_size 100M;
		root /var/rails/YOUR_APPLICATION_NAME_GOES_HERE/current/public;
		access_log /var/log/nginx/commuitycms.access.log main;

		if (-f $document_root/system/maintenance.html) {
			rewrite  ^(.*)$  /system/maintenance.html last;
			break;
		}

		location / {
			proxy_set_header X-Real-IP $remote_addr;
			proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
			proxy_set_header Host $http_host;
			proxy_redirect false;
			proxy_max_temp_file_size 0;

			if (-f $request_filename) {
				break;
			}

			if (-f $request_filename/index.html) {
				rewrite (.*) $1/index.html break;
			}

			if (-f $request_filename.html) {
				rewrite (.*) $1.html break;
			}

			if (!-f $request_filename) {
				proxy_pass http://mongrel_cluster;
				break;
			}
		}

		error_page 500 502 503 504 /500.html;
    location = /500.html {
      root /var/rails/tissue_bank_trunk;
    }
	}
}

Copy the file up to the server from your local machine.

scp ~/Desktop/smarticast_railsrumble/nginx.conf ip_address:~/nginx.conf

Server

Then backup the original version and move the new version in to replace the previous.

su - mongrel   #unless you are already logged in as mongrel
mv /etc/nginx/nginx.conf /etc/nginx/nginx.conf.old
mv ~/nginx.conf /etc/nginx/nginx.conf

Update the nginx.conf file server { root /var/rails/project_name/current/public } value to contain the name of your project. Then restart the server.

/etc/init.d/nginx restart

While we are on the server let’s create the database.

mysqladmin -u root -p create database_name

The last thing that has to be done is to ensure that the mongrel cluster will be started if the server is rebooted.

mkdir /etc/mongrel_cluster
sudo ln -s /var/rails/my_app/current/config/mongrel_cluster.yml /etc/mongrel_cluster/my_app.yml
cp /usr/lib/ruby/gems/1.8/gems/mongrel_cluster-1.0.2/resources/mongrel_cluster /etc/init.d
sudo chmod 755 /etc/init.d/mongrel_cluster
sudo /usr/sbin/update-rc.d -f mongrel_cluster defaults

It is now time to reboot the server, and once it starts back up if the site is accessible that is a good thing. If errors are being thrown the best place to find the problem is by checking out the log files on the server.

Local

Time for the deployment.

cap deploy:setup

If all goes well on the setup let’s deploy.

deploy:cold

All code updates from here on just have to be checked into the subversion repository by one of the following:

cap deploy

That is it. Time for a celebratory beer.

Additional Notes

To retain uploaded files upon later deployments you will have to place these files in a folder that exists outside your project and create a sym link to the folder from within your project. That is what the following code is doing:

desc “Sync the uploads to the new current project”
task :sync_uploads, :roles => [:app] do
  run “mkdir -p #{shared_path}/uploads”
  run “chown #{user}:#{user} #{shared_path}/uploads/”
  run “ln -nsf #{shared_path}/uploads #{current_path}/public/uploads”
end
...
after “deploy:symlink”, “localize:sync_uploads”

*** Read update below before installing RMagick ***
Many of you will also need an image library installed to allow you resize images on upload. Although there are a few libraries out there I have had the most success with RMagick. Some people may argue that it is a little bloated, but I can always get it to work. If I ever write an amazing web app that has so much traffic that the only way I can keep the site alive is to use a lighter library then I will do exactly that, but until then I will stick with RMagick.

To install it you will need to ensure that you have a number of required libraries installed first.

sudo apt-get install build-essential libc6-dev libmagick9 libmagick++9-dev libmagic9-dev

Then install RMagick

sudo gem install Rmagick

*** UPDATE ***
I take back what I said about successfully installing an image library. Mini-Magick is super easy to install and is carries a much lighter load. I was a little amazed, and happy, at the amount of memory that was freed up.

Install it with the following

sudo gem install mini_magick

Now update the image processor that attachment_fu will use by adding the following as a parameter to the has_attachement call

:processor => :MiniMagick

*** END OF UPDATE ***