Tag Archives: Capistrano

Generate Gzipped Assets in Rails 4.2 +

Doesn’t Rails generate gzipped assets on running rake assets:precompile? It did so, until Rails 4.2. What changed?

Rails uses sprockets gem, for compiling and serving web assets. Before Sprockets 3 was introduced, rake assets:precompile generated .gz versions for all the assets.

This change, seems backward, for several reasons

  1. Gzipping should always be enabled for static assets. It’s super charges serving of static content.
  2. Assets should be gzip’ed beforehand as part of the build process, rather than letting the webserver do it on the fly, saving on CPU cycles.
  3. Also, Nginx has the ability to serve static .gz files directly, by using the gzip_static on; directive.

The reason for dropping this, was its incompatibility with Apache.

Since Sprockets 3 dropped this feature, it means Rails had to loose it too. The Rails team, plans to bring it back as an opt-in. Nevertheless, what does one do in the meantime?

Here are the options to generate Gzipped Assets with Rails

  1. depends on parallel gem to use multiple CPU cores, but can easily be simplified
  2. gzip assets with Capistrano
  3. write an assets:gzip rake task that plugs into assets:precompile (mentioned below)
namespace :assets do
  desc "Create .gz versions of assets"
  task :gzip => :environment do
    zip_types = /\.(?:css|html|js|otf|svg|txt|xml)$/

    public_assets = File.join(
      Rails.root,
      "public",
      Rails.application.config.assets.prefix)

    Dir["#{public_assets}/**/*"].each do |f|
      next unless f =~ zip_types

      mtime = File.mtime(f)
      gz_file = "#{f}.gz"
      next if File.exist?(gz_file) && File.mtime(gz_file) >= mtime

      File.open(gz_file, "wb") do |dest|
        gz = Zlib::GzipWriter.new(dest, Zlib::BEST_COMPRESSION)
        gz.mtime = mtime.to_i
        IO.copy_stream(open(f), gz)
        gz.close
      end

      File.utime(mtime, mtime, gz_file)
    end
  end

  # Hook into existing assets:precompile task
  Rake::Task["assets:precompile"].enhance do
    Rake::Task["assets:gzip"].invoke
  end
end

Setting up Rails with Nginx + Thin on Amazon EC2 with Capistrano

1. Setting up Rails 3.2.12 on EC2 with Amazon Linux AMI

Amazon Linux AMI has Ruby 1.8.7 as default. To install ruby 1.9.3 and Rails 3.2.12 do the following

#Install ruby 1.9 by running 
sudo yum install ruby19

Note that /usr/bin might contain  ‘gem’ which actually refers to 1.8 version. So, in order to install gem1.9 do the following

sudo yum install ruby19-devel

If you do ruby -v you might still see 1.8, in order to change that,

cd /usr/bin
sudo rm ruby
sudo ln -s /usr/bin/ruby1.9 /usr/bin/ruby
sudo ln -s /usr/bin/gem1.9 /usr/bin/gem
ruby -v 
>> ruby 1.9.3p286 (2012-10-12 revision 37165) [x86_64-linux] 

Now that you have ruby 1.9.3 correctly installed, we can install rails.

sudo yum groupinstall "development tools"     #(Download size: 120M !)
gem install rails --no-ri --no-rdoc
rails -v
>> Rails 3.2.12

#Also install appropriate DB that you would be using. Look here if you want to install postgresql on ec2 
#Example, if using sqlite3
sudo yum install sqlite-devel

Congratulations, you have installed Rails 3+ on your Linux AMI.

2. Installing Nginx

sudo yum install nginx

3. Install Thin

gem install thin

4. Configure Git Repository

Let’s say we want to deploy a Rails App called as  rails-demo , for that lets initialize a git-repo as

 mkdir -p ~/git/rails_demo.git
 cd ~/git/rails_demo.git
 git --bare init

This would have initialized a git repository at ~/git/rails_demo.git/ .

5. Add server keys to authorized_keys [Deployment + Git Server === EC2 Linux AMI]

We would need to do this, since Capistrano would be accessing our code-repository as if it were remote, and hence the server-keys would have to be added to its own authorized keys.

test -e ~/.ssh/id_dsa.pub || ssh-keygen -t dsa
cat ~/.ssh/id_dsa.pub >> ~/.ssh/authorized_keys

6. Rails Deployment using Capistrano [Local Machine from where you want to push a rails application]

Lets say, we have a bare bones rails app created using only rails new <appname>.

rails new rails_demo
cd rails_demo
-----------------------
# Add the following to the Gemfile
gem 'capistrano'
gem 'thin'
gem 'bigdecimal'
-----------------------
# now run bundle install to install these 2 gems.
bundle install

A) Capify Deploy.rb

#In your rails application directory, run

capify . 

#[add] writing './Capfile'
#[add] writing './config/deploy.rb'
#[done] capified!

Go to deploy.rb and edit it as below. Only change the fields in bold

set :user, 'ec2-user'
set :domain, 'ec2-11-222-33-444.<region>.compute.amazonaws.com'
set :application, "rails_demo"
set :repository, "#{user}@#{domain}:git/#{application}.git"
set :deploy_to, "/home/#{user}/#{application}"
set :normalize_asset_timestamps, false

role :web, domain # Your HTTP server, Apache/etc
role :app, domain # This may be the same as your `Web` serverrole :db, domain, :primary => true # This is where Rails migrations will run
default_run_options[:shell] = false
default_run_options[:pty] = true
# miscellaneous options
set :deploy_via, :remote_cache
set :scm, 'git'
set :branch, 'master'
set :scm_verbose, true
set :use_sudo, false
# Define all the tasks that need to be running manually after Capistrano is finish
ed.
namespace :deploy do
task :bundle_install, :roles => :app do
 run "cd #{release_path} && bundle install"
 end
end
after "deploy:update_code", :bundle_install
 desc "install the necessary prerequisites"
 task :bundle_install, :roles => :app do
 run "cd #{release_path} && bundle install"
 run "cd #{release_path} && rake assets:precompile"end

*Note to change the fields in bold.

B) Thin Configuration

# Generate this thin configuration on local, which default the options to 3 thin 
# servers and production environment
thin config -C config/thin-config.yml -c ~/rails_app --servers 3 -e production

This would generate the config file with details as below. Change the chdir field to /home/ec2-user/rails_demo/current/  . (which is where the app would be deployed on ec2)

---
 chdir: /home/ec2-user/rails_demo/current/
 environment: production
 address: 0.0.0.0
 port: 3000
 timeout: 30
 log: log/thin.log
 pid: tmp/pids/thin.pid
 max_conns: 1024
 max_persistent_conns: 100
 require: []
 wait: 30
 servers: 3
 daemonize: true

C) Git Usage

Add all these files to git by doing the following

git init .
git add .
git commit -m "rails demo to ec2"
git remote add aws ssh://ec2-user@<ec2-ami-name>/~/git/rails_demo.git/
ssh-add <local-key>
git push aws master

D. Capistrano Deployment

cap deploy:setup
cap deploy:check
cap deploy

If everything goes well, take a break – you deserve it since the deployment has been done and we only need to start / configure nginx now. If you see on the deployment server there would be current directory here

/home/ec2-user/rails_demo/current  – which contains the latest deployment, we did.

7. Configuring + Starting Nginx and Thin

Now, that the application is on the server, let’s start thin and nginx.

cd /home/ec2-user/rails_demo/current 
thin start -C thin-config.yml

(This would start 3 thin instance on ports 3000 , 3001 and 3002 .)

Now, all we need to do is redirect nginx to these ports.

cd /etc/nginx 
vi nginx.conf 

and add the below content at appropriate places. I have only shown the  part to be change in bold of the http section. Only this section needs to be changed for nginx.

http {
 include /etc/nginx/mime.types;
 default_type application/octet-stream;
 server_names_hash_bucket_size 128;
 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 main;
sendfile on;
 keepalive_timeout 65;
#gzip on;
upstream thin {
 server 127.0.0.1:3000;
 server 127.0.0.1:3001;
 server 127.0.0.1:3002;
}
 
server {
 listen 80;
 server_name ec2-122-248-194-230.ap-southeast-1.compute.amazonaws.com;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
location / {
 root /home/ec2-user/rails_demo/current/public/;
 #root /usr/share/nginx/html;
 #index index.html index.htm;
# First attempt to serve request as file, then
 # as directory, then fall back to index.html
 try_files $uri $uri/ /index.html;
 # Uncomment to enable naxsi on this location
 # include /etc/nginx/naxsi.rules
 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 off;
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://thin;
 break;
 } }
# redirect server error pages to the static page /40x.html
 #
 error_page 404 public/404.html;
 location = /40x.html {
 root /usr/share/nginx/html;
 }
# redirect server error pages to the static page /50x.html
 #
 error_page 500 502 503 504 /50x.html;
 location = /50x.html {
 root /usr/share/nginx/html;
 }
include /etc/nginx/conf.d/*.conf;
}

Save this file, and

sudo service nginx start

Done!