Angular 5 – CI and CD with Docker

In this blogpost I will explain how to do continuous integration and continuous delivery for a containerized Angular 5 application.

Prerequisites

This blogpost assumes the following has been installed on your machine.

  • Docker CE for Windows (LINK)
  • An Angular application with unit tests run with Karma (LINK)
  • Any source code editor of your liking. I am using Visual Studio code (LINK)
  • A subscription at one of the following cloud providers: Amazon Web Services, DigitalOcean, Microsoft Azure, Packet.net, IBM SoftLayer. If you don’t have this, you will not be able to reproduce the continuous delivery part of this post.

Result

The following will be in place at the end of this post:

  • Continuous integration
    • The build on Docker hub is triggered when the source code of the app is pushed
    • The build runs Karma unit tests and bundles the source code
    • The build only succeeds when all unit tests pass
  • Continuous delivery
    • A containerized web server based on NGINX that hosts the app
    • A Docker cloud service
    • A trigger that starts the deployment when the build succeeds

Check this LINK for the resulting Docker image.

Introduction

Our starting point is an Angular 5 app that has been built using a test-driven development approach. The example that I will use in this post is the game app project, but any Angular 5 project created with the Angular CLI will do.
The goal is to setup an automated deployment environment. I.e.: after a specific push, the Docker container is initialized, the tests are run and only after completion the project will be deployed to the desired environment.

Setting up continuous integration

We want to achieve two things:

  1. Conditionally succeed the build (only if all unit tests pass)
  2. Build automatically when the repository is pushed to Github

Conditionally succeed the build

To the root of the project I added a Dockerfile and .dockerignore file of which the latter ignores the node_modules directory. The node_modules should be generated as part of the build when an npm install is done.

In the Dockerfile, we add a command that runs the tests and upon success builds the solution.

FROM node:7
WORKDIR /game-app

# Xvfb
RUN apt-get update -qqy \
    && apt-get -qqy install xvfb \
    && rm -rf /var/lib/apt/lists/* /var/cache/apt/*

# Google Chrome
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list \
    && apt-get update -qqy \
    && apt-get -qqy install google-chrome-stable \
    && rm /etc/apt/sources.list.d/google-chrome.list \
    && rm -rf /var/lib/apt/lists/* /var/cache/apt/* \
    && sed -i 's/"$HERE\/chrome"/xvfb-run "$HERE\/chrome" --no-sandbox/g' /opt/google/chrome/google-chrome

#Vim
RUN apt-get update -qqy \
    && apt-get -qqy install vim

COPY package.json /game-app
RUN npm install -g @angular/cli
RUN npm install

COPY . /game-app

#Replace a setting in the Karma test runner to only run once  
RUN sed -i "s|singleRun: false|singleRun: true|g" karma.conf.js
RUN ng test && ng build

In the karma.conf.js file, we need to set the “singleRun” property from false to true:

singleRun: true

If we don’t do this, the container will run ng test and never exit. Ideally, we want to change this only when we create the Docker image to be used for the automated build. When we are developing on a local machine, we want to keep Karma running to update the unit test results when code is changing.
The sed command in the Dockerfile replaces the value and sets it from false to true.
Vim is not necessarily needed (see the install vim command in the Dockerfile), but I added it to show the next screenshot, which is taken from the Docker container:

We can see that in the container, the “singleRun” setting in the karma.conf.js has been successfully replaced.
The last RUN command in the Dockerfile states: If “ng test” succeeds, “ng build” will run. If not, the docker build will exit with a code 1.

Let’s change a unit test so it fails (expect false instead of true).

it('can instantiate service with "new"', inject([HttpClient], (httpClient: HttpClient) => {
        expect(httpClient).not.toBeNull('http should be provided');
        let service = new GameCharacterService(httpClient);
        expect(service instanceof GameCharacterService).toBe(false, 'new service should be ok');
    }));

If we run “docker build . -t gameapp”, the following is shown at the end of the build output:

Automated build

There’s an excellent walkthrough available in the Docker documentation on how to add an automated Docker build when using Github. I have followed this walkthrough to create the automated build for the game app, so I will fast forward to the results.

Once the master branch is pushed to Github, the Docker automated build will be triggered:


After a while, an error is shown:

The details:

This is the same error that we had when building the image locally.
The result of this is that the image on Docker hub is not updated:

In fact, when we pull this image and try to execute vim (which was not installed on the previous image):

Now let’s fix the unit test and push the code.

it('can instantiate service with "new"', inject([HttpClient], (httpClient: HttpClient) => {
        expect(httpClient).not.toBeNull('http should be provided');
        let service = new GameCharacterService(httpClient);
        expect(service instanceof GameCharacterService).toBe(true, 'new service should be ok');
    }));

The Docker image is built successfully and is updated:

If we pull it and execute vim –version:

Setting up continuous delivery

What is great about the direct connection between Docker hub and Github is that we don’t need additional tools such as for example Jenkins to trigger automated Docker builds.
At this point we have the automated Docker build in the hub and of course we can pull and test it, but it’s just sitting there and nothing really happens.

The following diagram illustrates the desired result:

C:\Users\riccardo.corradin\AppData\Local\Microsoft\Windows\INetCache\Content.Word\Docker CI and CD.PNG

Preparing the image for Docker cloud

After the Angular application is tested and built it needs to be hosted on a server in order to serve the index.html page. We don’t want the ng serve command to be executed in a production environment, but instead host the contents of the dist folder. We can use the dockercloud/hello-world image as a starting point. This image uses Alpine and NGINX to host files and expose port 80.
The result is the following Dockerfile:

FROM node:7 as build
WORKDIR /game-app

# Xvfb
RUN apt-get update -qqy \
    && apt-get -qqy install xvfb \
    && rm -rf /var/lib/apt/lists/* /var/cache/apt/*

# Google Chrome
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list \
    && apt-get update -qqy \
    && apt-get -qqy install google-chrome-stable \
    && rm /etc/apt/sources.list.d/google-chrome.list \
    && rm -rf /var/lib/apt/lists/* /var/cache/apt/* \
    && sed -i 's/"$HERE\/chrome"/xvfb-run "$HERE\/chrome" --no-sandbox/g' /opt/google/chrome/google-chrome

#Vim
RUN apt-get update -qqy \
    && apt-get -qqy install vim

COPY package.json /game-app
RUN npm install -g @angular/cli
RUN npm install

COPY . /game-app

#Replace a setting in the Karma test runner to only run once  
RUN sed -i "s|singleRun: false|singleRun: true|g" karma.conf.js
RUN ng test && ng build -prod

#Using multi-stage builds to keep images small and separate build from deployment
FROM alpine:3.4 as deploy

RUN apk --update add nginx php5-fpm && \
    mkdir -p /run/nginx

COPY --from=build /game-app/dist/ /dist/
ADD nginx.conf /etc/nginx/
ADD php-fpm.conf /etc/php5/php-fpm.conf
ADD run.sh /run.sh
RUN chmod +x /run.sh


ENV LISTEN_PORT=80

EXPOSE 80
CMD /run.sh

The first part was already there and the second part (FROM Alpine…) boots up an NGINX server to host the Angular application. To keep the image size small and maintain a readable Dockerfile, multi-stage builds (as of Docker 17.05) are used. See this link for more information.

The index.html in the dist folder (containing a bundle of the Angular application, which is the output of “ng build -prod”) is hosted on port 80 and when the container is initialized the server starts.

This part is extremely important:

RUN chmod +x /run.sh

For some odd reason, if we leave this out, the following error is shown in Docker cloud after which the service immediately stops:

I spent hours trying to find why this happened. The strange thing is that building the same image locally and manually pushing it to the Docker hub works without the permission denied error. So there could be something going on in the build agent that is used in Docker’s cloud infrastructure. Please let me know if you have the answer to this.

Finally, the CMD command will be executed when the container is run (and is ignored when the image is built).

Setting up CD in Docker cloud

The next step would be to trigger deployments automatically if the automated build succeeds.

Docker Cloud and a Docker service will be used to accomplish this. Check “Automated Deployments with Docker Cloud” for a video describing the steps in more detail.

Let’s navigate to https://cloud.docker.com and navigate to the game-app repository. Docker cloud connects to Docker Hub (which in turn connects to GitHub) so it knows the repositories that have been defined.

You might need to switch the BETA Swarm Mode off to reproduce the steps in this section. This toggle bar setting is found in the header of the docker cloud pages.

After “Launch service” has been clicked, we switch on autoredeploy:

The run command is left to its default value (obtained from the Dockerfile), but we need to explicitly publish the exposed 80 port from the container by ticking the “Published” checkbox:

When “Create & Deploy” has been clicked it takes a while before the service has been created and the application will be deployed.

To be able to create a node, a Cloud provider needs to be linked to Docker cloud. This can be done in the settings page. I used my Windows Azure subscription to do this. More details can be found HERE.

The “TIMELINE” tab shows a live log of the steps that are being performed, while the service is started.

The latest version of the game-app is retrieved and the necessary layers are downloaded to the node.

The endpoint link is available:

Once this has been clicked, the application opens:

This application now runs in a Docker container in the Docker cloud. The cool thing is that if we change the unit tests and the title text to: “Hello from app!” this will trigger a new build and a redeploy.

And there we have it: Continuous delivery.

Summary

In this blogpost, I explained what is needed to do continuous integration and continuous delivery with Angular 5 and Docker. Setting up continuous integration is a matter of point and click in the Docker hub. For continuous delivery, we saw that additional care is needed to successfully host an Angular app inside a container on Docker cloud. I touched upon multi-stage builds in Docker and highlighted that running the container locally is not the same as in Docker cloud (chmod, permission issue).

I my opinion continuous delivery with Docker is great, but still feels somewhat brittle. With only a few ready to use Docker cloud images in Docker hub, there is room for improvement. My next goal would be to create a Dockerized generic Angular 5 app for Docker cloud and put in on Docker hub. Basically, everything we did here, except for the game context.

I hope you enjoyed reading this post. If you have any comments, suggestion or other stuff you would like to share, please do so in the comment box below. Thank you!

Leave a Reply