Force SSL For a Rails Application with an Nginx Proxy

Posted by Ben Reubenstein Wed, 09 Apr 2008 16:52:00 GMT

Today I needed an entire site to run over SSL. I implemented a very straight forward before_filter that would catch a request and redirect to SSL if the request was not local and not already over SSL.

class ApplicationController < ActionController::Base

  before_filter :ensure_ssl

  def ensure_ssl
    redirect_to url_for params.merge({:protocol => 'https://'}) unless (request.ssl? || local_request?)
  end

end

All this resulted in was an endless loop, with the action constantly redirecting. I turned on some debugging:

logger.info url_for params.merge({:protocol => 'https://'}) # Confirming URL was correct
logger.info request.ssl? # Confirming the request was SSL
logger.info request.port # Checking the port the request came in on

It turned out request.ssl? was nil and the port was always 80. Nginx was not properly proxying along the fact that it was running over ssl. I added the following to my server / location declaration in the nginx.conf:

proxy_set_header X_FORWARDED_PROTO https;

Restart Nginx and request.ssl? returned true and request.port returned 443. I also just found some other great nginx examples from halorgium

Taking Merb to Production 12

Posted by Ben Reubenstein Mon, 24 Dec 2007 15:11:00 GMT

UPDATE: I wrote this post very early in the evolution of the Merb platform. For more recent developments and tips, visit the Merb Wiki.

This past week I needed to implement a small webservice that needed to be fast and work well under a heavy load. I had part of the service implemented in Rails, but it was not as fast as I thought it could be so I decided to check out Merb. Merb is a lightweight MVC framework much like Rails. Merb however doesn't try to put a lot of "magic" at its core, so overall less time is spent in the framework and more time is spent in your code.

When I jumped into Merb, there were a lot of good articles but not much on how to productionize Merb, so I hope I can fill in the gap with this article.

NOTE: I am explaining a lot here, so if you run into spots that are confusing or you think could be done more efficiently, comment and I will get those adjustments into the doc.

Setting up Capistrano (Using 2.0)
Install Capistrano:

gem install capistrano

Like Rails apps there are certain files that will change in the production environment so they should not be in your repo. The ones I chose to exclude:
config/merb.yml
config/database.yml
Your excluded files may differ depending on the ORM you chose. I setup DataMapper for this particular application, though you could install the merb_activerecord gem and plug right into ActiveRecord.

Just like in a rails app run the capify command to get rolling:

$ capify .
[add] writing `./Capfile'
[add] writing `./config/deploy.rb'
[done] capified!

Edit your config/deploy.rb to match your settings here is my example with some notes:


set :application, "YOUR_APPLICATION_NAME"

# Set the path to your version control system (Subversion assumed)
set :repository, "http://something.com/svn/yourapplication/trunk"

# Set your SVN and SSH User
set :user, "your_ssh_user"
set :svn_user, "your_svn_user"
#Set the full path to your application on the server
set :deploy_to, "/PATH/TO/YOUR/#{application}"

#Define your servers
role :app, "your.appserver.com"
role :web, "your.webserver.com"
role :db, "your.databaseserver.com", :primary => true

desc "Link in the production extras and Migrate the Database ;)"
task :after_update_code do
  run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml"
  run "ln -nfs #{shared_path}/config/merb.yml #{release_path}/config/merb.yml"
  run "ln -nfs #{shared_path}/log #{release_path}/log"
  #if you use ActiveRecord, migrate the DB
  #deploy.migrate
end

desc "Merb it up with"
deploy.task :restart do
  run "cd #{current_path};./script/stop_merb"
  run "cd #{current_path};env EVENT=1 merb -c 4"
# If you want to run standard mongrel use this:
# run "cd #{current_path};merb -c 4"
end

#Overwrite the default deploy.migrate as it calls:
#rake RAILS_ENV=production db:migrate
#desc "MIGRATE THE DB! ActiveRecord"
#deploy.task :migrate do
#  run "cd #{release_path}; rake db:migrate MERB_ENV=production"
#end


Use Capistrano to initiate the environment, setting up the necessary directories on the server.
$ cap deploy:setup

Next, install the Gems you need on the Production server.

Note: if you are on the gem 0.9.4 run this upgrade, 0.9.5 is super nice ;). the -y / --include-dependencies option is on by default.

sudo gem update --system

sudo gem install merb
sudo gem install rspec

# For ActiveRecord
sudo gem install merb_activerecord
# For DataMapper
sudo gem install datamapper
sudo gem install do_mysql

# For Evented Mongrel
sudo gem install swiftiply

Create the directories and files that will be linked in.

mkdir /YOURDEPLOYPATH/shared/config
touch /YOURDEPLOYPATH/shared/config/database.yml
touch /YOURDEPLOYPATH/shared/config/merb.yml

Edit the .yml files to your liking and then be sure to create your database in MySQL! There is probably some neat deploy tricks you can do here with Capistrano, comment if you know it and I will update this section.

Deploy your app:

cap deploy

Now if you are amazing, it works the first time. If not check the errors. Most of them will likely be paths or a gem you missed.

Recap: We should now have our app deployed to the server and if you used the example config, 4 instances running. Hooray!

Setting up: NGINX
General Tip: If your server is still using Apache and you are just starting to experiment, run NGINX on another port, like 81. Be sure to clear a path through your Firewall ;)

Add the appropriate ip:ports to the upstream section for the merb instances you are running. The port that merb runs on and number of instances started are controlled by the deployment script and the merb.yml file.

upstream YOURUPSTREAMNAME {
 server 127.0.0.1:4000;
 server 127.0.0.1:4001;
 server 127.0.0.1:4002;
 server 127.0.0.1:4003;
}

Next lets add in a host section. This is just like a Rails host.


server {
 listen 80;

 client_max_body_size 50M;

 server_name YOURSERVERNAME;

 root /YOURPATH/current/public;

 #Want to log? include this
 #access_log /var/log/nginx/nginx.YOURAPPNAME.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://YOURUPSTREAMNAME;
   break;
  }  }

 error_page 500 502 503 504 /500.html;
 location = /500.html {
  root /YOURPATH/current/public;
 }
}


Restart nginx and enjoy your new Merb application!

Thanks to the folks in #merb and #datamapper for answering questions I had. Special thanks to Ezra for creating Merb and pushing performance in the Ruby web application world.

Resources: