Hero Image

Injecting system environment variables into containerized Vue.js applications

Problem

Vue.js applications rely on .env files for specifying application settings. This is fine if the application runs

  • in development mode via npm run serve
  • in production mode with the internal Node.js webserver via npm run serve
  • in production mode as packaged applications via npm run build.

As soon as the application gets packaged and is distributed into a container (e.g. Docker), there is no way to set dynamically application settings during the container's start-up. The reason for this is that the Vue.js application effectively runs on the client and is only distributed through the webserver. Having different application settings per environment would require a specific container for each environment. An application setting which differs from environment to environment could be the URL of the application's backend API endpoint.

Solution

To make your application settings configurable, the application settings must be externalized into its own config.js file. This config.js file is then loaded by Vue.js. If any application setting is present in there, it will override the default setting. The generation of the config.js could be done in a few different ways:

  1. Dynamically generate the config.js file through a script: This can either be accomplished through any scripting language like Python and PHP or by using ngx_lua and its content_by_lua_block statement. A drawback for this approach is that the underlying container gets polluted with dependencies you probably don't want to have.
  2. Mounting the config.js file from the container's host: Depending upon your environment you could either use Docker's -v argument to specify a custom config.js or use Kubernetes' volumeMounts to mount a secret which has the config.js content in it. Both approaches do work but they are feeling a little bit clumsy.
  3. Generate the config.js during container's startup: During container startup, a small shell script is executed which checks if required system environment variables exist and, if so, writes them back to the config.js file. When using Docker, the -e flag can be used, in Kubernetes you can use the env section.

This document describes approach 3.. We are using 1 and 2 as starting points.

Runtime

Sequence diagram: Client request

Introduce the public/config.js file

Create a new file public/config.js in your Vue.js application. In this, each application setting is defined:

// will be overwritten during container startup
window.APPLICATION_SETTING_API_ENDPOINT_URL = null;
window.APPLICATION_SETTING_2 = null;
// ...
window.APPLICATION_SETTING_N = null;

Load the custom public/config.js file

In your public/index.html include the config.js file:

<!DOCTYPE html>
<html lang="">
  <head>
    <!-- ... -->
    <script src="config.js" type="text/javascript"></script>
  </head>
  <body>
  <!-- ... -->
</html>

Inject the window.* settings into the Vue.js application

When using TypeScript, you'll have to add the following to your src/application.config.ts file:

// gain access to the *window* global
declare const window: any

// prefer the `window.` application setting over `environment`.
export const API_ENDPOINT_URL = (window.APPLICATION_SETTING_API_ENDPOINT_URL ? window.APPLICATION_SETTING_API_ENDPOINT_URL : environment.APPLICATION_SETTING_API_ENDPOINT_URL);
export const APPLICATION_SETTING_2 = (window.APPLICATION_SETTING_2 ? window.APPLICATION_SETTING_2 : environment.APPLICATION_SETTING_2);
// ...
export const APPLICATION_SETTING_N = (window.APPLICATION_SETTING_N ? window.APPLICATION_SETTING_N : environment.APPLICATION_SETTING_N);

Container startup

Flowchart

Create a new config.js based upon an existing config.js file

The following bash script reads the existing, unpopulated config.js file and fills in each application setting if the environment variable is present. The script does not use any heavy sed or awk features by purpose.

#!/bin/bash
SRC=${1:-} 

if [ -z $SRC ] || [ ! -e "$SRC" ]; then
    echo "Source file '$SRC' does not exist"
    exit 1
fi

OUTPUT="// generated"

while IFS="" read -r line || [ -n "$line" ]; do
    use_line=$line

    if [[ $line =~ "window." ]]; then
        ENVIRONMENT_VARIABLE_NAME=`echo $line | sed -re 's/window\.(.*)\s+\=(.*);/\1/g'`
        value=${!ENVIRONMENT_VARIABLE_NAME}

        if [[ ${value} && ${value-x} ]]; then
            use_line="window.$ENVIRONMENT_VARIABLE_NAME = '$value';"
        fi
    fi

    OUTPUT="$OUTPUT\n$use_line"
done <$SRC

echo -en $"$OUTPUT"

Put it beside your Dockerfile as create_config_js.sh.

Create runtime configuration during container build.

In your Dockerfile, the create_config_js.sh has to be copied to the target image:

WORKDIR /
COPY create_config_js.sh .
RUN chmod +x create_config_js.sh
COPY docker_entrypoint.sh .
RUN chmod +x docker_entrypoint.sh

EXPOSE 80
CMD ["/bin/sh", "docker_entrypoint.sh"]

Update your docker_entrypoint.sh to execute configuration generation

SRC=/usr/share/nginx/html/public/config.js

# quoting is required to keep line breaks
NEW_CONFIG="$(./create_config_js.sh $SRC)"
echo "$NEW_CONFIG" > $SRC

# start webserver, e.g. apache or nginx
nginx -g daemon off;