Handling multiple Angular environments with Docker and Nginx

Introduction

Before we get started, here is a quick summary of what this post is about. Imagine you have an angular project with multiple environments, and for each environment you will need to have separate Nginx templates/configs, separate Dockerfiles and, on top of that, more often than not your environmental variables will be duplicated. If this is something you would like to improve, then you are in the right place. πŸ™‚ Setting up dockerfiles and nginx configurations is not in the scope of this article, so keep in mind that some knowledge of docker and nginx is required, as we will not dive into these topics.

Getting started

Project structure

angularapp
β”œβ”€β”€ config
β”‚   β”œβ”€β”€ nginx
β”‚   β”‚   β”œβ”€β”€ default.conf.template
β”‚   β”‚   └── start-nginx.sh
β”‚   β”œβ”€β”€ ssl
β”‚   β”‚   β”œβ”€β”€ cert-name.crt
β”‚   β”‚   └── cert-name.key
β”‚   β”œβ”€β”€ tools
β”‚   β”‚   └── set-env.ts
β”‚   └── Dockerfile
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ package.json
└── src
...

- Dockerfile

Inside our dockerfile, beside installing dependencies and building the angular project, we will RUN:

npm run set-env

which will substitute environment.prod.ts with a new file where we define apiUrl and production variables.

sh -c usr/bin/start-nginx.sh

which will substitute from default.conf.template NGINX_SSL_CERT_PATH variable and replace the nginx default config.

#####################
### Angular/Nginx ###
#####################


# ---------------- ANGULAR SETUP START ----------------
FROM node:12 as builder

ARG api_url

ENV API_URL=${api_url}
ENV PRODUCTION=true

RUN mkdir /ng-app
WORKDIR /ng-app
COPY . .

RUN npm set progress=false && npm config set depth 0 && npm cache clean --force
RUN npm install
RUN npm run set-env
RUN node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng build --subresource-integrity --aot --output-hashing=all

# ---------------- ANGULAR SETUP END ----------------



# ---------------- NGINX SETUP START ----------------
FROM nginx:alpine
RUN apk add gettext

ARG ssl_cert_name

ENV NGINX_SSL_CERT_PATH="/app/ssl/${ssl_cert_name}"

COPY config/ssl/${ssl_cert_name}.crt config/ssl/${ssl_cert_name}.key /app/ssl/
COPY config/nginx/start-nginx.sh /usr/bin/start-nginx.sh
COPY config/nginx/default.conf.template /etc/nginx/nginx.conf.template

RUN chmod +x /usr/bin/start-nginx.sh
# ---------------- NGINX SETUP END ----------------



# 1. Copy build files to nginx html folder
# 2. Substitute ENV variables & Start nginx
COPY --from=builder /ng-app/dist/angularapp/ /usr/share/nginx/html/
CMD /bin/sh -c "usr/bin/start-nginx.sh"

- set-env.ts

const { writeFile } = require('fs');
const colors = require('colors');

const envConfigFile = `export const environment = {
   apiURL: '${process.env.API_URL}',
   production: '${process.env.PRODUCTION}',
};
`;

const targetPath = `./src/environments/environment${production === 'false' ? '' : '.prod'}.ts`;
console.log(colors.magenta(`The file ${targetPath} will be written with the following content: \n`));
console.log(colors.grey(envConfigFile));

writeFile(targetPath, envConfigFile, (err) => {
    if (err) {
        throw console.error(err);
    } else {
        console.log(colors.magenta(`Angular environment was generated correctly at ${targetPath} \n`));
    }
});

- default.conf.template

server {
  listen 80;

  sendfile off;

  gzip              on;
  gzip_types        text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;

  location / {
      alias /usr/share/nginx/html/;
      try_files $uri /index.html;
  }
}

server {
  listen 443 ssl;

  sendfile off;

  ssl on;
  ssl_certificate ${NGINX_SSL_CERT_PATH}.crt;
  ssl_certificate_key ${NGINX_SSL_CERT_PATH}.key;


  gzip              on;
  gzip_types        text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;

  location / {
      alias /usr/share/nginx/html/;
      try_files $uri /index.html;
  }
}

- start-nginx.sh

Here we first replace the variables defined as a first argument '$$VAR_1' in our /etc/nginx/nginx.conf.template then we replace our default nginx default config template with the new config template.

#!/bin/sh

set -e
echo "NGINX_VARS: $NGINX_SSL_CERT_PATH"

envsubst '$$NGINX_SSL_CERT_PATH' < /etc/nginx/nginx.conf.template > /etc/nginx/conf.d/default.conf
nginx -g 'daemon off;'

- package.json

{
  "name": "angularapp",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "set-env": "ts-node ./config/tools/set-env.ts",
    ...
  },
}

- docker-compose.web.yml

version: '3.6'

services:
  angularapp:
    container_name: angularapp
    build:
      context: .
      dockerfile: ./config/Dockerfile
      args:
        api_url: https://example.com
        ssl_cert_name: cert-name
    ports:
      - 4200:80

Building the dockerfile

Now that we have created all the necessary files, we can build our docker container.
We can do this two ways:

docker-compose

docker-compose -f .\docker-compose.web.yml up --build angularapp

docker build

docker build -t angularapp --build-arg ssl_cert_name=cert-name --build-arg api_url=https://example.com ./config/Dockerfile

docker run -p 4200:80 angularapp

Multiple environments

- docker-compose.web.yml

version: '3.6'

services:
  example:
    container_name: example
    build:
      context: .
      dockerfile: ./config/Dockerfile
      args:
        api_url: https://example.com
        ssl_cert_name: cert-name
    ports:
      - 4200:443

  example2:
    container_name: example2
    build:
      context: .
      dockerfile: ./config/Dockerfile
      args:
        api_url: https://example2.com
        ssl_cert_name: cert-name2
    ports:
      - 5200:443

Ready!
Your project can now host multiple environments. All you need to do is add a new container for each environment in your docker-compose.web.yml file.

Thank you for reading this post, feel free to leave a comment if you have any questions.

28