How to dockerize an Ember app

In model-based sidebars in ember I created a book collection manager called bookworm to demonstrate model-based sidebars. Then in track table selections in Ember I added table selection tracking.

In this post I'll show you how to use docker to create separate, isolated production and development environments.

To make it interesting we'll first update bookworm to use a real backend server and database.

Backend

The backend is a json:api server that uses mongo as its data store. It uses the jsonapi-server library with the jsonapi-store-mongodb handler.

"use strict";

var jsonApi = require("jsonapi-server");  
var MongoStore = require("jsonapi-store-mongodb");

jsonApi.setConfig({  
  protocol: "http",
  hostname: "localhost",
  port: 3000,
  base: "api",
});

var authorsHandler = new MongoStore({  
  url: "mongodb://bookworm_mongo_1:27017/bookworm",
});

var booksHandler = new MongoStore({  
  url: "mongodb://bookworm_mongo_1:27017/bookworm",
});

jsonApi.define({  
  resource: "authors",
  handlers: authorsHandler,
  attributes: {
    name: jsonApi.Joi.string(),
    books: jsonApi.Joi.belongsToMany({
      resource: "books",
      as: "author",
    }),
  },
  examples: [
    {
      id: "3092b4f2-a6b3-42bf-817f-acffffe2d9bc",
      type: "authors",
      name: "Hugh Howey",
    },
  ]
});

jsonApi.define({  
  resource: "books",
  handlers: booksHandler,
  attributes: {
    title: jsonApi.Joi.string(),
    author: jsonApi.Joi.one("authors"),
  },
  examples: [
    {
      id: "32fb0105-acaa-4adb-9ec4-8b49633695e1",
      type: "books",
      title: "Wool",
      author: {
        type: "authors",
        id: "3092b4f2-a6b3-42bf-817f-acffffe2d9bc"
      }
    },
  ]
});

jsonApi.start();

setTimeout(function() {  
  authorsHandler.populate(function() {
    console.log("Loaded authors");
  });
  booksHandler.populate(function() {
    console.log("Loaded books");
  };
}, 10000);

During development we want to be able to edit code on the host without having to rebuild the docker image and recreate the container. To facilitate this, gulp is used to watch for file changes. When a change occurs, the backend server is automatically restarted in the running container.

'use strict';

var gulp = require('gulp');  
var nodemon = require('gulp-nodemon');  
var spawn = require('child_process').spawn;

gulp.task('default', ['nodemon'], function () {  
});

gulp.task('nodemon', function(cb) {  
  var started = false;

  return nodemon({
    script: 'app/server.js',
  }).on('start', function() {
    // to avoid nodemon being started multiple times
    if (!started) {
      cb();
      started = true;
    }
  });
});

Frontend changes

To ensure that the frontend uses the backend, ember-cli-mirage should be disabled in development:

if (environment === 'development') {  
  ENV['ember-cli-mirage'] = {
    enabled: false
  };
}

The application adapter needs to be changed so that Ember-Data requests are sent to the backend:

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

export default DS.JSONAPIAdapter.extend({  
  host: config.ApiUrl,
  namespace: config.ApiPrefix
});

The values for the backend endpoints are pulled from the configuration file:

ENV.ApiUrl = 'http://localhost:3000';  
ENV.ApiPrefix = 'api';  

Docker

Now that we have a working backend and the required changes in the frontend, we can get to work setting the project up to use docker. The frontend and backend both need a Dockerfile and the project as a whole needs two docker-compose.yml files, one used in production and the other in development. On the filesystem, it should look like this:

bookworm  
├── backend
│   └── Dockerfile
├── frontend
│   └── Dockerfile
├── docker-compose-dev.yml
└── docker-compose.yml

The frontend Dockerfile is based on the node image from the docker hub. It copies the code to /app, installs the npm and bower dependencies, exposes the ember application and live-reload ports, and starts the Ember server:

FROM node:4.2

RUN mkdir -p /app  
WORKDIR /app

# Copy package.json separately so it's recreated when package.json
# changes.
COPY package.json ./package.json  
RUN npm -q install  
COPY . /app  
RUN npm -q install -g phantomjs bower ember-cli ;\  
    bower --allow-root install

EXPOSE 4200  
EXPOSE 49152

CMD [ "ember", "server" ]  

The backend Dockerfile works in a similar way. After dependencies are installed, the gulp is started via npm:

FROM node:4.2

RUN mkdir -p /app  
WORKDIR /app

# Copy package.json separately so it's recreated when package.json
# changes.
COPY package.json ./package.json  
RUN npm -q install  
COPY . /app

EXPOSE 3000

CMD [ "npm", "start" ]  

These dockerfiles are enough to start the frontend and backend; however, this gets tedious very quickly. docker-compose can help us.

The first docker-compose file will bring up the system for production use:

frontend:  
    build: "frontend/"
    dockerfile: "Dockerfile"
    environment:
        - EMBER_ENV=production
    ports:
        - "4200:4200"
        - "49152:49152"
backend:  
    build: "backend/"
    dockerfile: "Dockerfile"
    ports:
        - "3000:3000"
mongo:  
    image: mongo
    ports:
        - "27017:27017"

The second docker-compose file extends the configuration above, setting up a development environment. The main difference is that Ember runs in development mode, and the source on the host is mounted in the container as a volume. This means we can edit code on the host system and, thanks to the live-reload mechanism from Ember and gulp, see the changes immediately.

frontend:  
    environment:
        - EMBER_ENV=development
    volumes:
        - "./frontend:/app"
backend:  
    volumes:
        - "./backend:/app"

In the root of the project, we can build the images for each container with the command:

docker-compose --x-networking build

The --x-networking option is a new feature of docker-compose that replaces links. It setups up networks so that containers can communicate with one another. For more information see Networking in Compose in the Docker docs.

Bring up the stack

The entire application stack can then be brought up in production mode with:

docker-compose --x-networking up

To bring the entire application stack up in development mode:

docker-compose --x-networking -f docker-compose.yml -f docker-compose-dev.yml

The following services are now accessible on the host:

  • Frontend: localhost:4200
  • Backend: localhost:3000
  • Mongo: localhost:27017

Using docker has made the transition from isolated frontend development to full integrated development a pleasure, and I hope this guide will help you achieve the same.

A complete working example of the code can be found on GitHub in the repository jonblack/bookworm under the tag docker.

If you have comments or tips, or think there's a better way to use Docker with Ember, leave a comment or get in touch via Twitter.

Author image
Creator of Humble Coder and serial hobbyist