In this previous blog post, I have shown how to dockerize an Angular CLI application using NginX. However, the method works without adaptions only for classical (client-side rendered) Angular projects. In the current post, we will show, how an Angular Universal CLI project with server-side rendering and transition to client-side rendering can be dockerized. In this example, we will use the node http-server static server in order to deliver the Angular application to the browser.

We will apply the dockerization on my project I have created in the blog post Angular Universal CLI – Step-by-Step Example with REST Client that has been created based on the angular/universal-starter project.

Tools and Versions used

  • CentOS 7 image installed via Vagrant
  • Docker version 17.10.0-ce, build f4ffd25
  • git is installed (if not: sudo yum install -y git)

Step 1: Clone the Project from GitHub

(dockerhost)$ git clone https://github.com/oveits/universal-starter
(dockerhost)$ cd universal-starter/cli
(dockerhost)$ git checkout d6084ec

This is a project that has been forked from the /universal-starter project on GitHub.  I have added a REST interface towards WordPress API. With the git checkout command, you will start at the same step I have started this blog post.

You might prefer to perform your tests with the original project https://github.com/angular/universal-starter. This should work as well. Only the resulting page should look differently.

Step 2: Review the package.json

The Dockerfile will re-use scripts we find in the package.json. Therefore, we need to make sure that following scripts are defined in package.json:

...
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "start:dynamic": "npm run build:dynamic && npm run serve:dynamic",
    "start:static": "npm run build:static && npm run serve:static",
    "build": "ng build",
    "build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false",
    "build:static": "npm run build:client-and-server-bundles && npm run webpack:server && npm run generate:static",
    "build:dynamic": "npm run build:client-and-server-bundles && npm run webpack:server",
    "generate:static": "cd dist && node prerender",
    "webpack:server": "webpack --config webpack.server.config.js --progress --colors",
    "serve:static": "cd dist/browser && http-server",
    "serve:dynamic": "node dist/server"
...

Step 3: Prepare the Dockerfile

The initial Dockerfile has been inspired by /angular4-docker-example on Github. I have forked and slightly changed the file, and we can download it via

(dockerhost)$ curl https://raw.githubusercontent.com/oveits/angular4-docker-example/master/Dockerfile > Dockerfile

For the static HTTP Server, we need to change the content as follows. Here, I have kept the original lines commented out in red, and the added lines in bold blue:

# Dockerfile
### STAGE 1: Build ###

# We label our stage as 'builder'
FROM node:8-alpine as builder

COPY package*.json ./

RUN npm set progress=false && npm config set depth 0 && npm cache clean --force

## Storing node modules on a separate layer will prevent unnecessary npm installs at each build
RUN npm i && mkdir /ng-app && cp -R ./node_modules ./ng-app

WORKDIR /ng-app

COPY . .

## Build the angular app in production mode and store the artifacts in dist folder
#RUN $(npm bin)/ng build --prod --build-optimizer
RUN npm run build:static


### STAGE 2: Setup ###

#FROM nginx:1.13.3-alpine
FROM node:8-alpine

## Install http-server
RUN npm install http-server -g

## Copy our default nginx config
#COPY nginx/default.conf /etc/nginx/conf.d/

## Remove default nginx website
#RUN rm -rf /usr/share/nginx/html/*

## From 'builder' stage copy over the artifacts in dist folder to default nginx public folder
#COPY --from=builder /ng-app/dist /usr/share/nginx/html
COPY --from=builder /ng-app/dist /dist

#CMD ["nginx", "-g", "daemon off;"]

WORKDIR /dist/browser
CMD ["http-server"]

In STAGE 1, we have made use of the script defined in the package.json, which will create all HTML files ahead of time (AOT) and put them in the dist/browser folder. In STAGE 2, we copy the dist folder, and run the http-server in the dist/browser folder, after we have installed the http-server globally.

Step 4: Build, Tag and Upload the Docker Image

The Docker Image can be build with the command similar to:

(dockerhost)$ docker build . --tag oveits/angular_universal-starter_with_cli:v0.1

Here, I have tagged it with a name and version, so I can upload it to Docker Hub on my oveits account. I also set a latest tag like follows:

docker tag oveits/angular_universal-starter_with_cli:v0.1 oveits/angular_universal-starter_with_cli:latest

Step 5: Test the image locally

We can test the image with

$ docker run -it --net=host oveits/angular_universal-starter_with_cli
Starting up http-server, serving ./
Available on:
 http://127.0.0.1:8080
 http://10.0.2.15:8080
 http://192.168.33.12:8080
Hit CTRL-C to stop the server

In this case, we can see that I am using a VirtualBox machine with a host port network interface with static IP address. Since we have chosen the –net=host option, the port should be accessible on this IP address. If your are using Vagrant with the default Vagrant interface only, you might need to map the VM’s port to your Host port within VirtualBox.

The browser content looks as expected. But is server-side rendering working as well?

Step 6: Upload the Image to Docker Hub

Now we can upload the image to Docker Hub:

(dockerhost)$ docker login
Username: <-- answer with your user credentials
Password:
(dockerhost)$ docker push oveits/angular_universal-starter_with_cli:v0.1 
(dockerhost)$ docker push oveits/angular_universal-starter_with_cli:latest

Step 7: Use from any Docker Host

Now we can use the image from any Docker host that can access the Internet. In my case, this is a CentOS machine on AWS. If we want the application to be available on the standard port 80, we can run the application as follows:

(dockerhost)$ docker run --rm --name angular_universal-starter_with_cli -d -p 80:8080 oveits/angular_universal-starter_with_cli

Port 80 was occupied in my case already, so I have mapped port 8080 to 8080 instead:

Also here, the server-side rendering works fine: the innterHTML content is visible in the HTML source:

Summary

General

In this blog post, we have dockerized an Angular Universal CLI with server-side rendering by making use of the ahead-of-time (AOT) compilation scripts given in the /universal-starter seed project. We have covered the case, where the HTML pages are created ahead-of-time (AOT) and the npm http-server is used to serve the pages.

Dynamic vs Static

In Appendix A, you will see, what needs to be changed in the case we would like to use a (dynamic) NodeJS server instead of a (static) npm http-server. The NodeJS server case has the advantage that the user will never get out-of-date content from the server. I have not tested the performance yet, but I expect the latency of the dynamic case to exceed the one we can expect from the AOT compiled case.

In Appendix B, we can see how also NginX can be used to serve the pages that have been created ahead-of-time (AOT) with similar results.

This is no exact science, but in order to get a hint about the experienced latency, I have made a quick comparison of the loading times of the blog post page served by docker images running in detached mode on a CentOS VirtualBox VM as a Docker host on my local machine. The load times have been recorded on Chrome in debug mode (F12):

  • static with http-server: finish times in sec: 1.38, 1.37, 1.30, 1.42, 1.33, 1.33, 1.32, 1.42
  • static with NginX: finish times in sec: 1.85, 1.45, 1.22, 1.49, 1.56, 1.45, 1.70, 1.48
  • dynamic with NodeJS: finish times in sec: 2.75, 1.86, 2.35, 2.24, 2.28, 2.40, 2.65, 2.50

A look at the measured times reveals that the http-server is marginally quicker than the NginX server. The NodeJS server has a higher latency than the other both. This is expected, sind the NginX. The same trend holds, if the server Docker container are running on an AWS CentOS Docker host.

 

Appendix A: Dynamic NodeJS Server instead of static http-server

For the dynamic NodeJS server case, we need to change the following in the Dockerfile

### STAGE 1: Build ###

# We label our stage as 'builder'
FROM node:8-alpine as builder

COPY package*.json ./

RUN npm set progress=false && npm config set depth 0 && npm cache clean --force

## Storing node modules on a separate layer will prevent unnecessary npm installs at each build
RUN npm i && mkdir /ng-app && cp -R ./node_modules ./ng-app

WORKDIR /ng-app

COPY . .

## Build the angular app in production mode and store the artifacts in dist folder
#RUN npm run build:static
RUN npm run build:dynamic


### STAGE 2: Setup ###

FROM node:8-alpine

## Install http-server
#RUN npm install http-server -g

## From 'builder' stage copy over the artifacts in dist folder to default nginx public folder
COPY --from=builder /ng-app/dist /dist

#WORKDIR /dist/browser
#CMD ["http-server"]
CMD ["node", "/dist/server"]

When we run the service, we will notice that the server is accessing the WordPress API any time we click on the blog post link. In the static case, this is done only once at compile time (or more accurate: transpile time).

Appendix B: Static NginX Server instead of static http-server

For the (static) NginX server case, we need to change the following in the Dockerfile:

### STAGE 1: Build ###

# We label our stage as 'builder'
FROM node:8-alpine as builder

COPY package*.json ./

RUN npm set progress=false && npm config set depth 0 && npm cache clean --force

## Storing node modules on a separate layer will prevent unnecessary npm installs at each build
RUN npm i && mkdir /ng-app && cp -R ./node_modules ./ng-app

WORKDIR /ng-app

COPY . .

## Build the angular app in production mode and store the artifacts in dist folder
#RUN npm run build:static
RUN npm run build:dynamic


### STAGE 2: Setup ###

#FROM node:8-alpine
FROM nginx:1.13.3-alpine
## Install http-server
#RUN npm install http-server -g

## Copy our default nginx config
COPY nginx/default.conf /etc/nginx/conf.d/

## Remove default nginx website
RUN rm -rf /usr/share/nginx/html/*

## From 'builder' stage copy over the artifacts in dist folder to default nginx public folder
#COPY --from=builder /ng-app/dist /dist
COPY --from=builder /ng-app/dist/browser /usr/share/nginx/html

#WORKDIR /dist/browser
#CMD ["http-server"]
#CMD ["node", "/dist/server"]
CMD ["nginx", "-g", "daemon off;"]

As with the http-server, the WordPress API is contacted only once by the HTTP Server at compile time (or more accurate: transpile time). After the client (browser) has taken over, the client will contact the WordPress API, though.

 

3 comments

  1. Hi,

    In “Appendix B: Static NginX Server instead of static http-server” for building apllication you are using “RUN npm run build:dynamic”. Probably this is mistake and we have to use “RUN npm run build:static” ?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.