How to serve an angular project with NGINX on docker

Learn how to build a docker image for your angular app using nginx. API proxy location included as well.

Introduction

In this example we will learn how to build a docker image for our angular application and use NGINX to serve the generated app.


Create an nginx.config file for angular

First, let's create an nginx.conf file that we will use to replace the original file provided with nginx docker image by default. Our file will have some modified settings like gzip compression. You can modify these settings to your needs.

worker_processes 5;

events {
    worker_connections 4096; ## max connections for a single worker
}

http {

    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 120;

    gzip on;

    gzip_static on;
    gzip_disable "MSIE [1-6]\\.(?!.*SV1)";
    gzip_proxied any;
    gzip_comp_level 5;
    gzip_types text/plain text/css application/javascript application/x-javascript text/xml application/xml application/rss+xml text/javascript image/x-icon image/bmp image/svg+xml;
    gzip_vary on;


    server {
        listen 80;
        root /usr/share/nginx/html;
        include /etc/nginx/mime.types;
        server_tokens off;

        location / {

          try_files $uri $uri/ /index.html;

          proxy_set_header   X-Forwarded-For $remote_addr;
          proxy_set_header   Host $http_host;
          proxy_set_header Upgrade    $http_upgrade;
          proxy_set_header Connection $http_connection;

          add_header Access-Control-Allow-Origin *;

        }
    }
}

Our file tells nginx that anything that lands on / path, will be handled by index.html. This index file is generated from our angular application when we run ng build.


Create the dockerfile for our angular app

Now that we have our nginx.conf ready we want to create dockerfile that will:

  1. Copy our project files to the nginx root we defined earlier /usr/share/nginx/html.
  2. Build the dependencies of our angular project.
  3. Copy our nginx.conf to replace the default one in the original nginx image.

To achieve the steps above, we will use docker Multi Stage Builds. We can read more about multi-stage builds here

Stage 1: The project's dependencies

FROM node:14-alpine as builder

# Do everything as root (optional)
USER root

# Set the working directory. Notice how we use this path in the next stage
WORKDIR /app

# Copy our package.json file only
COPY package.json .

# Install node modules
RUN npm install

Now we can build this image that contains the dependencies of our project and tag it so we can use it in the next stage.

docker build --pull --rm -f "dependencies.dockerfile" -t myproject-dependencies:latest .

The above command will build our dependencies.dockerfile and tag it as myproject-dependencies:latest

Stage 2: the angular app

We can now create our second dockerfile that will take over the next stage of the build process.

# We will continue from the previous stage. That means our dependencies are already here.
FROM  myproject-dependencies as builder

# Copy the application files to image
# The working directory here is still /app because we defined it
# in the dependencies image stage.
COPY . .

# You may pass any additional arguments needed for your builder
ARG BUILD_ENV=production

# Example command to build the application using passed arguments;
RUN npm run ng build:${BUILD_ENV} --output-hashing=all

#----------NGINX----------
# At this point, we have our files generated and ready so now we can
# move everything to  the nginx image
FROM nginx:1.17.10-alpine

# Copy our custom nginx.conf and override the default one
COPY --from=builder /nginx/client.nginx.conf /etc/nginx/nginx.conf

# Cleanup the image root path where we will include our application
RUN rm -rf /usr/share/nginx/html/*

# Copy our generated files from builder stage to the nginx image
# Notice that the /app is the path we saved our files when
# creating the builder image earlier
COPY --from=builder /app/dist/client  /usr/share/nginx/html

We have all we need now to build our final image that will serve our project through nginx.

docker build --pull --rm -f "client.dockerfile" -t myproject-client:latest .

At this point we have built 2 images, dependencies.dockerfile and client.dockerfile. That means that when we build the client image, it will always start from where the dependencies image left of. This saves time for us because we don't have to build the node modules each time we want to deploy a new version of our app. It is important to note that if you made any changes to your package.json then and only then you may want to rebuild the dependencies.dockerfile again.


Optional: Add a proxy for your API requests

Usually, your angular application comes with an API. For development on your local machine you may use a proxy.conf.json to proxy requests to your API.

For Example:

{
  "/api/*": {
    "target": "http://localhost:9000",
    "secure": false,
    "pathRewrite": { "^/api": "" }
  }
}

Now this won't work on a production environment when serving your angular app as a static web service.

A workaround for this is to either do the path rewriting in NGINX or add a proxy directly to the docker container that runs your API.

For example let's assume that your API's container name is api and your API app is running on port 9000. Therefore you can add the following in your nginx.conf file to achieve the path rewrite.

location /api {
    rewrite /api/(.*) /$1 break;
    proxy_pass http://api:9000;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
}

This will redirect any requests starting from /api directly to your docker container that runs the API.

More posts in DevOps