Skip to content

Set runtime variables for an Angular app in a Docker Container

A guide for setting up runtime variables for an Angular App which is hosted in a Nginx Docker container (or any else). What's special about this? It does not need another file to be loaded before bootstrapping the app, neither to be included via the docker run command. It simply lets you set variables in your angular app via Environment variables passed to docker by docker run --rm -e "TEST_ENV=This really works!" -it <image_name>.

At the moment this is only tested with debian based docker images like nginx, alpine images do not work currently!

Other guides always want you to load a special file at the app startup.1 2 This does not only defer the startup time by one web request but also makes it harder to just fire up a docker container, as you will have to include the file somehow, by building your own image or mounting it at startup.

Try it out right now by running: docker run --rm -p 8080:80 -e "TEST_ENV=This really works!" -it danielhabenicht/docker-angular-runtime-variables. Change the variable to your liking and navigate to https://localhost:8080.

How does it work?

Basically this solution uses a script that substitutes the variables in some or all files (you can choose) with those given by the environment at container startup and then run the commands given to it.

1. Create the script

Those two scripts do just that. There are two variations as it depends on your needs to manipulate one or multiple files. A combination of both may also work for use cases where some variables are mandatory and other aren't. Go ahead and copy one of them to your project for now!

a) For one file only

This script utilizes the envsubst command to replace the variables given. Throws no error if the variable is undefined.

substitute_env_variables.sh
#!/bin/bash

# The first parameter is the path to the file which should be substituted
if [[ -z $1 ]]; then
    echo 'ERROR: No target file given.'
    exit 1
fi

# The included envsubst command (not available in every docker container) will substitute the variables for us.
# They should have the format ${TEST_ENV} or $TEST_ENV
# For more information look up the command here: https://www.gnu.org/software/gettext/manual/html_node/envsubst-Invocation.html
envsubst '\$TEST_ENV \$OTHER_ENV' < "$1" > "$1".tmp && mv "$1".tmp "$1"

# Set DEBUG=true in order to log the replaced file
if [ "$DEBUG" = true ] ; then
  exec cat $1
fi

# Execute all other commands with paramters
exec "${@:2}"

b) For multiple files

Uses some custom grep/sed logic to replace the variables given. Throws an error if a variable is undefined.

substitute_env_variables_multi.sh
#!/bin/bash

# State all variables which should be included here
variables=( TEST_ENV )

# The first parameter has to be the path to the directory or file which should be used for the substitution
if [[ -z $1 ]]; then
    echo 'ERROR: No target file or directory given.'
    exit 1
fi

for i in "${variables[@]}"
do
  # Error if variable is not defined
  if [[ -z ${!i} ]]; then
    echo 'ERROR: Variable "'$i'" not defined.'
    exit 1
  fi

  # Escape special characters, for URLs
  replaceString=$(echo ${!i} | sed -e 's/[\/&]/\\&/g')

  # Get all files including the environment variable (and ending with '.html') substitute the placeholder with its content
  if [ "$DEBUG" = true ]
  then
    # If DEBUG=true in order to log the replaced files
    grep -rl --include \*.html "$i" "$1" | xargs sed -i "s/\${""$i""}/$replaceString/Ig;w /dev/stdout"
  else
    # If DEBUG=false do it without logging
    grep -rl --include \*.html "$i" "$1" | xargs sed -i "s/\${""$i""}/$replaceString/Ig"
  fi
done

# Execute all other parameters
exec "${@:2}"

2. Prepare your angular app

This is kind of hacky but also the most straight forward way:

  1. Add the <script> Tag to your index.html as shown below.
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Docker - Angular Runtime Variables Demo</title>
    <base href="/" />
    <script>
      var ENV = {
        test: "${TEST_ENV}"
      };
    </script>

    <link href="https://fonts.googleapis.com/css?family=Major+Mono+Display" rel="stylesheet" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>
  1. Create a runtimeEnvironment.ts file in your src/environments folder and paste the content.
runtimeEnvironment.ts
declare var ENV;

export const runtimeEnvironment = {
  test: ENV.test === '${TEST_ENV}' ? false : ENV.test
};

Info

We do not use the environment.ts file as this does induce some errors with advanced angular apps (especially if use in .forRoot() functions), for example:

Error during template compile of 'environment.ts'
Reference to a local (non-export) symbols are not supported in decorators but 'ENV' was referenced

3. Stitch everything together in Docker

While building your docker image you have to copy the script you choose earlier and set the executable rights. Then define it as your entry point and do not forget to specify the path of the file you want to update with the environment variables.

Warning

The Bash script should have LF line endings, otherwise Docker will fail with exec user process caused "no such file or directory" If you you are not sure if you have LF ending in your git repository, check out this guide: https://stackoverflow.com/a/33424884/9277073

Dockerfile
#################
# Builder Image #
#################
# Just building the Angular application here, nothing special about it
FROM node:11.2 as builder

WORKDIR /usr/src/app
COPY ./package.json ./package.json
COPY ./package-lock.json ./package-lock.json
RUN npm ci --silent
COPY . .
RUN npm run build



####################
# Production Image #
####################
FROM nginx

# You can make clear that this image lets the user define some environment variables by stating them:
ARG TEST

# You can also define some standard values to you environment variables
ENV TEST="Hello variable"

# Copy the dist files from the builder image into this image
COPY --from=builder /usr/src/app/dist/docker-angular-runtime-variables /usr/share/nginx/html

# It is very important to set the WORKDIR to the directory of the file to be exectued after the ENTRYPOINT script or reference it absolutely
WORKDIR /etc/nginx

# Copy our ENTRYPOINT script into the docker container
COPY ./substitute_env_variables_multi.sh ./entrypoint.sh
# and mark it as executable
RUN chmod +x ./entrypoint.sh

# Define it as the entrypoint script together with the path or directory that should be searched and substituted with the environment variables
ENTRYPOINT ["./entrypoint.sh", "/usr/share/nginx/html/index.html"]
# Define the command that should be executed at the container startup
CMD ["nginx", "-g", "daemon off;"]

4. Use the Variables in you app

Include the runtimeEnvironment in your angular components, services, pipes etc.. For example:

src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { runtimeEnvironment } from 'src/environments/runtimeEnvironment';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  title = 'docker-angular-runtime-variables';

  constructor() {}

  ngOnInit() {
    this.title = runtimeEnvironment.test;
  }
}
src/app/app.component.html
<div id="content">
  <div class="wrapper"></div>
  <div class="wrapper">
    <h1 id="message">Your environment says: {{ title }}!</h1>
    <a
      id="link"
      target="_blank"
      rel="noopener"
      href="https://danielhabenicht.github.io/docker/angular/2019/02/06/angular-nginx-runtime-variables.html"
      >Link to the blog article</a
    >
  </div>
  <div class="wrapper"></div>
</div>

Thanks for reading. Hope you enjoyed it. ;)

Appendix - Unit Tests with karma

From https://stackoverflow.com/a/19263750/9277073

If you are using the environment variables during your tests you have to mock them by configuring a globals.js and adding it to your test files:

  1. Create a globals.js with your ENV variable.
src/globals.js
var ENV = {
  test: "Karma test value"
};
  1. Link the globals.js in your karma.config.js, by specifying files: ["globals.js]
src/karma.conf.js
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html

module.exports = function(config) {
  config.set({
    files: ["globals.js"],
    basePath: "",
    frameworks: ["jasmine", "@angular-devkit/build-angular"],
    plugins: [
      require("karma-jasmine"),
      require("karma-chrome-launcher"),
      require("karma-jasmine-html-reporter"),
      require("karma-coverage-istanbul-reporter"),
      require("@angular-devkit/build-angular/plugins/karma")
    ],
    client: {
      clearContext: false // leave Jasmine Spec Runner output visible in browser
    },
    coverageIstanbulReporter: {
      dir: require("path").join(__dirname, "../coverage/docker-angular-runtime-variables"),
      reports: ["html", "lcovonly", "text-summary"],
      fixWebpackSourcePaths: true
    },
    reporters: ["progress", "kjhtml"],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ["Chrome"],
    singleRun: false
  });
};

  1. https://juristr.com/blog/2018/01/ng-app-runtime-config/ 

  2. https://www.technouz.com/4746/how-to-use-run-time-environment-variables-in-angular/ 

Comments