Runtime environment variables in the browser

A limitation to all web development frameworks is that environment variables has to be injected during build time in order to made be available in the browser. If we have packaged our web application using Docker, that means that we would need to build a different container image for each different environment where we will want to host our application. This is a waste of resources and also introduces a risk of us ending up with a different codebase in our production environment than what we tried out in our QA or test environment.

In this blog post I will describe another approach, where it is possible to inject environment variables during runtime instead. This means that we can use the same container image in all our environments and just change the environment variables available to each running container.

Start by creating a .env file. This file will be populated with the environment variables to use during local development (i.e., when starting the application using npm start rather than Docker).

MY_ENV_VAR=some_value
ANOTHER_ENV_VAR=another_value

Next create a small script called env.sh that will output a JavaScript file containing our runtime environment variables. Remember to make it executable with chmod +x env.sh.

The resulting JavaScript file (env-config.js) containing the values for all our environment variables will be written to the /public folder for local development, and to the root folder when running the deployed version of the app.

#!/bin/sh
# line endings must be \n, not \r\n !

# Write env vars to /public folder when running locally. 
[ "$1" = "RUNNING_LOCALLY" ] && CONFIG_FILE="./public/env-config.js" || CONFIG_FILE="./env-config.js"

echo "window._env_ = {" > $CONFIG_FILE
awk -F '=' '{ print $1 ": \"" (ENVIRON[$1] ? ENVIRON[$1] : $2) "\"," }' ./.env >> $CONFIG_FILE
echo "}" >> $CONFIG_FILE

Now you need to add a line to your index.html in order to access the environment variables when the user visits the running web application.

For Vue it will look like this:

<script src="<%= BASE_URL %>env-config.js"></script>

If you are using React with Create React App, instead use the following line:

<script src="%PUBLIC_URL%/env-config.js"></script>

Change the start script in package.json to also execute the bash script before starting the web server (example for create-react-app, change as applicable for your frontend framework).

./env.sh RUNNING_LOCALLY && react-scripts start

(By specifying RUNNING_LOCALLY here, the script will write the resulting file to the /public folder)

The environment variables are now accessible from your JavaScript code as window._env_.MY_ENV_VAR and window._env_.ANOTHER_ENV_VAR.

You can also add public/env-config.js to .gitignore.

Docker

When running with Docker, we will use an image with node and npm installed to build the container image, and a lightweight NGINX image to run it.

First create a simple NGINX configuration file (nginx/default.conf):

server {
  listen 80;

  location / {
    root   /app;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }
}

The Dockerfile looks as follows. Remember to execute the env.sh script when NGINX starts up.

FROM node:16-alpine3.13 as build
WORKDIR /work
COPY package.json ./
COPY package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:stable-alpine as runtime
COPY --from=build /work/build /app
COPY --from=build /work/env.sh /app/env.sh
COPY --from=build /work/.env /app/.env
COPY --from=build /work/nginx/default.conf /etc/nginx/conf.d/default.conf

WORKDIR /app

EXPOSE 80

CMD ["/bin/sh", "-c", "/app/env.sh && nginx -g \"daemon off;\""]

Kubernetes

The above solution should work fine for most scenarios, but some modifications had to be made in order to get it to work in our Kubernetes cluster as well. The reason is that write permissions are locked down by default for security reasons. In order to mark a folder as writable, we need to mount it as an emptyDir in our pod specification, but that will in turn erase any content already at that path. The solution is to place env-config.js in a subfolder called env which we mount as an emptyDir.

In order to do this, simply replace all occurences of env-config.js above with env/env-config.js.

And finally mount the emptyDir in the pod specification (we use Kustomize for this). NGINX also needed write access to a few folders, which was accomplished in the same manner.

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: runtime-env-vars-demo
          volumeMounts:
            - mountPath: /var/cache/nginx
              name: nginx-cache-volume
            - mountPath: /var/run
              name: nginx-run-volume
            - mountPath: /app/env
              name: runtime-env-vars-volume
      volumes:
        - name: nginx-cache-volume
          emptyDir: {}
        - name: nginx-run-volume
          emptyDir: {}
        - name: runtime-env-vars-volume
          emptyDir: {}

Credits

Most of this solution was found here. Check it out for more details as well as some additional considerations in the issues!