Dockerizing an Angular Application

As software engineers, no matter what part of the stack we work on, we're almost certainly going to be working with Docker in some way shape or form. Maybe you're a backend engineer who's writing ETL micro services deployed to Kubernetes, or maybe you're a front end engineer who wants a consistent way to test their site before deploying it to S3. Or maybe you're like me and work in DevOps and you're always neck deep with Docker for one thing or another. Whatever your situation is, we can all probably agree that Docker is a great addition to any software development stack.

Now, all of the above groups I've mentioned sound like seasoned veterans of Docker. But what if you're just starting out learning about this thing called Docker and are trying to find your bearings. While this post is for you! We'll be centering this blog post about containerizing a Angular web app I wrote a while ago that allowed me to organize my work day following the Eisenhower Method of time management. in general though, you can think of this as just a fancier to do list application.

So for starters, to "dockerize" an application, we first need to know how we'd run it without Docker. So a bit about this app of mine we'll be working with:

  • It was built with NodeJS 14 in mind
  • It uses the Angular CLI
  • Like most NodeJS applications, it follows a typical build patten of: npm install then some form of running the actual app (in this case ng serve is what's used)

So how does this translate to a Docker image? While there's a few pieces of this particular Dockerfile that will help you get the hang of things ("Dockerfile" essentially being the code that illustrates what the image should do). First off, we have this line:

1FROM node:14.2.0

This demonstrates a very important principal with Docker images in that they're built in a layered architecture. What that means is each docker image is based off a parent docker image, which in turn has a parent image for itself and so on. What that looks like in practice, could perhaps be illustrated very simplictically with how the node image itself might be built. So what could be going on here, is that it would start with an image for some Linux distribution, say Ubuntu for fun. Then another image maybe has a similar first line like us that would be FROM ubuntu:20.04, maybe that image installs some low level C development libraries. Then from that image, we branch again with maybe an image that has some web development libraries tacked on. Then finally, we have this image that has node 14.2.0 installed. We could even take it a step further and create an Angular image that uses this same node parent image, then our Dockerfile we'll be building might have the first line FROM angular:10 and then the potential Dockerfile we'd write would be even shorter! But for now, we'll stick with using the node:14.2.0 base image.

Secondly, we have this line:

1WORKDIR /usr/src/app

This one's pretty straightforward, but what its saying is that we want to use the directory /usr/src/app as the directory to more or less "install" our application. This also means any further instructions in this Dockerfile will be running from here.

Now, Docker has many useful operation commands in its toolchest, but one useful command in particular is COPY. You can see us use is here:

1COPY . .

This is essentially us copying our project (i.e. the current working directory) to our docker image. So this means, things like our package.json file, our app.ts file, and so on will be copied onto our docker image. Now that we have our project on our docker image, we have a couple of npm commands to make use of these news files:

1RUN npm install -g @angular/cli@10.0.1
2RUN npm install

As I mentioned before, this app follows a pretty standard NodeJS build process, which means to start, we're going to run npm install to install a few global dependencies we need as well as to install the dependencies for our Angular app. In this case, we're installing the Angular CLI, then installing the entirety of what's inside our package.json file under its dependencies.

Now at this point, we have more or less our app code and its dependencies ready to go. However, Docker being a virtualization platform, we need to essentially poke a hole in our firewall between the docker container and our host network. What this does is let the browser we want to load our app in access the docker container running our app. Doing this has two parts essentially, one while build our image and one while running our image. Since we're currently building our image, let's start with that:

1EXPOSE 4200

This is basically us telling our docker image to expose port 4200 (the default Angular port) outside of the container. Once we go to run the image, we'll have to bind that container port to our host port to see it.

The last step in our Dockerfile is the following:

1CMD ng serve --host 0.0.0.0

Now this tells Docker that when the image is run, this is the command that should be run when the image starts. So in essence, since we run our app with ng serve we need to tell Docker that once you have everything set up properly, run ng serve to start the app.

One important concept to be mindful of at this point is the difference between what's run whilst building the image and what's run whilst running the image. Everything we've coded before that final ng serve command is run at build time. This means that the state that those commands produce are present in the output docker image. That last ng serve however, is only run when we go to run the Docker image. In this way, Docker allows our app to start up quickly as it doesn't have to run npm install and such first.

And with that, you have your Dockerfile. It should look something like this when all is said and done:

 1FROM node:14.2.0
 2
 3WORKDIR /usr/src/app
 4
 5COPY . .
 6
 7RUN npm install -g @angular/cli@10.0.1
 8RUN npm install
 9
10EXPOSE 4200
11
12CMD ng serve --host 0.0.0.0

Now, to finish off our post, here's what we can do with that Dockerfile. In the same directory of this file, we can run the following:

1docker build -t eisenhower-method:latest .

This is us building the Docker image from our Dockerfile and naming it "eisenhower-method" and giving it a tag of "latest". Once we really get going with Docker in our software development process, that "latest" could turn into an application version or a git hash which signifies what version of the code is actually contained in the image.

Lastly, now that we've gone through all of this work, let's run our Docker image and see what happens:

1docker run -it -p 4200:4200 eisenhower-method:latest

The above command runs the image by the name we've tagged it with and also binds the Docker image's exposed port (4200) with our host machine's port 4200. At this point, opening localhost:4200 on your browser should show your Angular app!

At this point, I think I'll tie a bow on this blog post. As a follow up to this, I'm thinking I'll do a similar piece on deploying a Docker image your made to Kubernetes. So stay tuned for that!