When using Angular, environent variables are kinda cooked into the application bundles on every single build that they are not meant to be changed afterward.

In a recent project, I encountered a challenge when trying to manage multiple environment settings in an Angular app running in a Docker container.

I will try here to solve the problem in a step-by-step, self-explanatory way.

Problem description

The application is a single-page app with a .Net Core backend API. An example of one variable that has to vary from one environment to another is the backend URL.  

Here is how the environment.ts file looks like:

export const environment = {
  production: false,
  backendBaseUrl: 'http://localhost:5151'
};

As you might have guessed, environnment.backendBaseUrl is the part that will be changing.

  • Locally we could have http://localhost:5151
  • On staging we could have staging.some-app-domain.com
  • And on production: some-app-domain.com

According to the Twelve-Factor App, configuration should be stored in the environment. By default, this is not possible with Angular's built-in environment variables. So we need to feed them from an external source.

Solution: Feeding Angular Env Variables from an external source

Let's consider the following:

  1. After a successful build, the default environment.ts gets cross-compiled into a nearly un-editable JavaScript file. One way around that can be externalizing the configuration from the cross-compiled app bundle files.
  2. The content of the  /assets directory never gets changed. It just gets copied into the build directory.

In a Docker environment, we could take advantage of behavior number 2 and create a volume on the assets directory. In that scenario, we just add an "extra environment file" that the main environment files of the Angular app can read from. And hence, whenever we change the config file in the /assets  directory (from the volume) the whole application will adapt. And that way, we have flexibility over environments.

Furthermore, we can make it a little bit better by creating a docker command that can provide values to placeholders in the file every time the docker image of the app is being built. (This will not even require to have a volume)

Okay 🤓, no more talking, let's make that happen.

Let's create a new  env.js   file in the  /assets directory with the following content.

(function (window) {
  window['env'] = window['env'] || {};

  // Environment variables
  window['env']['backendBaseUrl'] = 'https://dot-net-backend.com/api/v1';
})(this);

The Javascript function that we just created defines our future environment variables. It won't be cross-compiled but simply copied to the /dist directory since it is part of the /assets folder,  and can be edited in clear text later.

Now, let's call the function at application startup by adding it to the index.html file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ... -->

    <!-- Load environment variables -->
    <script src="assets/env.js"></script>
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>

Now let's feed the env variables from the new  env.js  in the normal Angular  environemnt.*.ts  files.

export const environment = {
  production: false,
  backendBaseUrl:
    window['env']['backendBaseUrl'] || 'http://localhosst:5151/api/v1'
};

Let's now create a template file for our environment variables.

Let's create a   env.sample.js  in the /assets director

(function (window) {
  window['env'] = window['env'] || {};

  // Environment variables
  window['env']['backendBaseUrl'] = '${BACKEND_BASE_URL}';
})(this);

The ${PLACEHOLDER} variables can now be overwritten during our Docker image creation. We are going to use the  envsubst  shell command for that.

What the command will basically do is copy-paste the content of the env.sample.js into  env.js   replacing the ${PLACEHOLDER} values in the process while picking them from the standard environment.

Here the code comes:

# Dockerfile to build and server the Angular application


###############
### STAGE 1: Build app
###############
FROM node:14-alpine as build

WORKDIR /usr/local/app
# Add the source code to app
COPY ./ /usr/local/app/
# Install all the dependencies
RUN npm install
# Generate the build of the application
RUN npm run build

###############
### STAGE 2: Serve app with nginx ###
###############
FROM nginx:1.19.3-alpine
COPY  --from=build /usr/local/app/dist /usr/share/nginx/html

# Expose port 80
EXPOSE 80

# When the container starts, replace the env.js with values from environment variables
CMD ["/bin/sh",  "-c",  "envsubst < /usr/share/nginx/html/assets/env.sample.js > /usr/share/nginx/html/assets/env.js && exec nginx -g 'daemon off;'"]

There we have everything set up and ready to fly!!!!

I've come to like   docker-compose because of the way it helps managing multiple services while also easily controlling the environment. I would suggest that we use it.

Let's create a  docker-compose.yml file that will build our image and start its container and make it listen on port 4200.

version: '3.5'

services:
  web:
    env_file: ".env"
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - '4200:80'

With the .env file having the following content:

BACKEND_BASE_URL=http://the-bakend-url.com/api/v1

Now if you just run  docker-compose up --build  at the root of the project, you will find the app on port 4200 with the right environment variable that you set in the .env file.

Thanks for taking the time to read.

Ciao 👋🏽

References:

  1. https://medium.com/@wkrzywiec/build-and-run-angular-application-in-a-docker-container-b65dbbc50be8
  2. https://www.youtube.com/watch?v=2nqkIDNkVfY
  3. https://skofgar.ch/dev/2020/08/how-to-quickly-replace-environment-variables-in-a-file/
  4. https://angular.io/guide/build