Late last year, we published a custom Ruby on Rails web application for a college instructor. She wanted to provide her students with a specific learning activity and then monitor if, and how, it improved their mastery of some critical material. The app had two equally important parts: the UI front-end for students and the data collection backend for the instructor.
After the project was completed, our customer began using the app in her own teaching and showing it to colleagues. Several expressed interest in integrating the app into their own courses. This was good news; the customer had always hoped she might be able to license the app to others and, perhaps, builds up a revenue stream from it. She was eager to explore the business opportunity but wanted to have a strict separation of the data collected for each instructor, both for simplicity and for privacy considerations.
The ‘right’ way to provide this separation, if the current interest turns into a real opportunity, is for us to develop a more robust administrative back-end for the app, with role-based administration that would let each instructor manage his or her own students and their data. But, in the spirit of providing a minimum viable product to allow the customer to explore the opportunity without sinking a lot of money into development, we came up with the idea of creating a single site with multiple copies of the application, each with its own database. We figured that for a reasonably stable codebase, the duplication of installed code would create minimal problems for the first 2, 3, maybe even 5 customers. After that, she’d have to invest in admin code if she wanted to support more.
And, at first glance, the approach seemed simple enough from a technical perspective. My famous last words.
In reality, this was a complex problem containing several hidden pitfalls, and requiring a high degree of knowledge in both Phusion Passenger and Rails configuration. I was surprised by how little documentation I could find on more advanced topics in the areas of Rails configuration, Rails deployment, and Rails routing. I found my ‘simple approach’ was actually a recipe for a few hard days of research, experimentation, and work.
What We Wanted
Our idea here was that we wanted the site to have a ‘main’ Rails application, but the site would also contain a (potentially unlimited) number of sub applications. These sub applications were essentially just clones of the main Rails application, but with differing databases.
Our goal was to have the main application be served if a user navigated to the root of the domain, but to serve the individual sub apps if the user navigated to a directory containing a sub app. These applications were to be completely independent of each other. With no code or data being shared.
www.example.com/ -> The user would hit the main rails app, and it would work like any other rails application.
www.example.com/valid-subsite/ -> The user would go straight into one of the sub apps.
www.example.com/invalid-subsite/ -> Main rails app will return 404 error.
Arch Linux x64 $(uname -r) # 3.9.9-1-ARCH Apache v2.2.25 Phusion Passenger v4.0.10 Rails v3.2.13 Ruby v2.0.0p247
Getting Multiple Apps Up and Running
After some deliberation and planning, we decided that a directory should be created for each Rails application, and that requests to the root site would be forwarded directly to the Main app, while requests to the sub-directories would go straight to the sub app in question.
The finished directory tree looked like this:
|- /home/user/ \ |- main_app/ |- subsite1_app/ |- subsite2_app/ |- www.example.com/ \ # These directories are all symbolic links |- main/ -> ~/main_app/public/ |- subsite1/ -> ~/subsite1_app/public/ |- subsite2/ -> ~/subsite2_app/public/ ...
This left two problems:
- How do you forward the root site’s requests to the main app?
- How do you allow requests to subsites to go outside the main app’s routing?
It turned out that the key to routing was setting Passenger Variables in various
.htaccess files. First, there are apache settings. Put this somewhere in your
<Directory /home/user/www.example.com/> AllowOverride All Options -Multiviews </Directory>
.htaccess file that you will create will go into the root directory of your website. In our case
/home/user/www.example.com/. This file should contain the following settings:
PassengerEnabled on PassengerAppRoot /home/user/main_app/
Next, each sub app will require its own
.htaccess file. In our example this would be
/home/user/subsite1_app/public/. This file should be put in the applications
public/ directory, and contain these settings:
PassengerEnabled on PassengerAppRoot /home/user/subsite1_app/ RailsBaseURI /subsite1 # This is the location of the apps public # directory relative to the sites root.
Watch Out! The location for the
.htaccess file in the sub apps may vary. If it doesn’t work with the
.htaccess file in the
public/ directory try putting the file in the sub app’s root directory.
Assuming that everything is set up right, you should be able to navigate to between all your different applications.
But we’re not done yet!
After all this, there is a good chance that things still won’t be working 100%. We’ve configured apache and passenger, but now there are some specific Rails settings that may need adjusted.
In this case several of the app assets for the sub apps were pointing to the main site’s asset location, and if the sub apps are clones of the main app you may not even notice this unless you are inspecting your browser’s request headers.
It turns out that Rails contains a configuration setting that allows you to set a relative url root. This property, aptly named
relative_url_root, can be set by adding the following to your sub applications’
# Don't forget to change 'MyApplication' to your app's name! MyApplication::Application.configure do # ... config.relative_url_root = '/subsite1' # This should match the # RailsBaseURI setting in # the .htaccess file # ... end
While this did fix the path issue for assets in general, in cases where custom links were being created it was still necessary to have a way to grab the relative root url.
I developed the following little helper method to do this. I recommend adding it to your
# Return a path that is prefixed with the Application's # relative_url_root def add_relative_root(path) root = Rails.application.config.relative_url_root if root.nil? path else uri = URI(path) uri.path = File.join(root, uri.path) uri.to_s end end
We’re all done! Your applications should be up and running now.
I found the complexity of this problem unexpected and the lack of applicable documentation surprising.
It is my hope that this article will help anyone who finds themselves in a similar situation.
Good luck. And, if you have questions or comments, please feel free to post them below. I’ll help anyone else if I can. Or, if you think you have a better technical solution, I’d love to hear about it.