Rework Your Angular Development Workflow

Jeffry Houser
disney-streaming
Published in
19 min readJul 30, 2021
Photo by Frederik Merten on Unsplash

I’m Jeffry Houser, a developer from the Polaris team in the content engineering group at Disney Streaming Services. Polaris was named after Magneto’s daughter from the X-Men. We build internal tools that allow editors to create magic for our customers in the video services we power.

My team does a lot with the Angular framework, and by association, we use the Angular CLI. The Angular CLI includes a built-in development web server that is great for simple things or quick prototypes. However, this built-in web server is not recommended for production use, so when we deploy to production we use NGINX to power our web app. If you’re like me, then at some point you’re going to wish that your development environment more closely resembled your production environment. This article will demonstrate how we use the Angular CLI, NGINX, and Docker to create that resemblance.

Prerequisites

There are a handful of prerequisites you’ll need to install so your Angular Development environment can mirror your production environment:

  • NodeJS: The Angular CLI is built on top of NodeJS, so a NodeJS installation is required. Download and install from the NodeJS web site.
  • Angular CLI: If you’re reading this, you’re probably already an Angular Developer and have the Angular CLI installed. If not, install the Angular CLI using Node’s package manager.
npm install -g @angular/cli
  • Docker Desktop: Docker is a way to create and run containers, which are virtual machines. It is an important piece to the puzzle.
  • NGINX: NGINX is a web server commonly used in microservice architectures. In addition to serving files on its own — like an Angular application — you can also create routes to act as an API gateway that proxy to other remote services. For this article, we’ll use a docker image that already includes NGINX, making a standalone installation unnecessary.
  • OpenSSL: OpenSSL is a set of tools that help you create SSL certificates. We’ll use it to create a self-signed SSL Certificate for our local server.

Setup

Let’s start by generating a default Angular application:

ng new reworkdevflow

This command will start you through the wizard to create an Angular app. I kept the app in non-strict mode. I added Angular routing, since that is common in most Angular apps, and I chose normal CSS.

Now you should be able to use the Angular CLI to serve the new project:

ng serve

You should see the following:

And now you can load the project by pointing your browser to localhost:4200:

Angular CLI Sample Application

You should see the default Angular application. If you do a lot of Angular development, you’ve probably been through this process before. If not, congratulations; you just built your first Angular app.

Create Docker Image

To create a docker image, we can start by creating a docker configuration file. In the root directory of your project, create a file named Dockerfile. This file has no extension, but it will contain a bunch of commands that are used by docker to create a custom image that will run NGINX.

Start the file with this line:

FROM nginx:latest

The above command tells Docker that we’re going to build a new container based on the latest existing image of NGINX. Let’s get this running with no further customizations.

Build the docker image from your command line, using the following command:

docker build -t ui .

The command tells Docker to build the container with the name ui:

Start the image:

docker run — rm — name ui -d -p 80:80 ui:latest

You should see something like the image below:

The following command was added as part of that screenshot:

docker ps

The docker ps command lists all the running containers, and you should see your ui container listed.

The feedback doesn’t provide a lot of feedback, so let’s dissect the docker run command:

  • docker: Tells docker to do something
  • run: Tells docker that to start a container.
  • -rm: Tells docker to destroy the container when it is stopped. By default, docker will keep them around, which is sometimes useful for debugging when you want to investigate the final state of the container. It is less useful when you’re just using a container.
  • -name ui: The name attribute gives the container a name. In this case, we are using the same name for both our running container and the tag of our underlying image.
  • -d: The do attribute tells the container to run detached, meaning that the container will not exit when the root process which runs the container exists. In conjunction with rm, d means the container is removed when the daemon that started it exits, or when the container exits.
  • -p 80:80: The p command maps ports from our machine to the inside of the container. In this case, we’re going to access the default web server port (80) and it will route to the internal port (also 80). You can specify different external or internal ports to match your own development environment if needed.
  • ui:latest: This last part of the command tells Docker the image you want to start executing. We want to start the ui image we just created.

Now, open up the browser and point it to localhost:

Default NGINX Home Page

Now you have the default NGINX running. The next step is to get NGINX working with Angular.

Create NGINX Config

Let’s create a custom NGINX config as part of the Docker build. I’m going to start with a very simple default NGINX configuration and it will expand throughout this article. Create a directory named nginx in the Angular project root and add a file named nginx.conf:

events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
}

The worker_connections statement determines the number of simultaneous connections that can occur to your NGINX instance at once. The http block defines web server configuration. It loads the default mime types with the include directive. Our web server will listen on port 80 as defined by the listen directive. The first listen directive will use a default IPv4 address, but the second listen command uses an IPv6 address. It is best to keep them both. The server_name is specified as localhost, and the location block specifies the root and the main index files to look for. This is all standard web server configuration.

Switch back to your Dockerfile and add a line to copy your custom config into the Docker image:

FROM nginx:latest
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf

Rebuild the Docker image:

docker build -t ui .

The docker build command is frequently used. You should see a result resembling the image below:

If you still have the docker image running, you’ll need to restart in order for the new changes to take place. Stop the existing Docker image:

docker stop ui

You should see the following:

I added a docker ps command in the screenshot so you could see nothing is running. Now restart the docker build:

docker run — rm — name ui -d -p 80:80 ui:latest

You’ll see the following:

The docker ps shows that the instance is running. Now, point your browser to localhost at port 80 and you’ll see the same NGINX intro screen, thus verifying that our new configuration is still working:

Default NGINX Home Page

Creating our custom config is just the first step to having NGINX serve our Angular application.

Copy Angular Build into NGINX

Let’s get NGINX serving our default Angular app. First, let’s build the Angular app:

ng build

The ng build command is different than ng serve because no development server is started and your code is written to a dist folder. You’ll see something like this:

The above result will create a dist folder in your project.

Now, tell the docker file to copy over the dist results into the NGINX HTML folder. Add a copy to the final line of the Dockerfile:

FROM nginx:latestCOPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./dist/reworkdevflow /usr/share/nginx/html

Now rebuild the Docker image to include the Angular build:

docker build -t ui .

You’ll see the following build:

Now kill the docker container if it is running:

docker stop ui

And start it up again:

docker run —-rm —-name ui -d -p 80:80 ui:latest

The docker ps to the screenshot so you can see the docker container is running again.

Load the site in the browser

Angular CLI Sample Application running on NGINX

Now we have an NGINX instance running inside of a Docker container that is successfully serving an Angular app. Sweet! Next, let’s get the Angular CLI to auto rebuild the Angular app, while still using our NGINX docker container.

Connect NGINX with Angular CLI Server

When you use ng serve with the Angular CLI, it automatically reloads code changes are made. This is probably my favorite feature of the Angular CLI. To get this same functionality with NGINX, we are going to set up a proxy from NGINX to the Angular CLI Server and run them both side by side. Open up your nginx.conf file and find the http section, and add the following:

upstream ui.local {
server host.docker.internal:4200;
}

The upstream block is used to define a bunch of connections from our NGINX server to some other service. In this case, we have one connection, defined with the server attribute. It is pointing to host.docker.internal which is a special endpoint that will resolve to the internal IP address used by the docker container. It points to port 4200 which is where the Angular CLI Development Server will run.

Find the location portion of the NGINX config. It will look like this:

location / {
root /usr/share/nginx/html;
index index.html index.htm;
}

We are going to add two things. First, we’ll add a resolver:

resolver 127.0.0.1 valid=15s;

The resolver is like NGINX’s own personal DNS. Next, add a proxy_pass:

proxy_pass http://ui.local;

This means that all connections to our NGINX instance will be passed onto “ui.local”, which is defined as an upstream connection that points to the docker container’s host’s IP. Rebuild your docker image:

docker build -t ui .

I hope you’re not getting bored of rebuilding this container yet:

Now you can try to run your Angular server. We’re going to use an expanded syntax with more command-line arguments than a typical ng-serve:

ng serve — disable-host-check=true — host=0.0.0.0

The disable-host-check argument will allow any connected client to access the server. By specifying the host IP address as 0.0.0.0 it allows anyone on your network to talk to the server. This is important because it is exactly what we want NGINX to do.

Now restart your docker container, I used another console window:

docker stop ui
docker run —-rm —-name ui -d -p 80:80 ui:latest

You should see this run:

As with many of the screenshots here, I added a docker ps to the end of the screenshot to show that the container was indeed running.

Now, load up your browser:

Angular CLI Dev Server and NGINX Working Together

Make some changes to the app. Open the app.component.ts file and find the title variable:

title = ‘reworkdevflow’;

Change it to:

title = ‘Hello Art of Possible Readers’;

The Angular CLI automatically recompiles the app:

Reload the browser, and you’ll see the change reflected:

Angular CLI Sample Application, Dynamically reloaded

Success!

Create Some package.json Helper Scripts

To get NGINX and the Angular CLI talking to each other, we run two different commands in separate console windows. We can do this in one step with a single npm command. You’ll find a start command in the package.json:

“start”: “ng serve”,

Replace the above command with:

“start”: “docker run — rm — name ui -d -p 80:80 ui:latest && ng serve — disable-host-check=true — host=0.0.0.0 &”,

Be sure to shut down both the Angular Development server and the Docker image before running this command. Now run the command:

npm run start

And you’ll see something like this:

Load the app in the browser and you should see the app load. Make some changes, and you’ll see the app recompile and the changes reflected in the browser.

We want to be able to stop this, too, so let’s add a stop command:

“stop-windows”: “(for /f \”tokens=5\” %a in (‘netstat -ano ^| find \”4200\” ^| find \”LISTENING\”’) do taskkill /f /pid %a) && docker kill ui”,

Lots going on in the above code, so let’s split the block into two separate commands. The first will find all instances of things listening to port 4200 — AKA the Angular CLI server — and shut them down. It is fancy Windows syntax. The second part of this will shut down the docker container. Run the script in a new console window:

You can see from the docker ps statement at the end of the console that the docker image is no longer running. The Angular CLI should be shut down if you check your other console window:

If you are a Mac user, like a lot of my development team, the following command serves the same purpose:

“stop”: “docker stop ui && lsof -ti :4200 | xargs kill -9,

You can then run the command:

npm run stop

And you should see something like this:

Configure for Different Environments

The NGINX config that we built so far is designed to always proxy to the local Angular CLI server. We won’t ever want to deploy such a config to production since the Angular CLI dev server is not suited for production use. We could create two separate NGINX configs, and two separate docker images to solve that. But that would lead to a lot of duplicate code between separate configs and we don’t want that. We’re going to use environment configuration variables to tweak the image and NGINX config on the fly.

We’ll create two different config files: one for local development and one for production deployments. Then we’re going to run a script as part of our docker image creation that will inject the environment variables into the NGINX configuration.

We’re going to add two config values into the NGINX config: one for the host machine address, and one for the resolver URL. Open up the nginx.conf file. Find the place where we define the upstream directive:

upstream ui.local {
server host.docker.internal:4200;
}

We want to replace the host.docker.internal with a config value, like this:

upstream ui.local {
server ${HOST_MACHINE_ADDRESS}:4200;
}

The syntax for variable replacement is similar to ES6 template strings, which you may know if you work in JavaScript.

Find the resolver line in the location section of the server section. It looks like this:

resolver 127.0.0.1 valid=15s;

Modify the resolver line to this:

resolver ${UI_RESOLVER} valid=15s;

Both the resolver and the upstream directives will be replaced by dynamic values with real values at runtime, giving us a flexible base.

The second thing we’re going to do is create a variable that NGINX can use:

set $env “${NODE_ENV}”;

You can put this line right before the location block. This will get the value that we pass in when running the docker image. With that variable in place, we can disable or disable the proxy_pass value by changing that line to this:

if ($env = ‘local’) {
proxy_pass http://ui.local;
}

For local development, the server will proxy to the Angular CLI dev server and for production deployments, it’ll serve the build that is built into the docker image.

Let’s create our config files. First, create one called local.env in the nginx directory:

UI_RESOLVER = 127.0.0.1
HOST_MACHINE_ADDRESS = host.docker.internal

This file will contain replacement values for the variables in the nginx.config. Now create a file named prod.env:

UI_RESOLVER = 192.168.0.3
HOST_MACHINE_ADDRESS = 127.0.0.7

The UI_RESOLVER will be any external IP address where your server is located. In this case, I used the internal IP address on my network, but for your production environment, you’ll need something more exposed. The HOST_MACHINE_ADDRESS value becomes the callback IP address.

Let’s tweak the Dockerfile to copy our config files into a new directory. Find this line:

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

This copies our custom NGINX config from our development machine into the nginx config folder inside the docker image. We’re going to change that and copy all our environment files and the NGINX config into a subdirectory of Docker’s nginx folder, like this:

COPY ./nginx/*.env ./nginx/nginx.conf /etc/nginx/ui/

This code will create a new ui directory inside the nginx folder, populated with all our config templates for different environments and the custom NGINX configuration.

Next, the script needs to use these configs to substitute the environment variables inside the nginx.conf with the actual values and then start the NGINX server with the resolved config. In the nginx folder, create a file called run_nginx.sh.

First, we want to load the variables for our environment:

set -a
env > “/tmp/priority.env”
. “/etc/nginx/ui/$NODE_ENV.env”
. “/tmp/priority.env”
set +a

The set -a is a way to automatically export the variable assignment from our environment files, without having to specify an export command. The env command is setting the environment variables from inside our env file to be available for use in the docker image.

Now, force a substitution from our NGINX config template and create a real NGINX config file:

envsubst '
${NODE_ENV}
${UI_RESOLVER}
${HOST_MACHINE_ADDRESS}
‘ <
/etc/nginx/ui/nginx.conf > /etc/nginx/nginx.conf

By explicitly listing all the relevant variables we want to be replaced, we make sure that NGINX template variables, such as our $env variable are not mistaken for the replaceable environment variables.

The last step in our shell script is to run NGINX with our custom config:

nginx -c /etc/nginx/nginx.conf -g ‘daemon off;’

Let’s move back to the Dockerfile to make sure we copy the new shell script into Docker image. Add this line to copy our startup script into the Docker image:

COPY ./nginx/run_nginx.sh /etc/nginx/ui/scripts/

Finally, at the end of the Dockerfile, add some code to execute the script:

CMD [“/bin/bash”, “/etc/nginx/ui/scripts/run_nginx.sh”]

This start-up script will execute every time that the docker image starts, allowing us to tweak configs at run time without re-building a brand new docker image each time. Before we do that, we will have to rebuild the docker image to get our scripts and config values:

docker build -t ui .

You’ll see something like this:

Let’s go back to the package.json and send in an environment variable to the start command:

“start”: “docker run — rm — name ui -e NODE_ENV=local -d -p 80:80 ui:latest && ng serve — disable-host-check=true — host=0.0.0.0 &”,

I added the env variable as part of the first half of the start script with the e argument. The argument name is NODE_ENV and the value is local. This will force the NGINX config to be populated with values from the local config, proxying the server back to the running Angular CLI. Now rerun:

npm run start

You’ll see docker and the Angular CLI start up:

Then check a browser to see that everything works,

Angular CLI Sample Application

To run your app for a different environment, you can just change the NODE_ENV variable as part of the script. This will use the prod environment variables:

“start-prod”: “docker run — rm — name ui -e NODE_ENV=prod -d -p 80:80 ui:latest”,

You’ll probably need to tweak the production environment configs I shared in this article for your local machine to make this work. Run this:

npm run start-prod

You’ll see something like this:

Open up the browser and the app should load as long as your prod.env values are set correctly. I want to note that the prod environment configuration does not proxy back to the Angular Development server, so your local changes will not automatically be updated to your browser.

Setup Local Domain

Sometimes developing locally using an IP address, or localhost is less than ideal. You can only have one dev server going at a time, for instance. The login provider that we integrate with at DSS will not redirect back to an IP address or localhost for security reasons. If you want to test your local site on HTTPS, a domain name is required. Although you could set up a DNS server to point to your local IP Address it is not practical, due to the complexity and the non-dynamic nature of your own IP Address. We decided to use the operating system’s local hosts file, to add local development domains that point back to our local machine.

On Mac machines, the local hosts file is located /etc/hosts. On Windows machines it is more hidden, at c:\windows\system32\drivers\etc\hosts. Adding an entry to these files is identical on both platforms:

127.0.0.1 local.ui.com

Reboot your browser and you’ll be able to surf your site at local.ui.com.

However, most likely you are working on a team. And you want to give future team members an easy way to set this up in the future. So, what do you do? Add some npm commands to edit the file. Being a Windows user, I use this:

“setup-windows”: “echo. >> C:\\Windows\\System32\\drivers\\etc\\hosts && echo 127.0.0.1 local.ui.com >> C:\\Windows\\System32\\drivers\\etc\\hosts”,

Run the command:

npm run setup-windows

You’ll see this:

Restart your Docker Image if it isn’t already running:

npm run start

And point your browser to http://local.ui.com :

Angular CLI Sample Application Running on its own Domain

Perfect!

My macOS friends can use this npm command to set up the domain:

“setup”: “echo \”127.0.0.1 local.ui.com\” >> /etc/hosts”,

You should see something like this:

I find these scripts helpful when onboarding new team members.

Setup HTTPS

If your production server is not using HTTPS, it should. In order to mirror the local environment against the production environment, the local server must be configured to HTTPS. The first step is to create an SSL Certificate. You may have already installed OpenSSL from the prerequisite list. If you haven’t, do it now.

Run this command from the nginx directory:

openssl req -new -x509 -nodes -days 3650 -out localhost.crt -newkey rsa:2048 -keyout localhost.key -subj “/C=US/ST=New York/L=New York/O=DSS/OU=polaris/CN=ui” -addext “subjectAltName=DNS:local.ui.com,DNS:127.0.0.1”

You’ll see something like this:

Let’s dissect each part of the command:

  • openssl: Runs the program
  • req: The openSSL command used for creating or processing certificate requests.
  • -new: Creates a brand-new certificate.
  • -x509: Creates a self-signed certificate. For local development, there is no need to have our cert accessed by a remote body.
  • -nodes: Tells OpenSSL not to encrypt the private key.
  • -days: Specifies the expiration date for the certificate.
  • -out: The filename for the resulting certificate
  • -newkey: Creates a new key.
  • · rsa:2048: Specifies the number of bits that the new key should have; in this case 2048.
  • · keyout: The filename for the newly created private key.
  • -subj: The subj parameter specifies a subject name for the new request. In this case, we specify the country — USl the state — New York, the Locale — New York, the Organization — DSS, the Organization Unit — Polaris, and the common name — ui.
  • -addext: The addext argument clarifies other values you want to add to the resulting certificate. In this case, we are specifying a subject alternative name and a DNS.

Next, run the command and you’ll see two extra files in the nginx directory: localhost.crt and localhost.key.

Now we need to tell NGINX how to find and use this. First, open the Dockerfile:

COPY ./nginx/localhost.crt ./nginx/localhost.key /etc/nginx/certs

The above command will copy the cert into the nginx folder inside the docker image. Now open up the nginx.conf file. We’re going to need to tell it where to find the SSL Cert. Find the server section and the server_name entry:

server_name localhost;

Replace the localhost with the domain name:

server_name local.my-ui.com;

Underneath the server name, add the following lines:

ssl_certificate /etc/nginx/certs/localhost.crt;
ssl_certificate_key /etc/nginx/certs/localhost.key;
ssl_protocols TLSv1.2 TLSv1.1 TLSv1;

The lines above tell us where to find the SSL Cert, the SSL Cert Key, and specifies the SSL Protocols to support. Now to find where you set up the two listen commands:

listen 80;
listen [::]:80;

You want to replace these with port 443, the default SSL port:

listen 443 ssl http2;
listen [::]:443 ssl http2;

Rebuild the docker image:

docker build -t ui .

It should build fine:

HTTPS works on port 443, so we need to make sure that map that in our start command. Open up the package.json

“start”: “docker run — rm — name ui -e NODE_ENV=local -d -p 80:80 -p 443:443 ui:latest && ng serve — disable-host-check=true — host=0.0.0.0 &”,

I added -p:443:433 to map the port. Now rerun the app:

npm run start

Point your browser to https://local.ui.com and you’ll probably get a browser error because the HTTPS Cert is self-signed. Select the ‘go to the site anyway’ option and it should load fine:

Angular CLI Sample Application Running On HTTPS

If you want to solve that earlier error, you can load the self-signed cert into your local computer’s keystore, but such instructions are beyond the scope of this article.

Final Thoughts

When my team started, there was a lot of discussion on the best way to approach this issue. I’m happy with the solution we found, and it has been working great for us internally. It worked for us, and it can work for you, too!

I want to give extra special thanks to my teammates for helping to hone the syntax for Mac-based npm commands and taking a few screenshots.

--

--

Jeffry Houser
disney-streaming

I’m a technical entrepreneur who likes to build cool things and share them with other people. I’ve been programming applications for a long time.