In this previous blog post, I have shown how to dockerize an Angular CLI application using NginX. However, the shown method has worked only for classical, client-side rendered Angular projects. In the current post, we will show an Angular Universal Docker example. We will show, how an Angular Universal CLI project with server-side rendering and transition to client-side rendering can be dockerized.
In the main blog post, we will use the node http-server static server in order to deliver the Angular application to the browser. However, you also will find NginX static and NodeJS dynamic variants of the same task. In the Summary section, we will compare the performance of the three variants (don’t expect too much: I just made a simple load time comparison in the Chrome 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 built 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 the latest tag as 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 a static IP address. Since we have chosen the –net=host option, the port should be accessible on this IP address. If you 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? Yes: when looking at the source of the page in the browser (e.g. press Ctrl-U in Chrome), the content of the page is shown: no javascript is required to show the content:
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 innerHTML 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):
- winner: static with http-server: load time average: 1.36 sec ± 0,05 sec
measured load times [sec]: 1.38, 1.37, 1.30, 1.42, 1.33, 1.33, 1.32, 1.42 - intermediate: static with NginX: load time average: 1.53 sec ± 0.19 sec
measured load times [sec]: 1.85, 1.45, 1.22, 1.49, 1.56, 1.45, 1.70, 1.48 - looser: dynamic with NodeJS: load time average: 2.37 sec ± 0.27 sec
measured load times [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. The same trend holds if the server Docker container is 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.
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“ ?
Hi Breit, I guess so. Thanks for the hint. I need to correct it soon.
Best Regards
Oliver
When I run the above command its starts the server but when I try to open the app in my browser using http://192.168.65.3:8080 it is not opening my app. Instead, I get: ‚No site found – This site can’t be reached‘
Did you try to curl to the addresses on the machine the docker run command is running?
In my case, I get:
yes I try to open all the following links in my browser:
http://127.0.0.1:8081
http://10.0.75.2:8081
http://192.168.65.3:8081
http://10.0.75.2:8081 open my app but without angular universal(no page source) and when I reload/refresh any page it shows no site found.
Please help me.thanks
Why port 8081? My application oveits/angular_universal-starter_with_cli is running on port 8080. Have you created your own image? Is it available on Docker hub?
BTW, I recommend performing the curl test on the same machine the docker run command runs on. This way, you can be sure there is no routing problem.
In your case, where is docker running and where is your browser running?
Yes I am creating my own image. It’s not pushed to docker hub yet I am trying tor run it locally. when I run commannd ‘curl http://192.168.65.3:8080‘ it display below error:
curl : Unable to connect to the remote server
At line:1 char:1
+ curl http://192.168.65.3:8080
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
Docker is running locally in my system and my browser is also the same.
Hi Isa, it seems like you are trying to perform the curl command on the Windows System. This is not local in the sense that the docker host ist Linux System running as a virtual machine on the Windows System in your case, I guess. Please perform the curl commands on the Linux virtual machine, not on the Windows system.
I don’t think the title of your article matches the content lol. Just kidding, mainly because I had some doubts after reading the article.