Docker, AngularJS and Tutum — Part 1

tutorial angular

Level: Beginner

This is part 1 of a 3 post series, where we will cover topics related to development, testing, Docker, continuous integration and continuous delivery.

Part 2 can be found here.

In this tutorial you will learn how to:

  • Set up a simple NodeJS and AngularJS app
  • Create a Docker image, push it to DockerHub and run a container
  • Unit tests with Karma and Mocha
  • Deploy to DigitalOcean and Continous Deployment with Tutum

The tools needed for development will be mentioned and explained throughout the tutorial.

All the code of this tutorial is available on github https://github.com/dciccale/docker-angular-tutum

After seeing this tweet, I will try to make this tutorial as easy to follow as possible.

Let’s get started.

Set Up a NodeJS and AngularJS App

First you need to download NodeJS (if not already installed).
For client dependencies, we will be using Bower. To install it run the following command in your terminal.

$ [sudp] npm install -g bower

Create a directory for your app

$ mkdir my_app

Create a package.json and a bower.json file, they should look similar to what’s in the repo or you can grab them from there directly (if you do that skip the next step and just run bower install and npm install).

$ npm init
... follow instructions
$ bower init
... follow instructions

Client app

Create a directory structure for our client code and change default bower_components location to be inside this directory by creating a .bowerrc file

$ mkdir -p client/app
$ mkdir client/app/main
$ mkdir client/app/about
$ mkdir -p client/components/navbar
$ echo '{"directory": "client/bower_components"}' > .bowerrc

The last command indicates to bower that all dependencies will be installed in client/bower_components

Install deps

$ bower install --save angular angular-ui-router bootstrap angular-bootstrap

We are going to use angular-ui-router, which is much more flexible and powerful, instead of angular’s. For now we’ll use it at a basic level but in following posts we’ll use more of what it provides.

Also notice that bootstrap css and angular-ui-bootstrap will be used.

Now create the scaffold of our AngularJS app:

Let’s do a quick explanation on what is inside these files.

In client/index.html we define our base html for the app, we load all css/js dependencies as well as our own files.

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible", content="IE=edge,chrome=1">
    <meta name="viewport", content="width=device-width, initial-scale=1.0">

    <!-- enable html5mode routes -->
    <base href="/">

    <title>Docker, AngularJS, Tutum</title>

    <!-- deps -->
    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap-theme.css">

    <!-- app -->
    <link rel="stylesheet" href="app/app.css">
  </head>

  <body ng-app="app">

    <!-- where ui-router load the views -->
    <div ui-view></div>

    <!-- deps -->
    <script src="bower_components/jquery/dist/jquery.js"></script>
    <script src="bower_components/angular/angular.js"></script>
    <script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
    <script src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>

    <!-- app -->
    <script src="app/app.js"></script>
    <script src="app/main/main.js"></script>
    <script src="app/main/main-controller.js"></script>
    <script src="app/about/about.js"></script>
  </body>
</html>

In client/app/app.js we define the basic angularjs bootstrap code, like configuration and some default values in the $rootScope.

// create a angular module named 'app'
angular.module('app', [
    'ui.bootstrap', // load angular-ui.bootstrap
    'ui.router' // load angular-ui-router
  ])
  // router options
  .config(['$urlRouterProvider', '$locationProvider',
    function ($urlRouterProvider, $locationProvider) {
    'use strict';

    $locationProvider.html5Mode(true); // allow html5mode routes (no #)
    $urlRouterProvider.otherwise('/'); // if route not found redirect to /
  }])
  // after the configuration and when app runs the first time we o some more stuff
  .run(['$rootScope', '$state', function ($rootScope, $state) {
    'use strict';
    // this is available from all across the app
    $rootScope.appName = 'app';

    // make $state available from templates
    $rootScope.$state = $state;
  }]);

 

In client/app/main/main.js we define specific configuration for our “main” section like routes. Instead of creating all routes in the app.js we define them on each section to make it more maintainable.

angular.module('app')
  .config(['$stateProvider', function ($stateProvider) {
    'use strict';

    $stateProvider.state('main', { // this is a name for our route
      url: '/', // the actual url path of the route
      templateUrl: 'app/main/main.html', // the template that will load
      controller: 'MainCtrl' // the name of the controller to use
    });
  }]);

In the template client/app/main/main.html we have a simple unordered list of items, and each item will be taken from some data source we will define in our controller. Read more about ng-repeat

<div ng-include="'components/navbar/navbar.html'"></div>

<div class="container">
  <h1>Main</h1>
  <div class="row">
    <div class="col-sm-6">
      <div class="panel panel-default">
        <div class="panel-body">
          <ul>
            <li ng-repeat="item in list1">{{item.label}}</li>
          </ul>
        </div>
      </div>
    </div>

    <div class="col-sm-6">
      <div class="panel panel-default">
        <div class="panel-body">
          <ul>
            <li ng-repeat="item in list2">{{item.label}}</li>
          </ul>
        </div>
      </div>
    </div>
  </div>
</div>

In client/app/main/main-controller.js we define some data that will be accessible from the template.

angular.module('app')
  .controller('MainCtrl', ['$scope', function ($scope) {
    // here we define the items to be repeated in the template
    $scope.list1 = [
      {label: 'one'},
      {label: 'two'},
      {label: 'three'}
    ];

    $scope.list2 = [
      {label: 'uno'},
      {label: 'dos'},
      {label: 'tres'}
    ];
  }]);

In client/app/about/about.js much of the same as the main.js but here we don’t define a controller, just the template to load.

angular.module('app')
  .config(['$stateProvider', function ($stateProvider) {
    'use strict';

    $stateProvider.state('about', {
      url: '/about',
      templateUrl: 'app/about/about.html'
    });
  }]);

and the client/app/about/about.html template:

<div ng-include="'components/navbar/navbar.html'"></div>

<div class="container">
  <h1>About</h1>
  <p>About page</p>
</div>

And finally the client/components/navbar/navbar.html:

Notice that there is some boilerplate for making the navbar responsive using the angular-ui-bootstrap collapse directive.

We are using the $state object in the template that we defined in our app.js and the $state.is() method to mark navigation items as active accordingly.

With ng-init we define the isCollapsed variable to false into the scope. Read more about ng-init

<div class="navbar navbar-default navbar-static-top" ng-init="isCollapsed = true">
  <div class="container">
    <div class="navbar-header">
      <button class="navbar-toggle" type="button" ng-click="isCollapsed = !isCollapsed">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a href="/" class="navbar-brand">{{appName}}</a>
    </div>
    <div collapse="isCollapsed" class="navbar-collapse collapse">
      <ul class="nav navbar-nav navbar-right">
        <li ng-class="{active: $state.is('main')}"><a ui-sref="main">Main</a></li>
        <li ng-class="{active: $state.is('about')}"><a ui-sref="about">About</a></li>
      </ul>
    </div>
  </div>
</div>

Server

Now that the client files are ready to be served, we need to create our server to display them in the browser.

We are going to use express.js plus a couple of other modules to make our server, so let’s install all those.

$ npm install --save express compression morgan errorhandler

To read more about what the other modules do here are the links:

  • compression (enable gzip/deflate compression)
  • morgan (log requests)
  • errorhandler (send error stack as response to the client if something failed)

Create the directory structure for our server code from the root of the project

$ mkdir -p server/config
$ mkdir -p server/components/errors
$ mkdir server/views

Now we will create all files needed for the server to run:

Let’s do a quick explanation on what is inside these files.

In server/app.js we define the code to run all our server:

'use strict';

// Set default node environment to development
process.env.NODE_ENV = process.env.NODE_ENV || 'development';

var express = require('express');
var config = require('./config');

// Setup server
var app = express();
var http = require('http');

// Express configuration
require('./config/express')(app);
// Route configutation
require('./routes')(app);

// Start server
http.createServer(app).listen(config.port, function () {
  console.log('Express server listening on %d, in %s mode', config.port, app.get('env'));
});

// Expose app
exports = module.exports = app;

In server/routes.js we define some configuration for the routes, like 404 and other routes.

'use strict';

var path = require('path');
var errors = require('./components/errors');

module.exports = function (app) {

  // All undefined asset routes should return a 404
  app.route('/:url(app|components|bower_components)/*')
   .get(errors[404]);

  // All other routes should redirect to the index.html
  app.route('/*')
    .get(function (req, res) {
      res.sendFile(path.join(app.get('appPath'), 'index.html'));
    });
};

in server/config/index.js we define some common configuration for the server:

'use strict';

var path = require('path');

module.exports = {
  // Environment
  env: process.env.NODE_ENV,

  // Root path of server
  root: path.normalize(path.join(__dirname, '../..')),

  // Server port
  port: 9000
};

And in server/config/express.js we setup express to serve our files.

'use strict';

var express = require('express');
var morgan = require('morgan');
var compression = require('compression');
var errorHandler = require('errorhandler');
var path = require('path');
var config = require('./index');

module.exports = function(app) {
  app.use(compression());
  app.use(express.static(path.join(config.root, 'client')));
  app.set('appPath', path.join(config.root, 'client')); // define the path of our app inside express to use across the server if needed
  app.use(morgan('dev'));
  app.use(errorHandler()); // error handler
};

In server/components/errors/index.js we define app errors like how 404 should behave and what to respond with.

'use strict';

var path = require('path');
var config = require('../../config');

module.exports[404] = function pageNotFound(req, res) {
  var viewFilePath = path.join(config.root, 'server/views/404.html');
  var statusCode = 404;
  var result = {
    status: statusCode
  };

  res.status(result.status);
  res.sendFile(viewFilePath, function (err) {
    // if the file doesn't exist of there is an error reading it just return a json with the error
    if (err) {
      return res.json(result, result.status);
    }
  });
};

And finally the server/views/404.html view

<!doctype html>
  <html>
    <head>
      <title>404 Not Found</title>
    </head>

    <body>
      <h1>404</h1>
    </body>
</html>

Now we are ready to start the server:

$ node server/app.js
Express server listening on 9000, in development mode

Open your browser and you should see the app in http://localhost:9000

Tests

Let’s write some tests with Mocha and Chai

For running the tests we will be using Karma test runner.

$ npm install --save-dev karma karma-mocha karma-chai karma-phantomjs-launcher karma-ng-html2js-preprocessor

Now we need to create a karma.conf.js file, grab it from the repo and put it in the root directory of the project.

And since we are writing tests for Angular, we will need angular-mocks, let’s install it and create the tests directory structure:

$ bower install angular-mocks
$ mkdir -p test/client/app
$ mkdir test/client/app/main
$ mkdir test/client/app/about

Create the following files:

See how we mimic the directory structure but with files ending in .test.js to easily know where the unit test is located for each file.

In test/client/app/app.test.js we add some tests to check the angular module is being created.

describe('app', function () {
  'use strict';
  // load our angular moule befor each test
  beforeEach(module('app'));

  describe('app tests', function () {
    it('should recognize our angular module', function () {
      expect(angular.module('app')).to.exist;
    });
  });
});

In test/client/app/main/main.test.js we test the main route.

describe('main', function () {
  'use strict';

  var $rootScope, $state;

  beforeEach(module('app'));
  beforeEach(module('app/main/main.html'));

  beforeEach(inject(function (_$rootScope_, _$state_) {
    $rootScope = _$rootScope_;
    $state = _$state_;
  }));

  describe('main tests', function () {
    it('should test routes', function () {
      $state.go('main');
      $state.transition.then(function () {
        expect($state.current.name).to.equal('main');
      });
      $rootScope.$digest();
    });
  });
});

In test/client/app/about/about.test.js we test the about route.

describe('about', function () {
  'use strict';

  var $rootScope, $state;

  beforeEach(module('app'));
  beforeEach(module('app/about/about.html'));

  beforeEach(inject(function (_$rootScope_, _$state_) {
    $rootScope = _$rootScope_;
    $state = _$state_;
  }));

  describe('about tests', function () {
    it('should test routes', function () {
      $state.go('about');
      $state.transition.then(function () {
        expect($state.current.name).to.equal('about');
      });
      $rootScope.$digest();
    });
  });
});

In test/client/app/main/main-controller.test.js we test that we have some values in the $scope, in this case the two lists.

describe('about', function () {
  'use strict';

  var $rootScope, $state;

  beforeEach(module('app'));
  beforeEach(module('app/about/about.html'));

  beforeEach(inject(function (_$rootScope_, _$state_) {
    $rootScope = _$rootScope_;
    $state = _$state_;
  }));

  describe('about tests', function () {
    it('should test routes', function () {
      $state.go('about');
      $state.transition.then(function () {
        expect($state.current.name).to.equal('about');
      });
      $rootScope.$digest();
    });
  });
});

Run tests

Add/modify the test command in the package.json file to be:

"scripts": {
  "test": "node ./node_modules/karma/bin/karma start --single-run"
}

Now you will be able to run the tests with

$ npm test

Docker

Installation

Check their website on how to install Docker for Mac OS X

One way is using Homebrew

1) Install Homebrew

$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

2) Install Homebrew Cask (for installing bin files through Homebrew)

$ brew install caskroom/cask/brew-cask

3) Install VirtualBox (needed to run Docker in Mac OS X)

$ brew cask install virtualbox

4) Install Docker

$ brew install docker

5) Install boot2docker (a virtual machine to run Docker)

$ brew install boot2docker

Start Docker

To start Docker run:

$ boot2docker up

You should see a similar output:

Waiting for VM and Docker daemon to start...
..........ooo
Started.
Writing /Users/<user>/.boot2docker/certs/boot2docker-vm/ca.pem
Writing /Users/<user>/.boot2docker/certs/boot2docker-vm/cert.pem
Writing /Users/<user>/.boot2docker/certs/boot2docker-vm/key.pem

To connect the Docker client to the Docker daemon, please set:
    export DOCKER_TLS_VERIFY=1
    export DOCKER_HOST=tcp://192.168.59.103:2376
    export DOCKER_CERT_PATH=/Users/<user>/.boot2docker/certs/boot2docker-vm

So follow the instructions to connect the Docker client, use the values provided in your terminal, (i.e. user should be your user)

$ export DOCKER_TLS_VERIFY=1
$ export DOCKER_HOST=tcp://192.168.59.103:2376
$ export DOCKER_CERT_PATH=/Users/<user>/.boot2docker/certs/boot2docker-vm

Now you should be able to run something like docker version to check docker is running.

So, what is Docker?

Watch an introduction video to Docker in the official site https://www.docker.com/whatisdocker/

Basically, it lets you create an isolated container with all the files such as dependencies and binaries for your app to run, making it easier to ship and deploy.

Dockerfile

To tell Docker what to include in the container we first need to create an image from a Dockerfile definition.

Let’s create the Dockerfile in the root directory of our project.

# This image will be based on the official nodejs docker image
FROM node:latest

# Set in what directory commands will run
WORKDIR /home/app

# Put all our code inside that directory that lives in the container
ADD . /home/app

# Install dependencies
RUN \
    npm install -g bower && \
    npm install && \
    bower install --config.interactive=false --allow-root

# Tell Docker we are going to use this port
EXPOSE 9000

# The command to run our app when the container is run
CMD ["node", "server/app.js"]

Create Docker image

So, as you can see everything is commented in the Dockerfile to understand what steps are taken for creating the image.

To finally create the image we need to run

$ docker build -t app .

To see the newly created image run docker images and you should see something like:

REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
app                 latest              4ad898544bec        4 minutes ago       751.6 MB

Run a Docker container

To run a container using the image we just created run:

$ docker run -d --name my_app -p 80:9000 app

This will:
– run a container in the background (detached -d with the --name my_app
– map port -p 80 to port 9000
– from the image named app

Now the container is running. To see our app inside the container we need to know the ip of boot2docker virtual machine:

$ boot2docker ip

With that, go to your browser and enter that ip address. You should see the app running.

Other Docker commands

To see running containers use docker ps

To see all containers use docker ps -a

To stop a container use docker stop [container_id_or_name]

To start an existing container use docker start [container_id_or_name]

To see the logs of a container use docker logs -f [container_id_or_name] -f is optional, will keep STDIN attached to current terminal

Push your image to DockerHub

DockerHub is kind of like Github but for Docker images.

We are going to add an Automated build repository in DockerHub. For that, we first need to push the code to Github.

See how to create a repository on github

Then link your Github account with DockerHub to add an automated build repo:

See how to add automated build repo in DockerHub

After adding your repo, you should see the build status of your image in the Build Details tab.

Creating an automated build repo means that every time you make a push to your github repo, a build will be triggered in DockerHub to build your new image.

In the next section we will see how to deploy your app in DigitalOcean automatically after each of those builds to publish the latest changes.

Tutum & Continuous Delivery

Tutum is a platform that helps you manage your software deployment lifecycle into any cloud service provider.

1) Sign up with your Github account in Tutum
2) Sign up in DigitalOcean
3) Link your DigitalOcean account with Tutum
4) Go to Tutum Dashboard and Create your first Node

Now we need to add a tutum.yml defining the stack to deploy in the created node.

tutum.yml

In the image field you should put your docker_hub_username/your_image_repo

web:
  image: dciccale/docker-angular-tutum
  autorestart: always
  ports:
    - "80:9000"

Now go to Tutum dashboard to create a new stack and drop the file or copy and paste the content and write a name for your stack. I will use docker-angular-tutum

Click on Create and Deploy. This will deploy your container in the emptiest node (that means, the one you just created).

Ok! so now we have our Dockerized nodejs-angularjs app running on DigitalOcean!

To see the live app go to the “Nodes” tab and get the DigitalOcean ip address that you can enter in your browser and see the app running from the container. In my case that is: http://46.101.189.72

Or you could use the auto-generated url by Tutum http://web-1.docker-angular-tutum.dciccale.cont.tutum.io, which you can find under the Services tab > clicking on your web service > clicking on the web-1 container.

Automatic deploy

Now we want DockerHub to tell Tutum to re-deploy the container whenever a new image is built.

1) Go to Tutum dashboard services list and click on the web service
2) Click on Webhooks tab and create a new webhook named something like redeploy
3) Copy the URL, go to your DockerHub repository, on the right sidebar click Webhooks then click on Add Webhook, put a Short name like redeploy-tutum, paste the URL there and save.

Now every time a successful build of your Docker image is done in DockerHub, Tutum will re-deploy the container with the latest image. How cool is that?

Next

This is not all, the next post level will be intermediate and we will cover:

  • Improve client app development with Gulp

  • Create a Development, Test and Production environment

  • Create an API with MongoDB as data store to run on its own container

  • Service orchestration with docker-compose

  • Run everything with Tutum

Tagged with: , , , ,
Posted in Tutorial
9 comments on “Docker, AngularJS and Tutum — Part 1
  1. How can I make the /home/apps directory in the container a writable volume? I want to write from OSX. I tried using “.:/home/app” but then I got an error about the express npm module not being found.

  2. Getting the error 10:59:36.498 Error: [$location:nobase] $location in HTML5 mode requires a tag to be present!
    http://errors.angularjs.org/1.4.0/$location/nobase1 angular.js:68:11

    after the section before the testing

  3. […] It is highly recommended to first read Docker, AngularJS and Tutum – Part 1 […]

  4. […] Docker, AngularJS and Tutum — Part 1 […]

  5. […] Source: Docker, AngularJS and Tutum — Part 1 | Tutum Blog […]

  6. daniel says:

    So, have a question. Using this dockerfile, we can start the node.js server. However, how to access to the web?

Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Categories
%d bloggers like this: