Hello again. Today we are going to be installing Ruby (1.8.7 patchlevel 174), RubyGems (1.3.7), Ruby on Rails (2.3.8) and PostgreSQL (8.4) on Ubuntu Desktop 32-bit 10.04 (Lucid Lynx) LTS (Long-Term Support release). This runthrough assumes a clean install of Lucid, with all available updates/upgrades applied.
We are going to be more or less following the instructions found in a gist from Marc Chung, with only minor additions and corrections where appropriate. We will be using the wonderful Ruby Version Manager (rvm) to install Ruby and RubyGems.
First, let’s install the Ubuntu packages we’ll need to install rvm, Ruby and RubyGems. While we’re at it, we’ll also install our database server (PostgreSQL) and two packages (libxslt1-dev and libpq-dev) we’ll need later to build the native extensions to two of our gems (nokogiri and pg, respectively). Run the following command from a Terminal window:
sudo apt-get install curl git-core bison build-essential zlib1g-dev libssl-dev libreadline6-dev libxml2-dev autoconf libxslt1-dev libpq-dev postgresql
Type your user password when prompted. The first two packages are needed to get rvm; the rest, except for the last three, are needed to build Ruby and RubyGems.
Now, let’s download and install rvm. To accomplish this, run the following commands in order:
curl http://rvm.beginrescueend.com/releases/rvm-install-head >rvm-install-head chmod a+x rvm-install-head ./rvm-install-head
This command sequence saves a local copy of an installer script from the rvm site and runs it. The installer script, in turn, grabs rvm from a repository on GitHub and installs it.
You’ll see some messages when rvm is done installing, telling you to modify your ~/.bashrc file. I modified mine as follows. I opened up the file in pico:
I commented out line 6:
[ -z "$PS1" ] && return
I inserted a new line 7:
if [[ ! -z "$PS1" ]] ; then
I then went to the end of the file and appended these two lines:
[[ -s $HOME/.rvm/scripts/rvm ]] && source $HOME/.rvm/scripts/rvm fi
I saved the file with Ctrl-O, exited pico with Ctrl-X, and then closed and reopened my Terminal.
Install Ruby and RubyGems
Next, we’ll download and install the particular “ruby” (singular form of “rubies”) that we mentioned above, along with the RubyGems gem management software and two actual gems — Rake and RDoc. Run the following:
rvm install 1.8.7-p174 rvm use 1.8.7-p174 rvm --default 1.8.7-p174
The second command tells rvm that the ruby we just installed is, in fact, the ruby we want to use and the one that will respond when we type “ruby” on the command line. The third sets that ruby as the default, so we don’t have to type “rvm use <whatever>” every time we open a new Terminal, which is just an unnecessary step if we only have one ruby installed.
Trust, But Verify
Before we go any further, let’s verify that we have, in fact, installed the correct versions of ruby, rubygems, rake and rdoc. The following command/response sequence should demonstrate that this is true (The $ represents the prompt):
$ ruby -v ruby 1.8.7 (2009-06-12 patchlevel 174) [i686-linux] $ gem -v 1.3.7 $ gem list *** LOCAL GEMS *** rake (0.8.7) rdoc (2.5.8)
Assuming we’re all good, let’s continue.
Install Bundler and Rails
Now, we’re ready to install Bundler and then the Ruby on Rails web application development framework. What’s Bundler, you ask? Good question. It’s a system to help you manage the gems required by your applications in a shared production environment (when you have more than one application running on a single server).
So, let’s run the following:
gem install bundler gem install rails
Again, let’s verify:
$ rails -v Rails 2.3.8 $ gem list *** LOCAL GEMS *** actionmailer (2.3.8) actionpack (2.3.8) activerecord (2.3.8) activeresource (2.3.8) activesupport (2.3.8) bundler (0.9.25) rack (1.1.0) rails (2.3.8) rake (0.8.7) rdoc (2.5.8)
Theoretically, the above list shows all the gems that we’ll ever need to install locally with the “gem install” command to develop an application using Rails 2.3.8. Any and all further gems required by our app will be installed for us (locally, in production, and everywhere else our app might live) by Bundler. We’ll tell Bundler about all of these other gems in a moment.
Create Rails App, Insert Gemfile
Now, let’s set up a new default Rails app, specifying PostgreSQL as the database:
mkdir workspace cd workspace rails rails238test -d postgresql cd rails238test
At this point, we’re ready to add a Gemfile to the project. As its name might suggest, this file lists all of the gems our application needs to run — on our local development machine, on our production server, and everywhere in between. Paste the following into a new text file in gedit, and save it to your project’s root (~/workspace/rails238test) under the name “Gemfile”:
source "http://rubygems.org" gem "rails", "2.3.8" gem "pg", "0.9.0" gem "capistrano" gem "heroku" gem "rack", "1.1.0" gem "clearance" gem "fastercsv" group :test do gem "rspec" gem "rspec-rails", "1.3.2" gem "faker" gem "database_cleaner" gem "capybara" gem "cucumber" gem "cucumber-rails" gem "test-unit" gem "factory_girl" gem "formtastic" gem "email_spec" end
As you might guess from taking a gander at this list of gems, we’re going to be getting into all kinds of cool new things here. We’re installing the PostgreSQL database adapter gem, which is obviously necessary and not new to us. We’re also going to be using Capistrano to deploy our app — again, not new.
But, other than those two, everything else on the list is something we haven’t dealt with before. We’ll be getting into unit testing with Test::Unit and Factory Girl, behavior-driven development (BDD) with RSpec and Cucumber, auto-generation of dummy data with Faker, integration testing with Capybara, and more. We’ll also be deploying our app to that great gig Ruby application platform in the sky — Heroku!
Let’s set this sucker off. Run the following:
This command will download and install all of the gems specified in our Gemfile, along with their dependencies. Four of them require native extensions to be built. Of those, two require the development libraries we installed earlier as Ubuntu packages.
Assuming all went well, our app now has a “bundle” of joy gems associated with it.
Replace Database Config
Next, we’ll replace the contents of our ~/workspace/rails238test/config/database.yml file with the following:
development: adapter: postgresql encoding: utf8 database: rails238test_development username: postgres password: host: localhost # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: &TEST adapter: postgresql encoding: utf8 database: rails238test_test username: postgres password: host: localhost
Set Up RSpec, Cucumber & Capybara
From your project root (~/workspace/rails238test), run the following:
$ ./script/generate rspec Configuring rspec and rspec-rails gems in config/environments/test.rb ... exists lib/tasks create lib/tasks/rspec.rake create script/autospec create script/spec create spec create spec/rcov.opts create spec/spec.opts create spec/spec_helper.rb $ ./script/generate cucumber --rspec --capybara force config/database.yml create config/cucumber.yml create config/environments/cucumber.rb create script/cucumber create features/step_definitions create features/step_definitions/web_steps.rb create features/support create features/support/paths.rb create features/support/env.rb exists lib/tasks create lib/tasks/cucumber.rake
Those two steps bootstrap our Rails app for use with RSpec, Cucumber and Capybara. They create some files, and add a couple lines to our database.yml file.
Generate a Cucumber Feature and a Scaffold
Now, let’s run the following (again, from our project root):
$ ./script/generate feature book title:string author:string publisher:string copyright_year:string isbn:string exists features/step_definitions create features/manage_books.feature create features/step_definitions/book_steps.rb $ ./script/generate scaffold book title:string author:string publisher:string copyright_year:string isbn:string exists app/models/ exists app/controllers/ exists app/helpers/ create app/views/books exists app/views/layouts/ exists test/functional/ exists test/unit/ create test/unit/helpers/ exists public/stylesheets/ create app/views/books/index.html.erb create app/views/books/show.html.erb create app/views/books/new.html.erb create app/views/books/edit.html.erb create app/views/layouts/books.html.erb create public/stylesheets/scaffold.css create app/controllers/books_controller.rb create test/functional/books_controller_test.rb create app/helpers/books_helper.rb create test/unit/helpers/books_helper_test.rb route map.resources :books dependency model exists app/models/ exists test/unit/ exists test/fixtures/ create app/models/book.rb create test/unit/book_test.rb create test/fixtures/books.yml create db/migrate create db/migrate/20100531032247_create_books.rb
All this did was auto-generate a feature, along with a scaffold matching that feature. In our case, the feature we want to implement is “basic CRUD functions for data on books”. The scaffold we generated implements that feature (that part should look familiar if you’ve seen my earlier series of posts on getting started with Rails).
Create Database & Schema
Now, we need to get our database and schema created, so that we can test what we just generated. Before we do this, we need to change one of our PostgreSQL config files to allow Rails to connect to it. Run the following:
sudo pico /etc/postgresql/8.4/main/pg_hba.conf
Scroll down to the bottom of the file, and you’ll see a section that looks like this:
# TYPE DATABASE USER CIDR-ADDRESS METHOD # "local" is for Unix domain socket connections only local all all ident # IPv4 local connections: host all all 127.0.0.1/32 md5 # IPv6 local connections: host all all ::1/128 md5
Make it look like this:
# TYPE DATABASE USER CIDR-ADDRESS METHOD # "local" is for Unix domain socket connections only local all all trust # IPv4 local connections: host all all 127.0.0.1/32 trust # IPv6 local connections: host all all ::1/128 trust
It took me a while, but I finally found an old Ruby on Rails wiki page that explains why this step is needed, and why the solution we applied in a previous post (changing only the “local” line of that section) doesn’t work.
It turns out that Rails connects to the local PostgreSQL server via TCP sockets, not UNIX domain sockets. (So why did our previous solution work before? It’s a mystery to me at the moment. Maybe I’ll discover the answer and write about it in a future post.)
This time, we’re trying to connect to the server as, and create a database for, the PostgreSQL user “postgres” — the only one that exists right now, which doesn’t have a password set.
The change we just made opens up the ability for any application, running locally as any UNIX user, connecting on any UNIX domain socket or TCP socket (using IPv4 or v6), to connect to the PostgreSQL server as any PostgreSQL user, without a password.
This level of security obviously will not do in a staging or production environment, but it will be fine for our local development purposes.
So, let’s save the config file with Ctrl-O, exit pico with Ctrl-X, and tell PostgreSQL to reload its configuration files with the following command:
sudo su postgres -c "/usr/lib/postgresql/8.4/bin/pg_ctl reload -D /var/lib/postgresql/8.4/main"
Now, let’s create our database:
$ rake db:create --trace (in /home/jeremy/workspace/rails238test) ** Invoke db:create (first_time) ** Invoke db:load_config (first_time) ** Invoke rails_env (first_time) ** Execute rails_env ** Execute db:load_config ** Execute db:create
And our schema:
$ rake db:migrate --trace (in /home/jeremy/workspace/rails238test) ** Invoke db:migrate (first_time) ** Invoke environment (first_time) ** Execute environment ** Execute db:migrate == CreateBooks: migrating ==================================================== -- create_table(:books) NOTICE: CREATE TABLE will create implicit sequence "books_id_seq" for serial column "books.id" NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "books_pkey" for table "books" -> 0.1953s == CreateBooks: migrated (0.1960s) =========================================== ** Invoke db:schema:dump (first_time) ** Invoke environment ** Execute db:schema:dump
First, let’s test our specs:
$ rake spec --trace (in /home/jeremy/workspace/rails238test) ** Invoke spec (first_time) ** Invoke db:test:prepare (first_time) ** Invoke db:abort_if_pending_migrations (first_time) ** Invoke environment (first_time) ** Execute environment ** Execute db:abort_if_pending_migrations ** Execute db:test:prepare ** Invoke db:test:load (first_time) ** Invoke db:test:purge (first_time) ** Invoke environment ** Execute db:test:purge ** Execute db:test:load ** Invoke db:schema:load (first_time) ** Invoke environment ** Execute db:schema:load NOTICE: CREATE TABLE will create implicit sequence "books_id_seq" for serial column "books.id" NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "books_pkey" for table "books" ** Execute spec
And now, let’s test our features:
$ rake cucumber --trace (in /home/jeremy/workspace/rails238test) ** Invoke cucumber (first_time) ** Invoke cucumber:ok (first_time) ** Invoke db:test:prepare (first_time) ** Invoke db:abort_if_pending_migrations (first_time) ** Invoke environment (first_time) ** Execute environment ** Execute db:abort_if_pending_migrations ** Execute db:test:prepare ** Invoke db:test:load (first_time) ** Invoke db:test:purge (first_time) ** Invoke environment ** Execute db:test:purge ** Execute db:test:load ** Invoke db:schema:load (first_time) ** Invoke environment ** Execute db:schema:load NOTICE: CREATE TABLE will create implicit sequence "books_id_seq" for serial column "books.id" NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "books_pkey" for table "books" ** Execute cucumber:ok /home/jeremy/.rvm/rubies/ruby-1.8.7-p174/bin/ruby -I "/home/jeremy/.rvm/gems/ruby-1.8.7-p174/gems/cucumber-0.7.3/lib:lib" "/home/jeremy/.rvm/gems/ruby-1.8.7-p174/gems/cucumber-0.7.3/bin/cucumber" --profile default Using the default profile... ............... 2 scenarios (2 passed) 15 steps (15 passed) 0m1.003s ** Execute cucumber
Create Local Git Repo, Prepare for Heroku
So, now, let’s Git-ify this thing (again, make sure you’re in ~/workspace/rails238test). First, let’s create a .gitignore file so that we can exclude certain files from our Git repository:
cat<<EOF >.gitignore > .bundle > .gitignore > EOF
We just excluded our .bundle directory, and the .gitignore file itself. Now, let’s run the following commands in order:
git init git config --global user.email <your_email_address> git config --global user.name "<your_name>" git add . git commit -m 'first commit'
Our app is now ready to go up to Heroku. Before we can do that, we’ll need an account on Heroku. Go here to sign up for a free account.
Once we’ve done that, we’ll need to generate an SSH keypair:
ssh-keygen -C "<your_email_address>" -t rsa
Create App on Heroku, Deploy
OK. Let’s get this baby onto Heroku! First, we’ll need to create the app on Heroku:
$ heroku create <some_unique_identifier>-rails238test Enter your Heroku credentials. Email: <your_email_address> Password: <your_heroku_password> Uploading ssh public key /home/jeremy/.ssh/id_rsa.pub Creating <some_unique_identifier>-rails238test.... done Created http://<some_unique_identifier>-rails238test.heroku.com/ | email@example.com:<some_unique_identifier>-rails238test.git
Next, we’ll add our remote destination to our git repo, and push our code up there:
$ git remote add heroku firstname.lastname@example.org:<some_unique_identifier>-rails238test.git $ git push heroku master The authenticity of host 'heroku.com (188.8.131.52)' can't be established. RSA key fingerprint is 8b:48:5e:67:0e:c9:16:47:32:f2:87:0c:1f:c8:60:ad. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added 'heroku.com,184.108.40.206' (RSA) to the list of known hosts. Counting objects: 116, done. Compressing objects: 100% (100/100), done. Writing objects: 100% (116/116), 97.84 KiB, done. Total 116 (delta 10), reused 0 (delta 0) -----> Heroku receiving push -----> Gemfile detected, running Bundler -----> Bundler works best on the Bamboo stack. Please migrate your app: http://docs.heroku.com/bamboo Unresolved dependencies detected; Installing... Fetching source index from http://rubygems.org/ Resolving dependencies Installing actionmailer (2.3.8) from rubygems repository at http://rubygems.org/ ... the rest of our gems ... Your bundle is complete! Locking environment -----> Rails app detected Compiled slug size is 11.2MB -----> Launching......... done http://<some_unique_identifier>-rails238test.heroku.com deployed to Heroku To email@example.com:<some_unique_identifier>-rails238test.git * [new branch] master -> master
Hmm. Looks like we should have specified the Bamboo stack when we created the app on Heroku. But that’s OK; we can migrate to another stack. Before we do that, let’s set up our app’s database on Heroku:
$ heroku rake db:migrate (in /disk1/home/slugs/<some_other_unique_identifying_string>/mnt) == CreateBooks: migrating ==================================================== -- create_table(:books) -> 0.0163s == CreateBooks: migrated (0.0164s) ===========================================
It’s now time for the moment of truth. Let’s point our browser at <some_unique_identifier>-rails238test.heroku.com/books, and see what we get!
If you see a page that says “Listing books” at the top, you’re in business!
Though we probably don’t need to (given that we’re just running our simple “books” CRUD app), let’s migrate to the Bamboo stack like Heroku says:
$ heroku stack:migrate bamboo-ree-1.8.7 -----> Preparing to migrate <some_unique_identifier>-rails238test aspen-mri-1.8.6 -> bamboo-ree-1.8.7 NOTE: You must specify ALL gems (including Rails) in manifest Please read the migration guide: http://docs.heroku.com/bamboo -----> Migration prepared. Run 'git push heroku master' to execute migration. $ git push heroku master Warning: Permanently added the RSA host key for IP address '220.127.116.11' to the list of known hosts. Everything up-to-date $ heroku stack * aspen-mri-1.8.6 bamboo-ree-1.8.7 (beta) (prepared, will migrate on next git push) bamboo-mri-1.9.1 (beta)
It won’t migrate unless it actually has some code changes to push up. So, we’ll create a dummy commit and push that:
$ echo >> Rakefile && git commit -a -m "migrating to bamboo stack" [master 48624c5] migrating to bamboo stack 1 files changed, 1 insertions(+), 0 deletions(-) $ git push heroku master Warning: Permanently added the RSA host key for IP address '18.104.22.168' to the list of known hosts. Counting objects: 5, done. Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 305 bytes, done. Total 3 (delta 2), reused 0 (delta 0) -----> Heroku receiving push -----> Migrating from aspen-mri-1.8.6 to bamboo-ree-1.8.7 -----> Gemfile detected, running Bundler Unresolved dependencies detected; Installing... Fetching source index from http://rubygems.org/ Using rake (0.8.7) from system gems Installing activesupport (2.3.8) from rubygems repository at http://rubygems.org/ ... the rest of our gems ... Your bundle is complete! Use `bundle show gemname` to see where a bundled gem is installed. Locking environment -----> Rails app detected Compiled slug size is 11.2MB -----> Launching............ done http://<some_unique_identifier>-rails238test.heroku.com deployed to Heroku -----> Migration complete, your app is now running on bamboo-ree-1.8.7 To firstname.lastname@example.org:<some_unique_identifier>-rails238test.git 87aa8d5..48624c5 master -> master
Success! We’re done!
Of course, we’ve just taken “one small step for [a] man, [but] one giant leap for Web-dev-kind.” There’s plenty more fun to be had exploring Ruby, Rails, TDD and BDD, and running apps on PaaS (Platform-as-a-Service) clouds like Heroku. I’ll be sure to chronicle more of my adventures here. Until next time, happy coding!