Webpacker allows you to run an Angular, React, or Vue app within Rails. But what about Ember? Since it’s not built with Webpack we don’t get integration out of the box. But here’s the setup I’ve come to to run Ember within my Rails app.

Oftentimes I would want to keep Ember and Rails repos separate and hosted separately–it’s a nice separation of concerns. But considerations are different for Firehose, my open-source link-tracking app. I want people to be able to easily self-host it, so I don’t want them to have to set up separate API and frontend hosting. I’d like them to be able to deploy one app that runs both the API and the frontend.

The Goal

We want different setups in development and production.

In production we want the Ember app to be served by Rails. This means building the Ember app into the /public/ folder. It also means ensuring that any URL the user enters that isn’t a valid API URL will display the Ember app’s index.html page.

In development we want to let Rails and Ember servers run as normal, so we get live reloading of our Ember app. In general, differences between environments aren’t ideal, but for frontend development having live reloading is such a major productivity boost that it’s been embraced widely. We’ll test out our “production” setup locally to make sure it works.

Installation

Since our apps will be deployed together, let’s install them in the same repo. We’ll create a new directory for the Ember app at the root level of the Rails app. If you have an existing Ember app you can just move its directory there. If you’re creating a new Ember app, you can use the ember new command, with an extra flag:

cd my-cool-app
ember new --directory ember my-cool-app

Usually Ember names your directory the name of the app, but here we use the --directory flag to specify a different directory name. This is because we want the Ember app’s name to be the same as our overapp app name, but it’s more descriptive to name the directory ember. (You could call it frontend instead if you like.)

ember new also initializes a git repo in the app directory, so let’s delete that repo so it’s tracked as part of the Rails app repo just like any other directory:

rm -fr ember/.git

Note that since Ember is in a subdirectory, you’ll need to cd into it to run any Ember commands, but Rails commands will need to be run at the root.

Host Name

First let’s configure Ember with the correct host to use in development and production. In development they will be on separate hosts (really, separate ports on localhost). In production they will be on the same host, so we can just not specify a hostname.

First, we’ll set up an ENV variable with the host name. Add the following to config/environment.js:

   if (environment === 'development') {
     //...
+    ENV.apiHost = 'http://localhost:3000';
   }

We use the default Rails port of 3000; enter a different value if your setup is different.

Let’s assume our app is using Ember Data for data, and Ember Simple Auth for authentication. If so, we need to configure the host in two places.

Ember Data

For Ember Data, we add the host name to our application adapter. If you don’t already have an adapters/application.js file, generate one:

ember generate adapter application

Make the following changes to it:

 import DS from 'ember-data';
+import ENV from '../config/environment';

-export default DS.JSONAPIAdapter.extend({
+let options = {
   // any options you previously sent to extend() here, e.g. namespace
-});
+};
+
+if (ENV.apiHost) {
+  options.host = ENV.apiHost;
+}
+
+export default DS.JSONAPIAdapter.extend(options);

If ENV.apiHost is set (as in the development environment) then we add a host property to the options object set to that value. Otherwise, we leave the host property off.

Incidentally, if your Rails API isn’t already set to serve data from a subdirectory like /api/, it’s a good idea to set that up. Ember apps use all kinds of different routes, so if both your Ember app and API have a /blog-posts path, they’re going to conflict. By putting all your API routes under /api/ it prevents overlap.

Ember Simple Auth

Ember Simple Auth has its own config we need to update. Make the following changes to your authenticator, under app/authenticators/. For example, I’m using the OAuth2PasswordGrantAuthenticator, and it takes a serverTokenEndpoint property. Because it has a single property for both the host and path, we need to join them together ourselves:

 import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant';
+import ENV from '../config/environment';

+const serverTokenPath = '/api/oauth/token';
+const serverTokenEndpoint = ENV.apiHost
+  ? ENV.apiHost + serverTokenPath
+  : serverTokenPath;

 export default OAuth2PasswordGrant.extend({
-  serverTokenEndpoint: '/api/oauth/token',
+  serverTokenEndpoint,
 });

CORS

If you’re already running Ember and an API on different hosts then you’re likely familiar with Cross-Origin Resource Sharing (CORS). If not, CORS is a mechanism to allow frontend apps to make web service requests to different hosts. In production our Rails API and Ember app will be on the same host, so we don’t need CORS, and it’s safer to leave it out. But in development we need CORS to allow the Ember app on localhost:4200 to access the API on localhost:3000.

Check your Gemfile for the rack-cors gem and add it if it isn’t present:

+ gem 'rack-cors'

If you added it, run bundle install to install it.

Now add your CORS configuration. The rack-cors gem README shows it added to config/application.rb, but since we only want it enabled in development, let’s add it to config/environments/development.rb instead. You can add it right near the end of the file, before the end keyword closing the Rails.application.configure block:

   # Use an evented file watcher to asynchronously detect changes in source code,
   # routes, locales, etc. This feature depends on the listen gem.
   config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+
+  config.middleware.insert_before 0, Rack::Cors do
+    allow do
+      origins '*'
+      resource '*', :headers => :any, :methods => [:get, :post, :options]
+    end
+  end
 end

See the rack-cors documentation for details on how to customize this configuration. But this is good enough for now.

Running the Servers in Dev

Foreman is a useful tool for starting multiple services at once. Install it globally if you haven’t before:

gem install foreman

Then create a Procfile.dev in your app root directory with the following:

api: bundle exec rails server -p 3000
frontend: cd ember && ember serve -p 4200

We run both Rails and Ember using their normal server commands. But note that we explicitly pass in the apps’ default ports of 3000 for Rails and 4200 for Ember. This is because Foreman automatically assigns its own ports to processes, starting at 5000 and increasing by 100 for each separate process type. I prefer to keep Rails and Ember running on their default ports; that way if we want to run them individually outside of Foreman they’ll still work.

I also like to create a bin script to make it easy to run my servers. Create a bin/serve file and add the following:

#!/usr/bin/env bash

foreman start -f Procfile.dev

Then make it executable:

chmod +x bin/serve

Now you can run your servers by running bin/serve.

One note is that Ember can take a while to build, but Foreman’s output doesn’t show Ember’s “building” animation. Just be patient and wait until the logs indicate that both Rails and Ember are ready.

Building for Production

For Rails to serve our frontend assets in production, the built assets need to be in our Rails app’s /public/ folder. That folder is usually tracked by version control, but in Ember we usually don’t commit our built assets to version control. Because we want Ember to completely own the public assets that are shown for our site, we can delete the /public/ folder out of source control entirely.

rm -fr public
git add public
git commit -m "Remove public folder from source control"

We should also add public to our .gitignore file to prevent it from being re-added in the future.

Next, let’s configure Ember to build our assets to the public folder. Add the following to .ember-cli:

 {
-  "disableAnalytics": false
+  "disableAnalytics": false,
+  "output-path": "../public"
 }

Next, let’s set up a script to run the build. Create a bin/production with the following:

#!/usr/bin/env bash

cd ember
ember build
cd ..
bundle exec rails db:migrate
bundle exec rails server

Note that we also run our DB migration, because that’ll be helpful in production.

Next, we need to configure Rails to server our Ember index.html page for all routes that aren’t otherwise matched by Rails routes. This is so that users can enter paths other than the root of the site and still get the Ember app.

Let’s create a frontend_controller.rb and add the following:

# frozen_string_literal: true

class FrontendController < ApplicationController
  def index
    path = File.join(Rails.root, 'public', 'index.html')
    html = File.read(path)
    render html: html.html_safe
  end
end

This will serve the contents of the index.html file.

Now the magic happens — we set up our routes to use this controller action as a default, if no other route is matched. Add the following to your routes.rb file:

 Rails.application.routes.draw do
   # ...
+  get '*frontend_path', to: 'frontend#index'
 end

The * means that any routes not otherwise matched will be matched by this route. The route is saved to the variable frontend_path, but note that we don’t actually need to do anything with that on the server side. The Ember app running in the browser will detect the browser’s URL and use that to serve up the right Ember route.

Now let’s run our app and try it! Run bin/production, then go to http://localhost:3000. Your Ember app should come up and run just fine. Now try manually typing in a path other than the root of the site; it should work too.