Exploring multi-stage Docker builds for Angular apps

Figuring out the best way to build and deploy any application regardless of the tech stack can be a sprint all in itself. So many questions to answer:

  • What CI/CD pipeline do you use?
  • Can't we just use the CLI?
  • What about right-click-publish?
  • Git Deploy?
  • npm, yarn ?
  • Docker, Kubernetes, Swarm, Containers...oh my!?!?!

Yes or "it depends" is always the answer to these questions depending on the development shop you're working in.

Docker, in version 1.17, introduced the concept of multi-stage builds which in short allows you to (in a single docker file) create images and utilize the output from one into the next to produce a final image and reduce the need for build scripts etc. See more in the docs -> https://docs.docker.com/engine/userguide/eng-image/multistage-build/#use-multi-stage-builds .

How does this apply to Angular?

The prescribed way of, or at least one way, building an Angular app is using the CLI (http://cli.angular.io).

ng build --prod

This command produces the /dist folder which contains the packaged app via webpack which can then be deployed to a server. However, there are some essential needs missing. Deep linking is one of them, so we need to add something like expressjs to the mix with some minor configuration to handle a link to something other than root. i.e. http://mysite/about. An alternative would be something like nginx configuration as well, but for simplicity sake, expressjs will do.

Here is a simple "Hello World" Angular application generated using the CLI.

ng new dockermulti
.
├── README.md
├── e2e
├── karma.conf.js
├── node_modules
├── package.json
├── protractor.conf.js
├── src
   ├── app
   ├── assets
   ├── environments
   ├── favicon.ico
   ├── index.html
   ├── main.ts
   ├── polyfills.ts
   ├── styles.css
   ├── test.ts
   ├── tsconfig.app.json
   ├── tsconfig.spec.json
   └── typings.d.ts
├── tsconfig.json
└── tslint.json

I'll add in the server folder which contains the expressjs components package.json, index.js.

Dockerfile

The goal of the Dockerfile is:

  • Use the CLI to produce the output of our application
  • Create the needed components for the express server to serve the app
  • Combine the outputs and create the final image for pushing to our cloud provider

The first step is to either define the CLI image or use one that already exists. In this case, I have a repository on Docker Hub http://hub.docker.com/r/spboyer/angular-cli which can be used for the base image.

Notice that there are a few tag options here latest, 1.3.2, 1.3.2-yarn depending on the version and package manager needed/wanted for your application. In this example, we will use 1.3.2 (latest).

#APP ========================================
FROM spboyer/angular-cli:1.3.2 as builder

WORKDIR /app
COPY package.json /app
RUN npm install

COPY . /app

RUN ng build --prod

Next, the expressjs server image is defined.

#SERVER =======================================
FROM node:6-alpine as server
WORKDIR /app
COPY /src/server /app
RUN npm install

Last, the final image is defined taking the output from the builder and server to create a container which can be tagged and pushed to Docker Hub or a private registry like Azure Container Registry.

#FINAL ========================================
FROM node:6-alpine
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY --from=server /app /usr/src/app
COPY --from=builder /app/dist /usr/src/app
ENV PORT 80

CMD [ "node", "index.js" ]

Build and run the image

Build the Docker image with the command docker build -t dockermulti .

To run the application image use docker run and map the internal port 80 to a local port.

docker run -d -p 3001:80 dockermulti

Browse to http://localhost:3001

Push your image to Docker Hub using docker push

docker tag dockermulti <dockerhub_user>/dockermulti

docker login

docker push <dockerhub_user>/dockermulti

Bonus, Push to Azure!

There are 2 quick and easy ways to push a Docker image to Azure. Using Azure Container Instances or App Service on Linux.

Pre-Requisites

Azure Container Instances

First, create a resource group to logically group your items in Azure. Name the group something that is relative to your app and set the location. i.e eastus or westus. Locations for each service can be found here.

az group create --name <resourceGroup> --location eastus

Next, create the container instance setting the name of the container, the image to use, resource group (just created) and IP address to public

az container create --name mycontainername --image <dockerhub_username>/dockermulti -g myResourceGroup --ip-address public

Running the command az container list -o table provides an easy to read output of the status and provisioning state and IP Address. The two columns to pay attention to here are Provisioning State and IP:Ports.

Name             ResourceGroup    ProvisioningState    Image                                     IP:ports          
---------------  ---------------  -------------------  ----------------------------------------  ----------------  
dockermulti      dockermulti      Succeeded            spboyer/dockermulti                       13.91.43.101:80   

Once the Provisioning State is Succeeded browse to http://IP:ports to see the application.

Azure App Service on Linux

Running a container on App Service gives you more control such as auto updating the containers using CI/CD from Docker Hub or Azure Container Registry, Scaling, and Logging.

First create an App Service Plan for Linux.

az appservice plan create -n <myPlanName> -g <MyResourceGroup> --islinux -l "southcentralus" --sku S1 --number-of-workers 1

Create the web application host for the Docker container giving it a name (-n), the plan name (-p just created), resource group (-g) and the docker image (-i).

az webapp create -n <appName> -g <resourceGroup> -p <planName> -i <dockerhub_username>/dockermulti

Browse your application, once created, with az webapp browse -n <appName> -g <resourceGroup>

Enjoy!