Multiple Applications Using a Single Code Base in Ruby

Combining 2 rails apps to a single codebase

Your idea of using engines is what I would suggest.

For routing, I would handle it outside of Rails.

For instance, you would do the following in nginx:

server {
# Match only one host.
listen 80 default;
server_name YOUR_SINGLE_APP_DOMAIN;

location / {
upstream YOUR_SINGLE_APP_RAILS;
}
}

server {
# Fall thru and match any other host.
listen 80 default;
server_name ~^.*$;

location / {
upstream YOUR_MULTI_DOMAIN_APP_RAILS;
}
}

How do I run a single codebase on multiple domains?

In a Rails applications there may be several areas where you need to access the domain associated with the application. Depending on the specific area of the code where you need it, you have 3 different cases:

  1. code that runs in the context of a request
  2. code that runs outside the context of a request, but it is triggered by a request (or may be triggered by a request)
  3. code that runs outside the context of a request

To give you an example:

  • a controller is run into the context of a request
  • a model method is executed outside the context of a request, but you can potentially pass the context when you invoke the method. However, this is now always applicable (e.g. if the same method is executed via the CLI or a background job)
  • background jobs or tasks run by a scheduler are completely outside the context of a request

Libraries that operates in the context of a request (directly or indirectly) are easy to handle. The approach is generally to detect the specific hostname from the request itself.

While in most cases this is practical, in others this is a little bit more complicated. A notable case is ActiveMailer, that is always sitting between the two contexts. In most case emails are triggered in response of an HTTP request, but not always. Moreover, there is no practical way to pass the request context from the controller to the mailer, unless you explicitly pass the request.env as argument to each email you want to send. But this is not the most practical solution.

For simple needs, and specifically for ActionMailer, I created a simple plugin called actionmailer_with_request that stores the context of the request in the current thread, and make it available to ActionMailer. I'm mentioning it here mostly for the sake of completeness, but I'm the first one admitting it is not a complete nor a definitive solution. It is also against the MVC architecture.

The reason is not the definitive solution, is because it doesn't completely solve the issue of making the domain available in your emails. In fact, if the mailer is triggered by a background job or a cron job, you will still have no request context, hence the hostname won't be present. Likewise, the issue still applies to all the systems outside ActionMailer that needs to access the current hostname. It could be another mail process, or whatever background/external process.

At this point, it's quite evident that just relying on the context of a request is not enough. In fact, you likely have to store the information somewhere.

You can have several different possibilities. There is no single, recommended solution. Some alternatives are:

  • The Rails configuration object, e.g. Rails.application.config.hostname = xxxx
  • A configuration system
  • A constant inside your Rails application or environment

In our case, we have a namespace inside lib where we store all our application related stuff. We also make heavy use of the environment variables.

Hence, assuming your application is called Something (hence in config/application you have module Something), we have in lib/something.rb:

module Something
def default_hostname
ENV['SITE_HOST']
end

def default_protocol
ENV['SITE_PROTOCOL']
end
end

These are convenient wrappers to not expose the environment variable directly. Moreover, it allows us to exchange between a config variable, or an env, or whatever we need, without modifying anywhere else the code.

Then, in config/environments/production.rb you need to update your mailer setting:

  config.action_mailer.default_url_options = { host: Something.default_hostname, protocol: Something.default_protocol }

You can also decide to configure it in config/application.rb if you want the setting to apply to all the contexts.

Wherever you need to access the hostname outside the context of the request, you can use

Something.default_hostname

The convenience of passing through a method, is that you can also set a default.

module Something
def default_hostname
ENV['SITE_HOST'] || "example.com"
end
end

and perhaps a warning

module Something
def default_hostname
ENV['SITE_HOST'] || begin
warn("Please set SITE_HOST to configure your site URL")
"example.com"
end
end
end

Multiple applications with Ruby on Rails

The easiest way to have admin panel is to use namespaces. You just put all admin stuff to admin namespace. It is very common practice.

On the other hand, if you want to have two (or more) applications sharing the same database and models it is quite easy. I have one project that has two RoR applications sharing the same database. So here are my thoughts about it:

  1. I put all migrations in my first project. It can be messy if you put all of them (or few here few there) in both applications. Then, after migration, you can copy schema.db to second project, or just us a symbolic link (in Unix-like systems) and don't care about it anymore.
  2. If you want to share some model in both application, then you can copy model file or use symbolic link. I used first method, because I didn't want to copy all related models that I don't use in second application.
  3. It works great. But in this case you also have to setup server for two applications. I've used different subdomains for both applications.

Hope it helps!

Running multiple sites from the same rails codebase?

We currently work with a setup quite similar with what you are describing.

We started developing a somewhat big Rails app (sales, stock management, product catalogue, etc) for a client. After finishing it, there came several new requests for almost identical functionality.

The original app, however, had to keep being maintained, adding new features, correcting bugs and whatnot.

The extended ones needed to maintain most functionality, but change appearance and looks.

What we did was follow a series of steps:

  1. First we started cleaning up the code, pulling hardcode references to tables, reducing and optimizing queries, looking up missing indexes and ways to improve our ActiveRecord use
  2. After being somewhat satisfied, we started developing missing tests. I can't stress hard enough why it's useful, since we'll be maintaining a same codebase for several apps, and need the core functionality to be as protected as it can be from new changes.
  3. That was also the magic word: core functionality. We started selecting base functionality that could be reused, and extrating all generic code. That gave us a mix of controllers, models and views, which we started to change into modules, plugins and gems.
    What goes where? Depends greatly on your code. As a rule of thumb, functionality that doesn't deal with the domain language goes to plugins (or gems if it doesn't depends too much on Rails)


    1. This approach led us to a several of plugins, gems which we then pulled together reassembling the original project, and then it got to it's own GIT repository. That way, we had a main "template" repository which glued all the components and several other GIT repositories for each of them.
    2. Finally, we develop an easy theme system (basically loading /stylesheets/themes/:theme_name/ and getting theme_name from the DB). Since it's an intranet project, we could almost do anything with proper CSS styling. I'd guess for working with IE you'd need a more complex approach.
    3. Then, we just used that main repository developing the new functionality on top of it.

Now, how do we deal with changes to the core base. We start with our template repository. We fix or define where the fix or change should be and either change it there or on it's corresponding gem/plugin. After properly testing it, we deploy it to our GitHub account.

Finally, we merge/rebase the other projects from that template repository, getting the new updates.

Sounds a bit complicated, but it was only for the setup. The current workflow is quite simple and easy, with the given advantage of working with several developers without bigger issues.

Multitenancy Passenger Rails Multiple Apps Different Versions Same Domain

ZOMG it works! So a big thanks to tadman for suggesting that I use Nginx as a reverse proxy server. That is, one Nginx serves individual Apache processes for each app. I am able to go to mywebsite.com and get the main app. I go to mywebsite.com/subapp and get my second app (same Ruby/Rails version as the main one). I can go to mywebsite.com/mea and get a third app (that has a different Ruby and Rails version than the first two!).

Part of this is possible because both Nginx and Apache have a Passenger component that you can install, giving you all sorts of Directives you can use to specify what kind of app each url should serve up (passenger_ruby lets you tell Nginx which ruby interpreter to use). I've never seen documentation as comprehensive and beautiful as on Phusion's website.

Figuring this out was fun. Here is my setup so that you can see what I did. And please let me know if there's anything I could do better.

Nginx config:
/etc/nginx/sites-enabled/apache

server {
listen 80;
server_name mywebsite.com www.mywebsite.com;

passenger_enabled on;

location / {
passenger_ruby /usr/local/rvm/gems/ruby-2.3.4/wrappers/ruby;
proxy_pass http://my_website_ip:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /subapp {
passenger_ruby /usr/local/rvm/gems/ruby-2.3.4/wrappers/ruby;
proxy_pass http://my_website_ip:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /mea {
passenger_ruby /usr/local/rvm/gems/ruby-2.5.0/wrappers/ruby;
proxy_pass http://my_website_ip:8082;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

My Ports file listens for the ports served by Nginx:

/etc/apache2/ports.conf

# If you just change the port or add more ports here, you will likely also
# have to change the VirtualHost statement in
# /etc/apache2/sites-enabled/000-default.conf

Listen 8080
Listen 8081
Listen 8082

<IfModule ssl_module>
Listen 443
</IfModule>

<IfModule mod_gnutls.c>
Listen 443
</IfModule>

My Apache VirtualHost definitions.

/etc/apache2/sites-enabled/login_app.conf

<VirtualHost *:8080>
ServerName mywebsite.com

# Tell Apache and Passenger where your app's 'public' directory is
DocumentRoot /var/www/login_app/code/public

PassengerRuby /usr/local/rvm/gems/ruby-2.3.4/wrappers/ruby

# Relax Apache security settings
<Directory /var/www/login_app/code/public>
Allow from all
Options -MultiViews
# Uncomment this if you're on Apache >= 2.4:
Require all granted
</Directory>

</VirtualHost>

<VirtualHost *:8081>
ServerName mywebsite.com

# Tell Apache and Passenger where your app's 'public' directory is
DocumentRoot /var/www/second_app/code/public

PassengerRuby /usr/local/rvm/gems/ruby-2.3.4/wrappers/ruby

# Adding a subapp to the base url
Alias /subapp /var/www/second_app/code/public
<Location /subapp>
PassengerBaseURI /subapp
PassengerAppRoot /var/www/second_app/code
</Location>

# Relax Apache security settings
<Directory /var/www/second_app/code/public>
Allow from all
Options -MultiViews
# Uncomment this if you're on Apache >= 2.4:
Require all granted
</Directory>
</VirtualHost>

<VirtualHost *:8082>
ServerName mywebsite.com

# Tell Apache and Passenger where your app's 'public' directory is
DocumentRoot /var/www/third_app/code/public

PassengerRuby /usr/local/rvm/gems/ruby-2.5.0/wrappers/ruby

# Adding a subapp to the base url
Alias /mea /var/www/third_app/code/public
<Location /mea>
PassengerBaseURI /mea
PassengerAppRoot /var/www/third_app/code
</Location>

# Relax Apache security settings
<Directory /var/www/third_app/code/public>
Allow from all
Options -MultiViews
# Uncomment this if you're on Apache >= 2.4:
Require all granted
</Directory>
</VirtualHost>

And the processes produced, from the command line: passenger-memory-stats

Version: 5.2.0
Date : 2018-02-09 03:22:39 +0000

--------- Apache processes ----------
PID PPID VMSize Private Name
-------------------------------------
148.9 MB 0.4 MB /usr/sbin/apache2 -k start
813.3 MB 3.1 MB /usr/sbin/apache2 -k start
557.3 MB 3.2 MB /usr/sbin/apache2 -k start
### Processes: 3
### Total private dirty RSS: 6.74 MB

---------- Nginx processes -----------
PID PPID VMSize Private Name
--------------------------------------
174.8 MB 0.7 MB nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
174.8 MB 0.8 MB nginx: worker process
### Processes: 2
### Total private dirty RSS: 1.57 MB

----- Passenger processes -----
PID VMSize Private Name
-------------------------------
379.5 MB 4.7 MB Passenger watchdog
666.2 MB 7.1 MB Passenger core
378.9 MB 4.2 MB Passenger watchdog
662.5 MB 5.5 MB Passenger core
318.0 MB 63.0 MB Passenger RubyApp: /var/www/login_app/code (production)
314.5 MB 60.3 MB Passenger RubyApp: /var/www/third_app/code (production)
315.7 MB 61.4 MB Passenger RubyApp: /var/www/second_app/code (production)
### Processes: 7
### Total private dirty RSS: 206.14 MB

Can I maintain two versions of one application with Git?

Branching and forking in git is not bad at all, as the merge support is great (possible the best of al VCMs).

Personally, I don't like the idea of branching or forking a project to provide different customization as it can very quickly become really difficult, e.g. what are you going to do if you have 15 different deployments?

I think a better approach is to build the application so it behaves differently depending on some parameters. I'm well aware that sometimes the implementations are very different, so this might not be useful anymore.
Another approach is to build the core of your app in a GEM which acts as a service to the application, and the only thing you customize per client are the views. Of course, the GEM should be generic enough to provide all the services you need.

Please share with us what you decided, as there's no best answer for your question.



Related Topics



Leave a reply



Submit