Programmers frequently run into the problem where code works on their machine, but doesn’t work on other’s machines, or in the target environment. Usually it comes down to something being different on their machine. Either extra files that they forgot to commit, a different version of a dependency or something else specific to their machine. A build server can help with this, but usually requires you to commit your changes and push them to the central repository.

In this blog post, I am exploring how Docker can be used to provide a clean and consistent build environment for local verification of your code, without affecting your development environment or requiring you to commit and push. I am also going to use a multi-stage Dockerfile to produce a production-like image that could be deployed to a Docker environment. By production-like, I mean that in a real production scenario you would probably want to take additional steps, such as security hardening, on the image and all I am doing is copying the application to it.

If you are new to writing Dockerfiles and building Docker images, I recommend watching this Pluralsight course.

I will be building an Angular web application using Angular CLI and deploying it to an nginx web server. It does appear that some design work has been done to add Docker support to Angular CLI, but as far as I can tell, nothing has been implemented yet. The following steps should work on Linux or OS X. If you use Windows you are on your own (you could follow along using a Linux VM).

Assuming you already have NodeJS installed, you can install Angular CLI with:

npm install -g @angular/cli

Then scaffold an Angular web application with:

ng new angular-docker

A frequent source of issues when building anything with NodeJS, is different versions of packages being installed on the programmer’s machine. You can ignore the node_modules directory and then install the packages during the Docker build by using a .dockerignore file. I will also be ignoring the Dockerfile itself, because otherwise any change to the Dockerfile will affect the build context and potentially invalidate caching when copying files into the image. You may also want to add other files that would normally be ignored in your .gitignore file.

.dockerignore

node_modules
Dockerfile

After a bit of experimenting, I came up with the following Dockerfile and nginx configuration to build an Angular app and deploy it to a production image.

Dockerfile

FROM node:8.9.0 as builder
WORKDIR /usr/src/app
COPY package.json .
COPY package-lock.json .
RUN npm install
ADD . .
RUN node_modules/.bin/ng build --prod
FROM nginx:1.13.6-alpine
COPY conf/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html

nginx.conf

server {
  listen 80;
  index index.html;
  server_name localhost;
  error_log  /var/log/nginx/error.log;
  access_log /var/log/nginx/access.log;
  root /usr/share/nginx/html;
  location / {
    try_files $uri$args $uri$args/ $uri/ /index.html =404;
  }
}

Here is a walkthrough of this Dockerfile: On line 1, I am specifying the base image to build the app with (the official node image), and aliasing it so I can refer to it later. On line 3, I am specifying the current working directory. This can be anything really since this image is temporary. On lines 5-7, I am copying in the package configuration files and installing packages. I am doing this before copying in the rest of the source code to take advantage of image caching when rebuilding the image later. Any change to either of these files will result in the cache being invalidated and a clean slate, which is exactly what we want. On lines 9-10, I copy in the rest of the source code and build the Angular app. On line 12, I specify the base image (the Alpine Linux version of the official nginx image) for the production image. On line 13, I copy in an nginx configuration file that allows the Angular app routing to work (it redirects any request that is not an existing file or directory to index.html). On line 14, I copy in the built Angular app from the build image into the production image.

You can build the image with:

docker build -t angular-docker .

If it built successfully, you can run it by using the following command to create a new container:

docker run -it --rm -p 4200:80 --name angular-docker angular-docker

The Angular application will be running at http://localhost:4200, where you can test it out, either manually or using the e2e tests generated by Angular CLI. Hit Ctrl-C to exit and Docker will remove the container.

Now, we really only accounted for Node packages so far, and another source of issues might be extra files in your working directory. So, what if we want to be really strict and only build with files that are committed to Git? I discovered that Docker can accept a tar file from standard input as the build context. And Git can export a tar file from a reference. So, to build only code committed to a Git branch, you can chain these together and could run a command like the following:

git archive master | docker build -t angular-docker -

Or if you want to skip specifying the branch and build from the currently checked out branch:

git archive $(git rev-parse --abbrev-ref HEAD) | docker build -t angular-docker -

You could, of course, add scripts to your package.json so you don’t have to remember these commands.