Wednesday, June 10, 2015

Series: How to create your own website based on Docker (Part 9 - Creating the nginx/Angular 2 web site Docker container)

It's about time to add some frontend logic to our project

This is part 9 of the series: How to create your own website based on Docker.

In the last two parts we've created the whole backend (REST API + database), so it's about time to create the website that makes use of it. Since we have a simple person REST API (see part 8), we need a site that can list all persons as well as create new ones. Since Angular 2.0 has achieved "Developer Preview" status, this sounds like a perfect demo for Angular 2.0!

A word of advice: Angular 2.0 is not production ready yet! This little project is perfect for playing around with the latest version of Angular 2.0, but please do not start new projects with it, since a lot of new changes are going to be introduced until the framework is officially released. If you have ever played around with Angular 2.0 alpha, you probably don't want to use it for production anyways... It's still very very unstable and the hassle with the typings makes me sad every time I use Angular 2.0. But this will change some time soon and then we'll be able to work Angular like pros! :)

Source code

All files mentioned in this series are available on Github, so you can play around with it! :)

Technologies to be used

Our website will use the following technologies (and to make sure that we have full support of everything, I recommend to use the latest greatest Chrome):

First things first - creating the nginx image

Creating the nginx image is basically the same every time. Let's create a new directory called /opt/docker/projectwebdev/ and within this new directory we'll create other directories called config and html as well as our Dockerfile:
# mkdir -p /opt/docker/projectwebdev/config/
# mkdir -p /opt/docker/projectwebdev/html/
# > /opt/docker/projectwebdev/Dockerfile
After creating the fourth Dockerfile, you should  now be able to understand what this file is doing, so I'm not going into details now - as a matter of fact, this is a pretty easy one.
# Pull base image.
FROM docker_ubuntubase

ENV DEBIAN_FRONTEND noninteractive

# Install Nginx.
RUN \
  add-apt-repository -y ppa:nginx/stable && \
  apt-get update && \
  apt-get install -y nginx && \
  rm -rf /var/lib/apt/lists/* && \
  chown -R www-data:www-data /var/lib/nginx

# Define working directory.
WORKDIR /etc/nginx

# Copy all config files
COPY ./config/default.conf /etc/nginx/conf.d/default.conf
COPY ./config/nginx.conf /etc/nginx/nginx.conf

# Define default command.
CMD nginx
Source: https://github.com/mastix/project-webdev-docker-demo/blob/master/projectwebdev/Dockerfile

I've created the following config files that you have to copy to the /opt/docker/projectwebdev/config/ folder - the Dockerfile will make sure that these files get copied into the image:

default.conf:
## Start www.project-webdev.com ##
server {
    listen  8081;
    server_name  _;
    access_log  /var/log/nginx/project-webdev.access.log;
    error_log  /var/log/nginx/project-webdev.error.log;
    root   /var/www/html;
    index  index.html;
}
## End www.project-webdev.com ##
nginx.conf:
user www-data;
worker_processes auto;
pid /run/nginx.pid;
daemon off;

events {
        worker_connections 768;
}

http {
  ##
  # Basic Settings
  ##
  sendfile on;
  tcp_nopush on;
  tcp_nodelay off;
  keepalive_timeout 65;
  types_hash_max_size 2048;
  server_tokens off;
  server_names_hash_bucket_size 64;
  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  ##
  # Logging Settings
  ##
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;

  ##
  # Gzip Settings
  ##
  gzip on;
  gzip_disable "msie6";
  gzip_http_version 1.1;
  gzip_proxied any;
  gzip_min_length 500;
  gzip_types text/plain text/xml text/css
  text/comma-separated-values text/javascript
  application/x-javascript application/atom+xml;

  ##
  # Virtual Host Configs
  ##
  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;
}

These two files are the basic configuration for our frontend server and will make sure that it listens on port 8081 (see architecture: 4-digit-ports are not exposed to the web) and enables gzip. These files contain basic settings and should be adjusted to your personal preferences when creating your own files.

That's it... your nginx frontend server is ready to run!

Let's create the frontend application

Since I don't want to re-invent the wheel I'm going to use a pretty cool Angular 2.0 seed project by Minko Gechev. This project uses gulp (which I'm using for pretty much every frontend project) as build system. We're going to use this as bootstrapped application and will add our person-demo-specific code to it - for the sake of simplicity I've tried not to alter the original code/structure much.

Our demo will do the following:

  • List all persons stored in our mongodb
  • Create new persons and store them in in our mongodb

Important: This demo will not validate any input data nor does it implement any performance optimizations, it's just a very very little Angular 2.0 demo application that talks to our Docker REST container.

Let's get started with the basic project setup

To get started, I've created a fork of the original Angular 2.0 seed repository and you can find the complete application source code right here! So grab the source if you want to play around with it! :)

The gulp script in this project offers several tasks that will create the project for us by collecting all files, minifying them and preparing our HTML markup. The task we're going to use is gulp build.prod (build production, which will minify files). You can also use gulp build.dev (build development) if you want to be able to debug the generated JavaScript files in your browser. Whenever you run your build, all project-necessary generated files will be copied to dist/prod/ - so the files in this directory represent the website that you need to copy to your docker host later - we'll cover that later.

Although I've said that I'm not going to alter the original code much, I've included Twitter Bootstrap via Bower - for those who don't know: Bower is a frontend dependency management framework. Usually you would call bower install to install all dependencies, but I added this call to the package.json file, so all you have to do is call npm install (which you have to do anyways when downloading my project from github), which will call bower install afterwards.

The model

We've covered the basic project setup, so let's get started with the code.

Since we're using TypeScript, we can make use of types. So we're creating our own type, which represents our Person and thanks to TypeScript and ES6, we can use a class for that. This model consists of the same properties that we've used for our mongoose schema in our REST API (idfirstnamelastname). For that I have created a models directory and within that directory I've added a file called Person.ts which contains the following code:
export class Person {
    private id:string;
    private firstname:string;
    private lastname:string;
    constructor(theId:string, theFirstname:string, theLastname:string) {
        this.id = theId;
        this.firstname = theFirstname;
        this.lastname = theLastname;
    }
    public getFirstName() {
        return this.firstname;
    }
    public getLastName() {
        return this.lastname;
    }
    public getId() {
        return this.id;
    }
}
Source: https://github.com/mastix/person-demo-angular2-seed/blob/master/app/models/Person.ts

The service

No matter if you're working with AngularJS, jQuery, ReactJS or Angular 2.0 you always have to make sure that you outsource your logic into a service or any other detached component that can be replaced if something changes. In Angular 2.0 we don't have a concept of Factories, Services and Providers like in AngularJS - everything is a @Component. So we're creating our PersonService class that allows us to read and store our data by firing XMLHttpRequests (XHR) to our REST API (api.project-webdev.com).

Since this service needs to work with our Person model, we need to import our model to our code. In TypeScript/ES6 we can use the import statement for that.
import {Person} from '../models/Person';
export class PersonService {
    getAllPersons() {
        var personService = this;
        return new Promise(function (resolve, reject) {
            personService.getJSON('http://api.yourdomain.com/person').then(function (retrievedPersons) {
                if (!retrievedPersons || retrievedPersons.length == 0) {
                    reject("ERROR fetching persons...");
                }
                resolve(retrievedPersons.map((p)=>new Person(p.id, p.firstname, p.lastname)));
            });
        });
    }
    addPerson(thePerson:Person) {
        this.postJSON('http://api.yourdomain.com/person', thePerson).then((response)=>alert('Added person successfully! Click list to see all persons.'));
    }
    getJSON(url:string) {
        return new Promise(function (resolve, reject) {
            var xhr = new XMLHttpRequest();
            xhr.open('GET', url);
            xhr.onreadystatechange = handler;
            xhr.responseType = 'json';
            xhr.setRequestHeader('Accept', 'application/json');
            xhr.send();
            function handler() {
                if (this.readyState === this.DONE) {
                    if (this.status === 200) {
                        resolve(this.response);
                    } else {
                        reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']'));
                    }
                }
            }
        });
    }
    postJSON(url:string, person:Person) {
        return new Promise(function (resolve, reject) {
            var xhr = new XMLHttpRequest();
            var params = `id=${person.getId()}&firstname=${person.getFirstName()}&lastname=${person.getLastName()}`;
            xhr.open("POST", url, true);
            xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xhr.onreadystatechange = handler;
            xhr.responseType = 'json';
            xhr.setRequestHeader('Accept', 'application/json');
            xhr.send(params);
            function handler() {
                if (this.readyState === this.DONE) {
                    if (this.status === 201) {
                        resolve(this.response);
                    } else {
                        reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']'));
                    }
                }
            }
        });
    }
}
Source: https://github.com/mastix/person-demo-angular2-seed/blob/master/app/services/PersonService.ts

The application

Since we can read and store persons now, it's about time to take care of the UI and the point where everything starts is the Angular 2.0 application itself. It creates the whole application by glueing logic and UI together. The file we're talking about here is the app.ts.
import {Component, View, bootstrap, NgFor} from 'angular2/angular2';
import {RouteConfig, RouterOutlet, RouterLink, routerInjectables} from 'angular2/router';
import {PersonList} from './components/personlist/personlist';
import {PersonAdd} from './components/personadd/personadd';
@Component({
    selector: 'app'
})
@RouteConfig([
    {path: '/', component: PersonList, as: 'personlist'},
    {path: '/personadd', component: PersonAdd, as: 'personadd'}
])
@View({
    templateUrl: './app.html?v=<%= VERSION %>',
    directives: [RouterOutlet, RouterLink]
})
class App {
}
bootstrap(App, [routerInjectables]);

Source: https://github.com/mastix/person-demo-angular2-seed/blob/master/app/app.ts


We need four different imports in our application:
  • Everything that is needed in order to render the application correctly
  • Everything that is needed in order to route our links correctly
  • Our page that allows us to list all stored persons
  • Out page that allows us to create a new person

Let's have a look at some snippets:
@Component({
    selector: 'app'
})
You can think of Angular apps as a tree of components. This root component we've been talking about acts as the top level container for the rest of your application. The root component's job is to give a location in the index.html file where your application will render through its element, in this case <app>. There is also nothing special about this element name; you can pick it as you like. The root component loads the initial template for the application that will load other components to perform whatever functions your application needs - menu bars, views, forms, etc.
@RouteConfig([
    {path: '/', component: PersonList, as: 'personlist'},
    {path: '/personadd', component: PersonAdd, as: 'personadd'}
])
Our demo application will have two links. One that loads a page which lists all stored persons and another one that allows to create a new one. So we need two routes in this case. Each route is directly linked to components, which we'll cover in a sec.
@View({
    templateUrl: './app.html?v=<%= VERSION %>',
    directives: [RouterOutlet, RouterLink]
})
The @View annotation defines the HTML that represents the component. The component I've developed uses an external template, so it specifies a templateUrl property including the path to the HTML file. Since we need to iterate over our stored persons, we need to inject the ng-For/For directive that we have imported. You can only use directives in your markup if they are specified here. Just skip the <%= VERSION %> portion, as this is part of the original Angular 2.0 Seed project and not changed in our application.
bootstrap(App, [routerInjectables]);
At the bottom of our app.ts, we call the bootstrap() function to load your new component into its page. The bootstrap() function takes a component and our injectables as a parameter, enabling the component (as well as any child components it contains) to render.

The index.html

The index.html represents the outline which will be filled with our components later. This is a pretty basic html file, that uses the aforementioned <app> tag (see our app.ts above) - you might also want to use a name like <person-app> or something, but then you need to adjust your app.ts. This is the hook point for our application.
<!DOCTYPE html>
<head>
[…]
</head>
<body>
[…]
<div class="jumbotron">
    <div class="container">
        <h1>Person Demo</h1>
        <p>This is just a little demonstration about how to use Angular 2.0 to interact with a REST API that we have
            created in the following series: <a
                    href="http://project-webdev.blogspot.com/2015/05/create-site-based-on-docker-part1.html"
                    target="_blank">Series: How to create your own website based on Docker </a>
    </div>
</div>
<div class="container">
    <app>Loading...</app>
    [...]
</div>
<!-- inject:js -->
<!-- endinject -->
<script src="./init.js?v=<%= VERSION %>"></script>
</body>
</html>
That's it... we've created our base application. Now everything we'll create will be rendered in the <app> portion of the page.

Creating the person list

Since encapsulation is very important in huge deployments, we're adding all self-contained components into the following folder: /app/components/ so in terms of the person list, this is going to be a folder called /app/components/personlist/.

Each component consists of
  • the component code itself
  • the template to use

As mentioned before, everything in Angular 2.0 is a component and so the structure of our personlist.ts pretty much looks like the app.ts.
import {Component, View,NgFor} from 'angular2/angular2';
// import the person list, which represents the array that contains all persons.
import {PersonService} from '../../services/PersonService';
//import our person model that represents our person from the REST service.
import {Person} from '../../models/Person';
@Component({
    selector: 'personlist',
    appInjector: [PersonService]
})
@View({
    templateUrl: './components/personlist/personlist.html?v=<%= VERSION %>',
    directives: [NgFor]
})
export class PersonList {
    personArray:Array<string>;
    constructor(ps:PersonService) {
        ps.getAllPersons().then((array)=> {
            this.personArray = array;
        });
    }
}
Source: https://github.com/mastix/person-demo-angular2-seed/blob/master/app/components/personlist/personlist.ts

As you can see, we're importing the following:

  • Standard Angular Components to be render the page (NgFor Directive is used to iterate through our list later)
  • Our Person service
  • Our Person model

We need to inject our PersonService into our component and the NgFor directive into our View, so we can use them later (e.g. see https://github.com/mastix/person-demo-angular2-seed/blob/master/app/components/personlist/personlist.html).

The real logic happens in the PersonList class itself - ok, there's not much here... but it's important. The constructor of this class uses the PersonService to fetch all Persons (the service will then fire a request to our API to fetch the list of persons) and to store them in an array. This array will then be accessible in the view, so we can iterate over it.
<table class="table table-striped">
    <tr>
        <th>ID</th>
        <th>FIRST NAME</th>
        <th>LAST NAME</th>
    </tr>
    <tr *ng-for="#person of personArray">
        <td>{{person.id}}</td>
        <td>{{person.firstname}}</td>
        <td>{{person.lastname}}</td>
    </tr>
</table>
Source: https://github.com/mastix/person-demo-angular2-seed/blob/master/app/components/personlist/personlist.html

We're using a table to represent the list of persons. So the only thing we need to do is to iterate over the personArray that we have created in our PersonList component. In every iteration we're creating a row (tr) with 3 fields (td) that contains the person's id, first name and last name.

Creating the person add page

Ok, since we can now list all persons, let's add the possibility to create a new one. We're following the same pattern here and create a personadd component (/app/components/personadd) that consists of some logic and a view as well.
import {Component, View, NgFor} from 'angular2/angular2';
// import the person list, which represents the array that contains all persons.
import {PersonService} from '../../services/PersonService';
//import our person model that represents our person from the REST service.
import {Person} from '../../models/Person';
@Component({
    selector: 'personadd',
    appInjector: [PersonService]
})
@View({
    templateUrl: './components/personadd/personadd.html?v=<%= VERSION %>',
})
export class PersonAdd {
    addPerson(theId, theFirstName, theLastName) {
        new PersonService().addPerson(new Person(theId, theFirstName, theLastName));
    }
}
Source: https://github.com/mastix/person-demo-angular2-seed/blob/master/app/components/personadd/personadd.ts

I'm not going to cover the annotations here, since they follow pretty much the same pattern like the PersonList. But what's important here is that the PersonAdd class offers a property/method/function called addPerson(), which takes three parameters: id, firstname, lastname. Based on these parameters, we can create our Person model and call our PersonService to store it on our server (in our mongodb Docker container via our ioJS REST Docker container).

Important: Usually you would add some validation here, but for the sake of simplicity I've skipped that.

As mentioned before, everything that we specify in the class will be available in the View, so this method can later be called from the HTML markup.
<form>
    <div class="form-group">
        <label for="inputId">ID</label>
        <input #id type="number" class="form-control" id="inputId" placeholder="Enter ID">
    </div>
    <div class="form-group">
        <label for="inputFirstName">First name</label>
        <input #firstname type="text" class="form-control" id="inputFirstName" placeholder="First name">
    </div>
    <div class="form-group">
        <label for="inputLastName">First name</label>
        <input #lastname type="text" class="form-control" id="inputLastName" placeholder="Last name">
    </div>
</form>
<button class="btn btn-success" (click)="addPerson(id.value, firstname.value, lastname.value)">Add Person</button>
Source: https://github.com/mastix/person-demo-angular2-seed/blob/master/app/components/personadd/personadd.html

I could have used angular2/forms here, but believe me, it is not ready to work with... I've struggled so much that I've decided to skip it (e.g. I'd have to update my type definitions and so on...). But what's really important here is that we can call our addPerson() method from our PersonAdd component and pass the values from our fields. Pretty easy, right?

Now we can build our project by running gulp build.prod and copy the contents of the newly created dist/prod/ folder to our docker host. Remember: In our docker compose file we've specified that our /opt/docker/projectwebdev/html folder will be mounted in our container (as /var/www/html). So we can easily update our HTML files and the changes will be reflected on our website on-the-fly.

So when you've copied all files, the directory structure should look like that:
├── config
│   ├── default.conf
│   └── nginx.conf
├── Dockerfile
└── html
    ├── app.html
    ├── app.js
    ├── bootstrap
    │   └── dist
    │       └── css
    │           └── bootstrap.min.css
    ├── components
    │   ├── personadd
    │   │   └── personadd.html
    │   └── personlist
    │       └── personlist.html
    ├── index.html
    ├── init.js
    ├── lib
    │   └── lib.js
    └── robots.txt

Here is what it looks like later

Adding a new person


Listing all persons



That's it... we have the backend and the frontend now... it's about time to create our nginx reverse proxy to make them all accessible!


19 comments:

  1. Thanks for posting..

    I am following this blog from begging and I was able to successfully follow the instructions that you provided. In Part 9 after creating default.conf and nginx.conf what should I do? Should I get the gulp script or run the gulp build.prod and copy the files to docker host?

    ReplyDelete
    Replies
    1. Hey Raj,

      you need to build the source code like mentioned in the ReadMe file: https://github.com/mastix/person-demo-angular2-seed.

      After that you'll get a dist/prod folder, which contents you can just copy into /opt/docker/projectwebdev/html.

      Regards,

      Sascha

      Delete
  2. Hi Sascha,

    It build the source code as per ReadMe file but I don't see the bootstrap directory. Here is my output for gulp build.prod.

    am I missing something?


    $gulp build.prod
    [09:54:50] Using gulpfile ~/person-demo-angular2-seed/gulpfile.js
    [09:54:50] Starting 'build.prod'...
    [09:54:50] Starting 'clean.prod'...
    [09:54:50] Finished 'clean.prod' after 12 ms
    [09:54:50] Starting 'build.ng2.prod'...
    [09:55:00] Finished 'build.ng2.prod' after 9.33 s
    [09:55:00] Starting 'build.lib.prod'...
    [09:55:20] Finished 'build.lib.prod' after 20 s
    [09:55:20] Starting 'clean.tmp'...
    [09:55:20] Finished 'clean.tmp' after 7.89 ms
    [09:55:20] Starting 'build.app.prod'...
    [09:55:20] Starting 'clean.app.prod'...
    [09:55:20] Finished 'clean.app.prod' after 2.54 ms
    [09:55:20] Starting 'build.init.prod'...
    [09:55:21] Finished 'build.init.prod' after 1.21 s
    [09:55:21] Starting 'build.js.tmp'...
    app/components/personlist/personlist.ts(22,13): error TS2322: Type '{}' is not assignable to type 'string[]'.
    Property 'length' is missing in type '{}'.
    app/services/PersonService.ts(8,59): error TS2339: Property 'length' does not exist on type '{}'.
    app/services/PersonService.ts(11,42): error TS2339: Property 'map' does not exist on type '{}'.
    [09:55:22] TypeScript: 3 semantic errors
    [09:55:22] TypeScript: emit succeeded (with errors)
    [09:55:22] Finished 'build.js.tmp' after 1.03 s
    [09:55:22] Starting 'build.js.prod'...
    [09:55:22] Finished 'build.js.prod' after 280 ms
    [09:55:22] Starting 'build.assets.prod'...
    [09:55:22] Finished 'build.assets.prod' after 60 ms
    [09:55:22] Starting 'build.index.prod'...
    [09:55:22] gulp-inject 1 files into index.html.
    [09:55:22] Finished 'build.index.prod' after 67 ms
    [09:55:22] Starting 'clean.tmp'...
    [09:55:22] Finished 'clean.tmp' after 4.5 ms
    [09:55:22] Finished 'build.app.prod' after 2.67 s
    [09:55:22] Finished 'build.prod' after 32 s
    raj@ubuntu:~/person-demo-angular2-seed$ cd dist/
    raj@ubuntu:~/person-demo-angular2-seed/dist$ ls -l
    total 4
    drwxr-xr-x 4 root root 4096 Jun 15 09:55 prod
    raj@ubuntu:~/person-demo-angular2-seed/dist$ cd prod/
    raj@ubuntu:~/person-demo-angular2-seed/dist/prod$ ls -l
    total 28
    -rw-rw-r-- 1 root root 331 Jun 15 09:55 app.html
    -rw-r--r-- 1 root root 5677 Jun 15 09:55 app.js
    drwxr-xr-x 4 root root 4096 Jun 15 09:55 components
    -rw-rw-r-- 1 root root 2379 Jun 15 09:55 index.html
    -rw-r--r-- 1 root root 1704 Jun 15 09:55 init.js
    drwxr-xr-x 2 root root 4096 Jun 15 09:55 lib


    ReplyDelete
  3. Sorry for my late reply. Did you manage to run the gulp script now? If not, please open an issue here: https://github.com/mastix/person-demo-angular2-seed/issues

    ReplyDelete
    Replies
    1. I tried this morning and still don't see bootstrap directory, for now I copied the file from github:
      https://github.com/mastix/project-webdev-docker-demo/tree/master/projectwebdev/html/bootstrap/dist/css

      I will open an issue..

      Thanks


      Delete
  4. Hello Sascha, Thanks very much for detailing out Full Scale JS project.

    One question on the domain name :
    Where is the connection between domain name - api.project-webdev.com and the container - projectwebdev-api happen?
    {https://github.com/mastix/project-webdev-docker-demo/tree/master/projectwebdev-api }

    Understanding is that this Container will not be known to the outside word, except the REVERSE Proxy .. So, there won't be any static IP and hence there is no Domain Mapping in godaddy and all.. Right?
    Please clarify . Thanks in advance.


    Best Regards,
    Raj

    ReplyDelete
    Replies
    1. Hey Raj,

      when a Docker container is created and exposes a port, an IP address is automatically assigned to it. When the container is created, you can easily see the IP when running "docker inspect ". This IP will automatically be available as environment variable for each container that is linked to this container (e.g. the nginx reverse proxy is linked to the project-webdev-api container; see docker-compose.yml).

      So you can use this environment variable in the nginx configuration (see: http://project-webdev.blogspot.de/2015/06/create-site-based-on-docker-part10-nginx-reverse-proxy-docker-image.html). Then the link looks as follows: api.project-webdev.com => nginx reverse proxy => upstream blog-api => IP & Port of the container! The magic happens in the nginx reverse proxy (see part10 of this guide again). Regards, Sascha

      Delete
  5. It is important to reach more recipients.

    ReplyDelete