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 theas
parameter. If we don't specifyas
, Rails will automatically name routes that don't contain dynamic segments (like :id): trim "/", and replace "/", "-" with "_". For example, the name ofget 'posts/comments'
isposts_comments
. In addition, the name ofroot 'index#index'
isroot
.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 callsurl_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
isENV['RAILS_RELATIVE_URL_ROOT']
, so you can also specify the environment variable when starting the application. In this case, I would recommend to useENV['RAILS_RELATIVE_URL_ROOT']
inconfig.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 useRails.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];
}