Sometimes you may want to deploy a Rails application to sub-uri. But it's not seamless if you didn't write your code carefully. The main problem is absolute url.

Avoid absolute url

By default, Rails application only work under root uri unless properly configured. And (maybe) most developers are supposing that the application will be deployed under root uri when writing code. So absolute url is widely used, and they break when deployed under sub-uri. So we should avoid using absolute url. Use relative url or url helpers instead. The are two types of url in Rails: asset url and route url.

Asset url

Since most Rails applications are using Rails Asset Pipeline to manage assets, asset urls are usually generated by asset url helpers like asset_path, asset_url, image_path, etc. These helpers will take care of sub-uri for you if you have configured properly. (See AssetUrlHelper for more helpers and their usage. These helpers are available in view layer. To use them in other places, prefix them with ActionController::Base.helpers.) However, if you put assets directly under public folder, then it's your own responsibility to take care of sub-uri. In such case, use relative url or prefix with Rails.configuration.relative_url_root.

Asset Pipeline

You must set RAILS_RELATIVE_URL_ROOT environment variable when precompiling assets, otherwise url generated by url helpers (such as image-url in scss) would not include sub-uri.

RAILS_RELATIVE_URL_ROOT=/sub-uri bundle exec rake assets:precompile

If you use some deployer (such as mina, capistrano) to automatically precompile assets in production environments, then you should refer to the deployer's documentation about how to pass environment variables to precompiling task. For mina 0.3.x, you can set env_vars in your deploy.rb:

set :env_vars, 'RAILS_RELATIVE_URL_ROOT=/sub-uri'

Route url

Suppose we have properly configured to deploy to sub-uri /app, and we have the following routes defined in config/route.rb:

root 'index#index'
get 'posts', to: 'posts#index'
get 'posts/comments', to: 'posts#comments'
get 'posts/:post_id/comments/:comment_id', to: 'comments#show', as: 'post_comment'

There are two methods to generate proper route url:

  • Named route helper

    We can name a route and then use [ROUTE_NAME]_path or [ROUTE_NAME]_url to generate url. The name of a route can be specified by the as parameter. If we don't specify as, Rails will automatically name routes that don't contain dynamic segments (like :id): trim "/", and replace "/", "-" with "_". For example, the name of get 'posts/comments' is posts_comments. In addition, the name of root 'index#index' is root.

    root_path                 # /app/
    posts_comments_path       # /app/posts/comments
    post_comment_path(1, 1)   # /app/posts/1/comments/1
    post_comment_path(1, 1, query: 'test')                      # /apps/vote/posts/1/comments/1?query=test
    post_comment_path(post_id: 1, comment_id: 1, query: 'test') # /apps/vote/posts/1/comments/1?query=test
    

    resources method also automatically name the routes. See Rails Routing from the Outside In for more about naming route.

  • url_for

    This helper is available in both view and controller. You can pass it the symbol representation of a route name, or detailed options.

    url_for(:root)                                 # /app/
    url_for(:posts_comments)                       # /apps/vote/posts/comments
    url_for(controller: 'posts', action: 'index')  # /apps/vote/posts
    url_for(controller: 'comments', action: 'show', post_id: 1, comment_id: 1)                # /apps/vote/posts/1/comments/1
    url_for(controller: 'comments', action: 'show', post_id: 1, comment_id: 1, query: 'test') # /apps/vote/posts/1/comments/1?query=test
    

    See UrlFor for more. There are tag helpers like link_to to generate <a> tag. In fact inside these tag helpers Rails calls url_for with url_options passed to the helper to generate url.

Configuration

If you have written your code carefully, it's quite easy to make your application work under sub-uri. Usually both web server and application should be configured. As for web servers, I'll only show the nginx configuration in the following. Configuration for other servers may or may not be similar. There are two kinds of configuration: one general and the other Passenger specific. Suppose we want to deploy to the sub-uri /app.

General configuration

This method works for all application servers, like Passenger, Puma, Unicorn.

  • Application configuration

    # config.ru
    map '/app' do
      run Rails.application
    end
    
    # config/application.rb
    config.relative_url_root = '/app'
    

    Whether the sub-uri ends with "/" or not seems not matter. In fact, the default value for config.relative_url_root is ENV['RAILS_RELATIVE_URL_ROOT'], so you can also specify the environment variable when starting the application. In this case, I would recommend to use ENV['RAILS_RELATIVE_URL_ROOT'] in config.ru too, so you can change sub-uri without changing any code. If you need to get the sub-uri in your application code, it'll be wise to use Rails.configuration.relative_url_root rather than hard code the sub-uri.

  • Nginx configuration

    # use nginx to serve static files
    # Note: the values of location and alias should be consistent about whether ending with "/" or not
    location /app/ {
        alias /[PATH-TO-APPLICATION]/public/;
        # do not include "$uri/" in try_files, otherwise @my-app not work
        try_files $uri @my-app;
    }
    
    location @my-app{
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass [SOCKET/HTTP_ADDRESS];
    }
    

Passenger specific configuration

Passenger makes it more simpler. Changing nginx configuration is enough:

# use nginx to serve static files
# Note: the values of location and alias should be consistent about whether ending with "/" or not
location /app/ {
    alias /data/app/liveneeq-vote/current/public/;
    # do not include "$uri/" in try_files, otherwise @my-app not work
    try_files $uri @vote_app;
}

location @vote_app {
    passenger_enabled on;
    passenger_base_uri /app;
    passenger_env_var RAILS_RELATIVE_URL_ROOT /app;
    passenger_app_root [PATH-TO-APPLICATION];
}