How to Deploy an Ember CLI app to Amazon S3 or a Linux box

At some point, we all baked our own deployment scripts. But they tend to be buggy and hard to extend. What if there was a standardized, battle-tested approach to Ember deployment?

Ember CLI Deploy is the go-to solution for deploying Ember apps. It ships with best practices to successfully deploy Ember in a real world staging/production environment.

Based on a modular architecture (a set of hooks in a pipeline) virtually any functionality can be implemented through plugins. Even the most basic action, build, is a plugin. To make things more convenient, plugins can be packaged in “packs” and be installed in one go (example). It’s easy to create our own, should we ever need something more custom.

To get started, we should pick any new or existing application (I’ll use a new one) and deploy it to a production server.

Build and prepare

Once inside our app’s directory, installing Ember CLI Deploy is the first step:

$ ember install ember-cli-deploy

We will go through the basic plugins one by one (no “packs”) so as to understand the process:

So we’ll install them:

$ ember install ember-cli-deploy-build
$ ember install ember-cli-deploy-revision-data
$ ember install ember-cli-deploy-display-revisions
$ ember install ember-cli-deploy-gzip

What are revisions?

As index.html is our app’s main file and entry point, it will request certain assets (JS, CSS, etc) to be loaded. It will therefore determine the running version of the application.

Revision data is appended to our index.html, for example resulting in index.html:d307bff and uploaded like that. Once we choose which version we want in production, we activate it – that is, we symlink index.html:d307bff => index.html. More on this later.

This is the reason why some plugins target the index file and others the assets, as we’ll in the next sections.

Moving forward, let’s have a look at Ember CLI Deploy’s configuration file:

// config/deploy.js

module.exports = function(deployTarget) {

  var ENV = {
    build: {
      environment: deployTarget
    }
  };

  return ENV;

};

ENV is the object that holds config information of the various plugins.

For instance, the build object is configuration for the build plugin (more in the docs).

About environments

deployTarget is the actual environment (development, staging, production). It was named deployTarget to prevent clashing with the Ember CLI concept of environment.

This argument comes from the command-line, for example, ember deploy production. Production is what we’ll use throughout this guide.

For staging config, one would return a different ENV based on deployTarget == 'staging'.

Revisions: we want them to be based on Git commit SHAs. The revision Data plugin can generate one of: file-hash, git-tag-commit, git-commit, version-commit:

// config/deploy.js

module.exports = function(deployTarget) {

  var ENV = {
    build: {
      environment: deployTarget
    },
    'revision-data': {
      type: 'git-commit'
    }
  };

  return ENV;

};

Let’s give it a whirl!

$ ember deploy production --verbose

Registering hook -> configure[build]
Registering hook -> build[build]
Registering hook -> configure[display-revisions]
Registering hook -> configure[gzip]
Registering hook -> willUpload[gzip]
Registering hook -> configure[revision-data]
Registering hook -> prepare[revision-data]
Executing pipeline
|
+- configure
|  |
|  +- build
|    - validating config
|    - Missing config: `outputPath`, using default: `tmp/deploy-dist`
|    - config ok
|  |
|  +- display-revisions
|    - validating config
|    - Missing config: `amount`, using default: `[Function]`
|    - config ok
|  |
|  +- gzip
|    - validating config
|    - Missing config: `filePattern`, using default: `**/*.{js,css,json,ico,map,xml,txt,svg,eot,ttf,woff,woff2}`
|    - Missing config: `zopfli`, using default: `false`
|    - Missing config: `keep`, using default: `false`
|    - Missing config: `distDir`, using default: `[Function]`
|    - Missing config: `distFiles`, using default: `[Function]`
|    - config ok
|  |
|  +- revision-data
|    - validating config
|    - Missing config: `filePattern`, using default: `index.html`
|    - Missing config: `distDir`, using default: `[Function]`
|    - Missing config: `distFiles`, using default: `[Function]`
|    - Missing config: `versionFile`, using default: `package.json`
|    - config ok
|
+- setup
|
+- willDeploy
|
+- willBuild
|
+- build
|  |
|  +- build
|    - building app to `tmp/deploy-dist` using buildEnv `production`...
|    - ✔  assets/todo-app-7d5943d245e93fcf82e262e13580863a.js
|    - ✔  assets/todo-app-d41d8cd98f00b204e9800998ecf8427e.css
|    - ✔  assets/vendor-d41d8cd98f00b204e9800998ecf8427e.css
|    - ✔  assets/vendor-edfe68d7b279bd3b33a06f5a84e10349.js
|    - ✔  crossdomain.xml
|    - ✔  index.html
|    - ✔  robots.txt
|    - build ok
|
+- didBuild
|
+- willPrepare
|
+- prepare
|  |
|  +- revision-data
|    - creating revision data using `git-commit`
|    - generated revision data for revision: `0909dff`
|
+- didPrepare
|
+- willUpload
|  |
|  +- gzip
|    - gzipping `**/*.{js,css,json,ico,map,xml,txt,svg,eot,ttf,woff,woff2}`
|    - ✔  assets/todo-app-d41d8cd98f00b204e9800998ecf8427e.css
|    - ✔  assets/vendor-d41d8cd98f00b204e9800998ecf8427e.css
|    - ✔  assets/todo-app-7d5943d245e93fcf82e262e13580863a.js
|    - ✔  crossdomain.xml
|    - ✔  robots.txt
|    - ✔  assets/vendor-edfe68d7b279bd3b33a06f5a84e10349.js
|    - gzipped 6 files ok
|
+- upload
|
+- didUpload
|
+- willActivate
|
+- activate
|
+- didActivate
|
+- didDeploy
|
+- teardown
|
Pipeline complete

Ember CLI Deploy 0.5.x disabled all output (should be fixed soon). We need to call --verbose to see what’s going on. As a bonus, we can look closer at how plugins use the different pipeline hooks.

The result of this command, basically, is a bunch of production-ready JS and CSS files in a certain folder, gzipd, and ready to be deployed… somewhere.

Ember best practices delivered straight to your inbox? Tell me where:

(One e-mail every month. No BS. Unsubscribe anytime!)

Deploying everything to S3

To proceed with the deployment to S3, we will install these plugins:

$ ember install ember-cli-deploy-s3-index
$ ember install ember-cli-deploy-s3

Ready? Config time!

// config/deploy.js

module.exports = function(deployTarget) {

  var ENV = {
    build: {
      environment: deployTarget
    },
    'revision-data': {
      type: 'git-commit'
    },
    's3-index': {
      accessKeyId: "YOUR4CC3SSK3Y",
      secretAccessKey: "s3cr3t/4ccessKey",
      bucket: "your-app-deployment-bucket",
      region: "us-east-1",
      allowOverwrite: true
    },
    's3': {
      accessKeyId: "YOUR4CC3SSK3Y",
      secretAccessKey: "s3cr3t/4ccessKey",
      bucket: "your-app-deployment-bucket",
      region: "us-east-1"
    }
  };

  return ENV;

};

Important: I know I know! You’re tempted to DRY up that config. But don’t. Don’t share configuration objects between these plugins! This can currently lead to issues. Authors are working on a fix.

In a real-world setting we probably don’t want to hard-code paths or commit any sensitive security information. Instead, we can make use of environmental variables within our config:

// config/deploy.js

// ...

    's3-index': {
      accessKeyId: process.env['S3_ACCESS_KEY'],
      secretAccessKey: process.env['S3_SECRET_ACCESS_KEY'],

// ...

If you need to read configuration from a file, it’s also possible to return a promise that resolves with the ENV object. Ain’t that great news?!

On to the bucket…

Creating the bucket in S3 console

Now that we’ve dealt with our config, we’re free to launch the deploy!

$ ember deploy production --verbose --activate=true

# ...

+- upload
|  |
|  +- s3
|    - Using AWS access key id and secret access key from config
|    - preparing to upload to S3 bucket `your-app-deployment-bucket`
|    - ✔  assets/todo-app-7d5943d245e93fcf82e262e13580863a.js
|    - ✔  assets/todo-app-d41d8cd98f00b204e9800998ecf8427e.css
|    - ✔  crossdomain.xml
|    - ✔  robots.txt
|    - ✔  assets/vendor-d41d8cd98f00b204e9800998ecf8427e.css
|    - ✔  assets/vendor-edfe68d7b279bd3b33a06f5a84e10349.js
|    - uploaded 6 files ok
|  |
|  +- s3-index
|    - preparing to upload revision to S3 bucket `your-app-deployment-bucket`
|    - ✔  index.html:0909dff
|
+- didUpload
|
+- willActivate
|
+- activate
|  |
|  +- s3-index
|    - preparing to activate `0909dff`
|    - ✔  index.html:0909dff => index.html
|
+- didActivate
|
+- didDeploy
|
+- teardown
|
Pipeline complete

Yup, we asked it to activate the current revision!

If we navigate to our address http://your-app-deployment-bucket.s3-website-us-east-1.amazonaws.com/ we get an error:

Changing the settings in Properties -> Static Website Hosting will allow us to serve the app:

Notice that we’re adding index.html as Error Document so that routing with pretty URLs works in our client-side app. Probably a hack-y workaround. For a proper solution, check S3 website configuration documentation.

New attempt gives us…

That link to /todos will properly use the History API

Cool, so let’s update the title of our app. Or any other visible change, really. We should not forget to Git commit, so that the commit-SHA can be used as a new revision hash by the Revisions plugin.

We can query all revisions in the server:

$ ember deploy:list production --verbose
# ...
+- displayRevisions
|  |
|  +- display-revisions
|    -   timestamp           | revision
|    - =================================
|    - > 2016/01/18 16:30:37 | 563bd46
|    -   2016/01/18 16:26:12 | 0909dff
# ...

To activate a particular revision we can use the following command:

$ ember deploy:activate production --revision=0909dff --verbose
# ...
+- activate
|  |
|  +- s3-index
|    - preparing to activate `0909dff`
|    - ✔  index.html:0909dff => index.html
|
+- didActivate
# ...

What we just did was roll back to a previous version of the app.

Deploying with SSH and Rsync

For classic deployment to a Linux box, we can make use of tools like SSH and Rsync through Ember CLI Deploy.

The plugins we will use are:

Installation:

$ ember install ember-cli-deploy-ssh-index
$ ember install ember-cli-deploy-rsync

Naturally, we need to provide these plugins some information about our server:

// config/deploy.js

module.exports = function(deployTarget) {

  var ENV = {
    build: {
      environment: deployTarget
    },
    'revision-data': {
      type: 'git-commit'
    },
    'ssh-index': {
      remoteDir: "/usr/local/www/myapp",
      username: "www",
      host: "192.168.0.136",
      privateKeyFile: "/Users/me/.ssh/id_rsa",
      allowOverwrite: true
    },
    rsync: {
      dest: "/usr/local/www/myapp",
      username: "www",
      host: "192.168.0.136",
      delete: false
    }
  };

  return ENV;

};

As we saw when deploying with S3, we can use process.env[] to access environmental variables (in lieu of hard-coding sensitive data).

Why define delete: false in rsync?

If we pass delete: true, rsync will remove all extraneous files from the destination – including our index.html:* revision files.

Disabling plugins

If we previously deployed using S3 but want to change strategy, we need to disable the S3 plugins. Removing the packages from package.json and deleting the corresponding configuration from config/deploy.js is enough.

Uploading gzipd files (depending on web server configuration) can cause issues with file integrity. When using rsync I disable ember-cli-deploy-gzip and let nginx handle compression.

Let’s hit it!

$ ember deploy production --verbose --activate=true

# ...

+- upload
|  |
|  +- rsync
|    - Uploading using rsync...
|    -
|  |
|  +- ssh-index
|    - Attempting to connect to remote host: www@192.168.0.136
|    - Successfully connected to remote host
|    - Attempting to establish an SFTP session
|    - SFTP session established
|    - Writing /usr/local/www/myapp/index.html:84aed2d to remote host
|    - ✔  /usr/local/www/myapp/index.html:84aed2d
|
+- didUpload
|
+- willActivate
|
+- activate
|  |
|  +- ssh-index
|    - Attempting to connect to remote host: www@192.168.0.136
|    - Successfully connected to remote host
|    - Attempting to establish an SFTP session
|    - SFTP session established
|    - ✔  /usr/local/www/myapp/index.html:84aed2d => /usr/local/www/myapp/index.html
|
+- didActivate
|
+- didDeploy
|  |
|  +- rsync
|
+- teardown
|
Pipeline complete

Result:

In the same way, we query all available revisions:

$ ember deploy:list production --verbose
# ...
+- displayRevisions
|  |
|  +- display-revisions
|    -   timestamp           | revision
|    - =================================
|    -   2016/01/18 18:04:07 | 84aed2d
|
# ...

Phew! We can now grab a cup of coffee and relax: our app is deployed!

Sample nginx configuration: Routing with pretty URLs

server {
    listen 80;
    server_name ember.app;

    gzip on;
    gzip_min_length  1100;
    gzip_buffers  4 32k;
    gzip_types text/plain application/javascript application/json application/x-javascript text/css;
    gzip_vary on;

    root /usr/local/www/myapp;

    location / {
      try_files $uri /index.html;
    }

    location /api {
      proxy_pass http://127.0.0.1:8000;
    }

}

Mixed strategies

Now that we’ve seen many plugins and understand how they work, we can mix and match. So we could easily have S3 for assets and manage the index in our Linux box, whether in the filesystem, with Redis, or any other!

What is your current deployment strategy? Facing any challenges? Let me know in the comments below!

Enjoyed this article? Don't miss my next one!

Leave me your e-mail for content that will help you master Ember:

Do you want to master Ember fast?

Leave me your e-mail for helpful updates delivered straight to your inbox.

(A few e-mails per month. No BS. Unsubscribe anytime!)