How to access environment variables from a built frontend application in an Nginx container

Jan Tuomi
Jan Tuomi
Dec 19, 2019 · 7 min read

Environment variables are a natural way to configure a containerised application without having to rebuild it when the configuration changes. This is useful when implementing artefact promotion: a built Docker image is signed off in staging/QA and moves on to production. Only the environment changes, no rebuilding necessary.

To use environment variables, the containerised application must read them from the environment on container startup. However, your frontend application is built into servable files, such as index.html and bundle.js, which have no way of accessing the system environment.

In a production environment, it is a good idea to serve a pre-built application using a file server for performance. Nginx is a commonly used file server in frontend images. Nginx doesn’t modify the contents of your built frontend assets, and as such, is unable to inject environment variables to your frontend application.

In this tutorial you will find out how to use environment variables in your containerised pre-built frontend application served by Nginx.

The setup

This tutorial will use the following stack:

  • Rollup (for building the frontend application)
  • Node.js (for running Rollup)
  • Nginx (for serving the built application in production)
  • Docker (for containerising the whole ordeal)
  • Shell scripting (for injecting environment variables)

The technologies above are containerised with Docker so it is not mandatory to install them locally.

The method will work for alternative technologies as well. Feel free to exchange Rollup for Webpack or Parcel, Node.js for Rails, or even Nginx for any other file server.

For the purpose of going through the process we will create an demo project in a fresh directory. If you just want to see the end result, see the end of this post for a repository link.

Initialise the project with npm and install Rollup with some plugins.

npm init -y
npm install -D rollup rollup-plugin-node-resolve rollup-plugin-copy

Plug in some Rollup commands in the script section of your package.json :

...
"scripts": {
"build": "rollup -c rollup.config.js"
},
"devDependencies": {
"rollup": "^1.27.13",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-node-resolve": "^5.2.0"
}
...

And populate your rollup.config.js :

import resolve from 'rollup-plugin-node-resolve';
import copy from 'rollup-plugin-copy';
export default {
input: 'src/index.js',
output: {
file: 'dist/app.js',
format: 'cjs',
name: 'demo'
},
plugins: [
resolve(),
copy({
targets: [ { src: 'src/index.html', dest: 'dist/' } ],
}),
],
};

Create a src directory and create index.js and index.html . Populate index.html with the following:

<!doctype html>
<html lang="en">
<head>
<title>Frontend demo with Nginx + environment variables</title>
</head>
<body>
<h1>Frontend demo with Nginx + environment variables</h1>
<script src="/app.js"></script>
</body>
</html>

Running npm run build should now build your application in the dist directory.

Adding Docker

Create a Dockerfile with the following content.

FROM node:alpine AS builderWORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY rollup.config.js ./
COPY src ./src
RUN npm run buildFROM nginx:alpine AS serverCOPY --from=builder /app/dist /usr/share/nginx/html

The contents of this Dockerfile are fairly straightforward. It uses a multi-stage build with Node.js in the builder stage to bundle our application, and Nginx in the server stage to serve the files.

To run this in Docker, issue the following commands:

docker build -t demo .
docker run -it -p 8080:80 demo

The project is now running on port 8080. Visit http://localhost:8080 to check it out.

This is a great starting point for any production-to-be frontend image. However, this is not enough since we are interested in how to use environment variables in the app.

Injecting environment variables

Any executables and scripts run in the container are able to read the container’s environment variables. Our approach to exposing environment variables to the built application is to construct an entrypoint script that generates a new javascript file that contains our environment variables. This javascript file is then executed in the app.

Here are two ways of implementing such a script.

1. Substituting variables in a template with envsubst

Create a file called inject.template.js in the src directory with the following content:

window.injectedEnv = {
NODE_ENV: '${NODE_ENV}',
FRONTEND_EXAMPLE_ENV: '${FRONTEND_EXAMPLE_ENV}',
};

This file will contain an entry for each environment variable you might want to inject into the application. The values in the injectedEnv object are strings containing shell variable expansion syntax. This is because such strings can be substituted with the actual environment variables using the envsubst tool from the Gettext package.

Modify rollup.config.js to copy the new file into dist/ :

...
plugins: [
resolve(),
copy({
targets: [
{ src: 'src/index.html', dest: 'dist/' },
{ src: 'src/inject.template.js', dest: 'dist/' }
],
}),
],
...

Create a new script file called nginx-entrypoint.sh and give it executable permissions. Plug in this content:

#!/bin/shWWW_DIR=/usr/share/nginx/html
INJECT_FILE_SRC="${WWW_DIR}/inject.template.js"
INJECT_FILE_DST="${WWW_DIR}/inject.js"
envsubst < "${INJECT_FILE_SRC}" > "${INJECT_FILE_DST}"[ -z "$@" ] && nginx -g 'daemon off;' || $@

Modify the Dockerfile to install the gettext package and set the new entrypoint:

FROM nginx:alpine AS serverWORKDIR /usr/share/nginx/html
RUN apk add --no-cache gettext
COPY nginx-entrypoint.sh /
COPY --from=builder /app/dist ./
ENTRYPOINT [ "sh", "/nginx-entrypoint.sh" ]

Let’s build and run the application, and check out the contents of the generated inject.js file using cat:

docker build -t demo .
docker run -e FRONTEND_EXAMPLE_ENV=foobar demo cat inject.js

This should give the following output:

window.injectedEnv = {
NODE_ENV: '',
FRONTEND_EXAMPLE_ENV: 'foobar',
};

Great. Notice how unset environment variables like NODE_ENV will result in an empty string.

Scroll down to Using the injected variables to see how this file could be used.

2. Generating a javascript file from scratch with a shell script

Create a file called nginx-entrypoint.sh and give it executable permissions. Populate it with this script:

#!/usr/bin/bashWWW_DIR=/usr/share/nginx/html
ENV_PREFIX=FRONTEND_
INJECT_FILE_PATH="${WWW_DIR}/inject.js"
echo "window.injectedEnv = {" >> "${INJECT_FILE_PATH}"for envrow in $(printenv); do
IFS='=' read -r key value <<< "${envrow}"
if [[ $key == "${ENV_PREFIX}"* ]]; then
echo " ${key}: \"${value}\"," >> "${INJECT_FILE_PATH}"
fi
done
echo "};" >> "${INJECT_FILE_PATH}"[ -z "$@" ] && nginx -g 'daemon off;' || $@

This looks like your standard string manipulating, shell scripting mess, but the idea is very straightforward. Generate a file called inject.js that contains a javascript statement that constructs the object window.injectedEnv. Any environment variables that start with the substring $ENV_PREFIX (FRONTEND_ in this case) are injected into the object. For example, when developing a React application with CRA, the prefix should be REACT_APP_.

Add some commands to the Dockerfile to make this script our entrypoint. Also, install bash. It is possible to write the entrypoint script using only sh if you have experience in advanced shell wizardry.

...
FROM nginx:alpine AS server
WORKDIR /usr/share/nginx/html
RUN apk add --no-cache bash
COPY nginx-entrypoint.sh /
COPY --from=builder /app/dist ./
ENTRYPOINT [ "bash", "/nginx-entrypoint.sh" ]

Modify the Docker command to set an environment variable called FRONTEND_EXAMPLE_ENV and inspect the generated file with cat:

docker build -t demo .
docker run -e FRONTEND_EXAMPLE_ENV=foobar demo cat inject.js

The output should be valid javascript and the environment variables should now be accessible by the application.

Using the injected variables

To use the inject.js file, simply include it in your index.html. In addition, add in an empty div with id app for displaying our results later.

<!doctype html>
<html lang="en">
<head>
<title>Frontend demo with Nginx + environment variables</title
<script src="/inject.js"></script>
</head>
<body>
<h1>Frontend demo with Nginx + environment variables</h1>
<div id="app"></div>
<script src="/app.js"></script>
</body>
</html>

This script is run before our application code, so the variables are accessible immediately. If the file is not found (for example in development where the entrypoint script is not used) it will simply return 404.

To use the injected environment variables, you can just directly invoke window.injectedEnv.FRONTEND_EXAMPLE_ENV. However, in a normal use case there exists a separate development configuration that uses e.g. create-react-app‘s react-scripts to build and run the project. In such a case, environment variables are accessed through process.env.

To avoid having to access both process.env and window.injectedEnv, you can create a config file that reads environment variables from both of them. It is common to call this config.js:

const processEnv = typeof process !== 'undefined' ? process.env : {};
const injectedEnv = window && window.injectedEnv ? window.injectedEnv : {};
const env = {
...processEnv,
...injectedEnv,
};
export { env };

Using environment variables in your application now requires importing config.js and using the env object.

Finishing the app

Add a bit of application code to read the environment variables from the window object. Populate index.js with the following:

import { env } from './config';Object.keys(env).forEach(key => {
document.getElementById('app').innerHTML += `<div>${key}: ${env[key]}</div>`;
});

This bit of javascript reads all environment variables from env and displays them in their own respective divs on the page.

Conclusions

Using environment variables when serving pre-built assets is more complicated than using them with a live Node.js server, since they are just files in a filesystem without the means to access the system environment. A suitable approach to solving this problem is to inject the specified variables to a file accessible by the application in the browser.

The first approach to injecting the variables is better if you want to specify exactly which environment variables you want to inject, one by one. The second one is more suitable if there is a common prefix for all injected environment variables.

The demo project built in this tutorial is available online: https://gitlab.com/jans.tuomi/medium-frontend-env-demo

Thanks for reading!

Jan Tuomi

Written by

Jan Tuomi

Software Developer in Finland

More From Medium

Also tagged DevOps

Also tagged JavaScript

Also tagged DevOps

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade