Docker & Angular: Dockerizing your Angular app easily

No Comments

Docker is an open platform for developing, shipping, and running applications. This enables you to separate applications from the infrastructure, making the software delivery much faster. It has become a widely used production standard and in order to easily deploy your Angular app to any of the cloud providers, you should dockerize it.

Problems to solve

  • Compiling the Angular app inside the Docker container
  • Executing tests inside the Docker container
  • Providing Angular configuration at runtime through environment variables
  • Enabling HTML5 routing on the Nginx server

The source code for the complete example can be found here.

Compiling the Angular app inside the Docker container

In order to create production-ready Docker image, a good practice is to use Docker multi-stage builds, which are useful to anyone who has struggled to optimize Dockerfiles while keeping them easy to read and maintain.

This is a complete Dockerfile from our example project.

FROM gmathieu/node-browsers:3.0.0 AS build

COPY package.json /usr/angular-workdir/
WORKDIR /usr/angular-workdir
RUN npm install

COPY ./ /usr/angular-workdir
RUN npm run build

FROM nginx:1.15.8-alpine

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

COPY ./dev/nginx.conf /etc/nginx/nginx.conf

COPY --from=build  /usr/angular-workdir/dist/angular-docker /usr/share/nginx/html

RUN echo "mainFileName=\"\$(ls /usr/share/nginx/html/main*.js)\" && \
          envsubst '\$BACKEND_API_URL \$DEFAULT_LANGUAGE ' < \${mainFileName} > main.tmp && \
          mv main.tmp  \${mainFileName} && nginx -g 'daemon off;'" > run.sh

ENTRYPOINT ["sh", "run.sh"]

With multi-stage builds, we will use multiple FROM statements in our Dockerfile. Each FROM statement can use a different base, and each of them is starting a new stage of a build. You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image. We can optimize the build process by copying package.json into the container and by executing npm install before we copy the project source code into the container. That means if package.json hasn’t changed since the last build, we can use the cached output of the npm install layer for every other build. In order to create a production package, we will execute npm run build. Inside package.json you can see that npm run build is running linter, executing tests and creating the production package at the end. After that, we will just copy the production build inside the Nginx HTML folder. That way, the Docker image size will be only 16.4 MB.

Executing tests inside the Docker container

By running npm run build, tests will be executed. In karma.conf.js we have configured Headless Chrome to run tests.

// add to the bottom of karma.conf.js
browsers: ['ChromeHeadlessNoSandbox'],
customLaunchers: {
  ChromeHeadlessNoSandbox: {
    base: 'ChromeHeadless',
    flags: ['--no-sandbox']
  }
},
singleRun: false,
captureTimeout: 30000,
browserDisconnectTolerance: 3,
browserDisconnectTimeout : 30000,
browserNoActivityTimeout : 30000

There is no need to configure Headless Chrome inside the Docker container because we are using the gmathieu/node-browsers:3.0.0 image, and it comes with pre-installed browsers, Node and npm. We could also use the official node image, but that would imply a need to install and configure Headless Chrome with every build and that’s a time-consuming process.

Providing Angular configuration at runtime through environment variables

This is the trickiest part of our dockerization process. There are multiple options available:

  • Configuring an Nginx reverse proxy
  • Making a HTTP request to get the configuration on app initialization
  • Using placeholders that will be overridden on container startup

We will use an option with placeholders and overriding them with values from environment variables before the container is started.
To achieve this, you need to create a provider for every property that you want to configure. Providers should ONLY be declared inside app.module.ts. After a properly compiled project, we can be sure that placeholders are only located inside main.js.

First of all, add placeholders to environment.prod.ts.

export const environment = {
  production: true,
  backendApiUrl: '${BACKEND_API_URL}',
  defaultLanguage: '${DEFAULT_LANGUAGE}'
};

Then register providers inside app.module.ts.

providers: [
  {provide: 'BACKEND_API_URL', useValue: environment.backendApiUrl},
  {provide: 'DEFAULT_LANGUAGE', useValue: environment.defaultLanguage}
],

You can inject the configuration like every other provider just like this:

constructor(@Inject('BACKEND_API_URL') private apiUrl: string) {}

Next, we will define the run.sh script inside the Dockerfile that we are going to use as entry point for the Docker container.

RUN echo "mainFileName=\"\$(ls /usr/share/nginx/html/main*.js)\" && \ 
          envsubst '\$BACKEND_API_URL \$DEFAULT_LANGUAGE ' < \${mainFileName} > main.tmp && \ 
          mv main.tmp \${mainFileName} && nginx -g 'daemon off;'" > run.sh

On container startup, all placeholders from main.js will be replaced with values from environment variables, and the Nginx server will be started.
Remember that you should NEVER include and use values from environment files anywhere else than app.module.ts. Also, keep in mind that your placeholders need to have unique names.

Enabling HTML5 routing on the Nginx server

For serving the content of our app, we will use an Nginx server. Before our image is production-ready, the Nginx server needs to be configured to use HTML5 routing, and we are going to solve it by setting up Nginx rewriting rules. The idea is to forward all related requests to a single Angular entrypoint HTML page, normally it’s the index.html from the root.

worker_processes  1;
 
events {
  worker_connections  1024;
}
 
http {
  server {
    listen 80;
    server_name  localhost;
 
    root   /usr/share/nginx/html;
    index  index.html index.htm;
    include /etc/nginx/mime.types;
 
    gzip on;
    gzip_min_length 1000;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
 
    location / {
      try_files $uri $uri/ /index.html;
    }
  }
}

We should provide this Nginx configuration to the Nginx image at build time. We can achieve this by adding a statement from below to a Dockerfile.

# Part of a Dockerfile
COPY ./dev/nginx.conf /etc/nginx/nginx.conf

Running your Docker container

First, we should build a docker image.

docker build -t angular-docker .

We can run the Docker image just by executing the command shown below.

docker run -d -p 80:80 --env BACKEND_API_URL=yourApiUrl --env DEFAULT_LANGUAGE=de angular-docker

Conclusion: Using Docker for your Angular apps

I can say for sure that Docker has become a widely accepted industry standard. By dockerizing your apps, you can easily ship them to any of the cloud providers. Configuring your Angular app through the environment variables is a must. As already mentioned, there are multiple options to achieve this, but I choose to go with the simplest and fastest one. Configuring an Nginx reverse proxy is also a solution, but generally speaking, I will most certainly rather avoid the approach where you will need to make a HTTP request on app initialization to get a JSON configuration.

Milos Brdar

Milos is a software developer at codecentric’s Doboj office since April 2014. He is primarily working with Java and JavaScript technologies.

Comment

Your email address will not be published. Required fields are marked *