Originally published 2010-11-27
One of the major reasons I wrote my own custom blogware was to gain the complete experience of developing a web application with Rails 3, from starting with an empty directory for source code to deploying the application to the public Internet. This article summarizes my experience deploying my blog and serves as a tutorial for similar deployments.
Choices
Before I could actually deploy my blog, I had to make a few choices about how I would deploy it.
Source Control
I am using Git. Deployment is easiest when source code is contained in a Git repository accessible via the Internet. Therefore I used GitHub.
Managed Hosting vs. Virtual Private Slice
This decision was a no brainer. While I have heard good things about managed hosting services such as Heroku or Engine Yard, with whom all you have to do is git push your code and they take care of the rest, one of my goals for deployment was to gain experience, so the managed hosting services were out.
On the other hand, servers are expensive. Leasing my own dedicated server would be prohibitively expensive. Cheap hosting that gives you a sandbox on a server would limit my ability to experiment with the configuration of my deployment.
Fortunately, this is 2010, and there is an abundance of virtual private slice (VPS) hosts available. A VPS is a simulated dedicated server that uses virtual machine technology to give several users the appearance of having independent servers while sharing the same physical server. Some companies, such as VMware, have made their whole business on virtualization software. The VPS hosts that offer Linux servers with Internet connections typically use the open source Xen hypervisor software.
Linode and Slicehost were VPS services that came recommended from the official Ruby on Rails web site. I went with Linode since they were about half the price of Slicehost. Since a blog is a simple application, I went with their smallest VPS, a measly 512 MB of memory, for only about 20 USD a month.
Domain Name Registrar
Ever since the early days of the Internet, selecting a domain name registrar has been thorny territory. There are many of them, they are all competing to do basically the same thing, and for the majority of webmasters, interaction with them is minimal. Some registars, such as GoDaddy, have muscled their way into the public consciousness with aggressive marketing, which isn’t my style.
Beyond just price, I had one further consideration. The Internet Corporation for Assigned Names and Numbers (ICANN), which governs domain names, requires that a physical address be provided when a domain name is registered. That is fine, except that the physical address will be publicly available in WHOIS domain queries. If you are a business with an office location, this is no big deal. However, you don’t have to be a privacy nut for the idea of anyone being able to find out your home address anonymously over the Internet to make you worry a bit.
Fortunately, many registrars offer some sort of privacy service for an extra fee in which they will be the physical addressee of your domain names. I made sure to search amongst registrars that offered such a service.
After shopping around a bit, I went with Moniker, since they had the best prices. I registered a .info domain name and a .com domain name, both with their physical address, for a year for about 21 USD.
Apache vs. nginx
The Apache HTTPD web server is by far the most popular web server used on the Internet. The nginx web server (pronounced “engine ‘X’”) is one of the new kids on the block. I’m going to make this short since I don’t want to add to the verbosity on the subject that I found when I googled it: I went with nginx.
Metrics report that nginx outperforms Apache. Apache is far more prolific, and pretty much everything you can ever imagine doing with a web server someone has done with Apache. Therefore there are more libraries and such for Apache. Since my use case does not involve doing anything remotely exotic, I went with nginx. Also, nginx configuration is slightly less arcane, so that is a plus as well.
Linux Distribution
Again, I shall keep this sort since I don’t want to add to the verbosity on the subject. I went with Ubuntu. It is based on the Debian distribution, which is one of the “purest” Linux distributions. Ubuntu has a more frequent life cycle and is thus better supported. Also, Ubuntu is popular, which makes it a network good: whatever you want to do with it, odds are someone else has done it before and blogged about it.
Database
I am a typically a PostgreSQL guy. However, database management systems (DBMS) tend to be resource intensive, and I was trying to do this on the cheap as much as possible. Since there really isn’t much data in a blog application, I just went with SQLite, and I am able to squeeze my entire application setup onto a mere 512 MB slice quite easily.
Step by Step Deployment
It turns out that deployment from GitHub of a Rails 3 application using SQLite being served by nginx on an Ubuntu VPS is quite easy. (Still, I managed to hose my server the first time around and I had to start over from a fresh install.)
Installing and Booting VPS and Setting Up DNS
This is dependent on your VPS provider and domain name registrar. See their documentation for details. With Linode and Moniker, everything was through web user interfaces and was quite easy. A few clicks here and there and I was ready to go.
Using Public Key Authentication
When installing my Linux distribution (Ubuntu 10.04 LTS) on my VPS, I was prompted to select a root password. Once booted up, I SSH’ed in with my root password. (Replace “joshuaborn.info” below with your domain name.)
ssh root@joshuaborn.info
# Provide password when prompted.
The first thing I did after SSH’ing into my server with my root password was make it so that I couldn’t SSH with my root password anymore. Instead, I setup public key authentication. If you’re using GitHub, you probably have personal SSH keys setup already. All you have to do is put your public SSH key on your server and disable authentication with passwords.
mkdir ~/.ssh
vim ~/.ssh/authorized_keys
# Paste in your public key into Vim and :wq to write and quit.
vim /etc/ssh/ssh_config
# Change "PasswordAuthentication yes" to "PasswordAuthentication no" and uncomment the line by deleting the '#'. Write and quit.
Now exit to log out and SSH back to your server. You should be authenticating with your SSH keys instead of your root password now.
Setting the Hostname
echo "joshuaborn" > /etc/hostname
hostname -F /etc/hostname
(Clearly you will want to replace “joshuaborn” with your own hostname.)
Configuring /etc/hosts
127.0.0.1 localhost.localdomain localhost
173.230.131.180 joshuaborn.info joshuaborn
Write a file to /etc/hosts with the above content, again replacing “joshuaborn.info” and “joshuaborn” with your domain name and hostname respectively.
Setting the Time Zone
dpkg-reconfigure tzdata
The above command line utility makes this easy in Ubuntu.
Updating Security Patches and Retrieving Necessary Libraries
apt-get update
apt-get install build-essential libpcre3-dev libssl-dev libcurl4-openssl-dev libreadline5-dev
Installing Ruby from Source
As of the time of my deployment, Ruby 1.9.2-p0 was the latest and greatest. You will likely want to replace it in the below with whatever is the latest version of Ruby when you are doing your deployment.
cd /usr/local/src
wget ftp://ftp.ruby-lang.org//pub/ruby/1.9/ruby-1.9.2-p0.tar.gz
tar -xvzf ruby-1.9.2-p0.tar.gz
rm ruby-1.9.2-p0.tar.gz
cd ruby-1.9.2-p0
./configure --prefix=/usr/local/ruby-1.9.2-p0
make && make install
ln -s /usr/local/ruby-1.9.2-p0/ /usr/local/ruby
vim /etc/environment
# Add "/usr/local/ruby/bin:" to the front of the path and write and quit.
I like to install into a sandbox to make changing Ruby versions less painful.
Installing Passenger and nginx
Phusion Passenger has become the preeminent way to run Rails applications in production. It makes Rails deployments a breeze.
gem install passenger --no-ri --no-rdoc
passenger-install-nginx-module
# Follow the prompts and use the defaults.
Setting up an Init Script for nginx
The Linode Library was so helpful as to include an init script for nginx deployments. Running nginx from an init script makes sense since if your server is rebooted unexpectedly, you will likely want nginx to start back up so that your web application is interrupted for as little time as possible.
wget http://library.linode.com/web-servers/nginx/installation/reference/init-deb.sh
mv init-deb.sh /etc/init.d/nginx
chmod +x /etc/init.d/nginx
/usr/sbin/update-rc.d -f nginx defaults
Creating a User to Run Passenger
This was my biggest gotcha during deployment. It turns out Passenger does not like to run as root. Typically Passenger runs as whoever the owner of your environment.rb file of your application is. However, if this is root, then it will run as an unexpected user and you will get puzzling permission errors all over the place. The moral of the story is that you should create a user to run your application, and you should deploy your code as this user (or at least chown your code over to this user after deployment) so that Passenger knows to run as this user. I called my user “passenger” since I am creative like that.
useradd -d /home/passenger -m passenger
chown -R passenger:passenger /usr/local/ruby/
su - passenger
ssh-keygen -t dsa
cat .ssh/id_dsa.pub
# Add this public key to your Git repository.
exit
cp /root/.ssh/authorized_keys /home/passenger/.ssh/
chown passenger:passenger /home/passenger/.ssh/authorized_keys
mkdir /u
chown passenger:passenger /u
exit
The /u directory is the default location that Capistrano uses to deploy your code. If you are going to deploy to /var/www or somewhere else, then change it above appropriately.
Installing Git
apt-get install git-core
su - passenger
git clone -q git@github.com:joshuaborn/bornography.git temp/
rm -rf temp
exit
Clearly you will want to replace the path above with the path to your repository.
Installing the Database Management System
apt-get install sqlite3 libsqlite3-dev
SQLite basically removes DBMS setup from our concern.
Installing Bundler
gem install bundler --no-ri --no-rdoc
Configuring Capistrano
Locally, install the Capistrano gem and Capify your project if you haven’t already. Eventually the default Capistrano recipes that ship with the gem will catch up to the state of the art in Rails deployment, including Passenger, which makes the restart task obsolete, and Bundler, which is integrated into Rails 3. In the mean time, here is my deploy.rb file, with modifications for Passenger and Bundler, for reference.
set :application, "bornography" set :repository, "git@github.com:joshuaborn/bornography.git" set :scm, :git set :user, "passenger" set :use_sudo, falserole :app, "joshuaborn.info" role :web, "joshuaborn.info" role :db, "joshuaborn.info", :primary => truenamespace :deploy do task :start, :roles => :app do run "touch #{current_release}/tmp/restart.txt" endtask :stop, :roles => :app do # Do nothing. enddesc "Restart Application" task :restart, :roles => :app do run "touch #{current_release}/tmp/restart.txt" end endnamespace :bundler do task :create_symlink, :roles => :app do shared_dir = File.join(shared_path, 'bundle') release_dir = File.join(current_release, '.bundle') run("mkdir -p #{shared_dir} && ln -s #{shared_dir} #{release_dir}") endtask :bundle_new_release, :roles => :app do bundler.create_symlink run "cd #{release_path} && bundle install --without test" end endafter 'deploy:update_code', 'bundler:bundle_new_release'
You will want to replace the application name, domain name, possibly the user name, etc, as appropriate.
Once you have the correct recipes going, it is time to deploy your application to your server. Run the below locally in your Rails root.
cap deploy:setup
cap deploy:check
cap deploy
Configuring the Database
Rails stores its SQLite database files in the db directory off the Rails root by default. Unless you want to wipe out your data and start over on every deploy, you will have to reconfigure your database file to go into a shared location. I added the below to my config/database.yml file.
production:
adapter: sqlite3
database: /u/apps/bornography/shared/db/production.sqlite3
pool: 5
timeout: 5000
Back on the server, I ran the below commands.
mkdir /u/apps/bornography/db
cd /u/apps/bornography/current
rake db:migrate RAILS_ENV=production
chown -R passenger:passenger /u
Configuring nginx
Below is my /opt/nginx/conf/nginx.conf file on my server.
user passenger;worker_processes 1;events { worker_connections 1024; }http { passenger_root /usr/local/ruby-1.9.2-p0/lib/ruby/gems/1.9.1/gems/passenger-3.0.0; passenger_ruby /usr/local/ruby-1.9.2-p0/bin/ruby;include mime.types; default_type application/octet-stream;sendfile on;keepalive_timeout 65;server { listen 80; server_name joshuaborn.info; root /u/apps/bornography/current/public; passenger_enabled on; if ($host != 'joshuaborn.info' ) { rewrite ^/(.*)$ http://joshuaborn.info/$1 permanent; } } }
Since Passenger’s worker processes were already running as the passenger user, I made nginx’s worker processes run as passenger as well. The rewrite rule is just to handle the people using the “.com” version of my domain name or prefixing the domain with the superfluous “www.”
Start nginx
All that should remain is to start everything up.
/etc/init.d/nginx start
Everything was humming, and I had my first application on the web for only about 20 USD a month and 20 USD annually.
Links
- Ruby on Rails
- GitHub
- Linode – Xen VPS Hosting
- Moniker – Domain Names
- Ruby Programming Language
- Phusion Passenger
- Capistrano Documentation Wiki