A likely scenario you will run into in your career as a front-end developer is that you will want to have your application deployed to multiple environments. Although these environments are mostly the same your application might have to behave slightly differently in each one of them.
For example, an application running on a staging environment might have to make calls to the API server running on the staging domain, or your whitelabel application might have to show a different brand based on which environment it is deployed.
This is where environment variables can help out. You can provide an environment file and build your application for each environment on which your application can possibly run. This is actually a very common approach that is used by tools like Angular CLI, Create React App and Vue CLI.
Although this is a great solution, it has a couple of downsides when your application continues to grow in complexity:
Multiple builds
If you have set up a CI/CD pipeline, your build server will have to build your whole application for each environment. The more complex your application becomes the longer you will have to wait and waste precious resources and time.
Less portable
Besides complicating your build, you will also have to deploy the end result to the correct environment. The downside of this is that the code can only run on that specific environment and nowhere else.
To solve the issues mentioned above we can take a note from what our fellow developers do with applications that run on the server, which is to provide these environment variables at the moment our application boots up. This is easier said than done since we’re deploying our applications as static files, and thus we have no code running on the server itself.
Since Docker has become the industry standard for shipping applications, we’ll be using it here to make a new application deployable and to provide environment variables dynamically to it. If you have no prior experience with Docker it is recommended to read up on this topic first.
Note: We’re creating a new application here but the steps outlined below can also be applied to any existing front-end application, compiled or not.
Let’s start by creating a simple boilerplate for our application with an index.html
file:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My awesome application</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="main.js"></script>
</body>
</html>
We’re using a script element here to directly load our JavaScript. This is done to keep this tutorial as simple as possible but you can use whatever tooling you prefer, such as WebPack or the built-in CLI tools of your framework of choice to build your application.
Let’s add the main.js
file and use it to add some content to the page:
const root = document.getElementById('root')
root.innerHTML = '<h1>Hello World!</h1>'
If all goes well you should be seeing the 'Hello World!' message displayed in your browser when opening the index.html
file.
Tip: You can start a simple HTTP server for local development by running npx http-server .
in your working directory.
Now that we have our application up and running we can start putting it in a Docker image so that it can be easily shared and deployed. Let’s start off by placing the newly created files in a directory called src
. This is where we will keep our application code that will end up as static files in the Docker image. In case you are compiling your application, this will likely be your dist
or build
directory.
To serve the files we’re going to need an HTTP server. Let’s create a new file called Dockerfile
in the root of our working directory and add the following content:
FROM nginx:latest
RUN rm -rf /usr/share/nginx/html/*
COPY ./src/ /usr/share/nginx/html/
Here we’re using the latest version of NGINX as our server, and the files that are used by NGINX to show the default splash page are removed and replaced with the contents of our own application. Now that we have a Dockerfile
let’s build a Docker image for our application by running the following command in the working directory:
docker build --tag frontend .
This will build a new Docker image tagged with the label 'frontend', which we can run in combination with the docker run command:
docker run --detach --publish 8080:80 --name server frontend:latest
If you run this command and navigate to http://localhost:8080 you should now see the same page we saw before but now served from NGINX using Docker!
To hold our default environment variables in the project we’re going to create a new file called environment.js
and add it to the src
directory.
const defaultEnvironment = {
APP_TITLE: 'Hello Docker!'
}
export default defaultEnvironment
We want to use our new APP_TITLE
variable and display it on our page, so let’s update main.js
to reflect this.
import environment from './environment.js'
...
root.innerHTML = `<h1>${environment.APP_TITLE}</h1>`
Great, now let’s see if these changes are working correctly. Stop the Docker container that is running with the following command:
docker rm --force server
Now run the previous commands again to re-build and run the Docker container:
docker build --tag frontend .
docker run --detach --publish 8080:80 --name server frontend:latest
If all is well we should now see our APP_TITLE
variable displayed as expected:
Ok, so far so good. We have a separate file for our environment variables and our application is running in Docker. However our APP_TITLE
variable will always be the same no matter where we run our container. To truly make our application portable, we’ll need some way to provide the environment variables to the application when we are starting our Docker container.
To do this we’re going to use another common practice in front-end frameworks when transferring state from a server-side rendered application, which is to put the state into a script element when the index.html
is rendered. Let’s add the following code to our index.html
:
<script id="environment" type="application/json">$FRONTEND_ENV</script>
Here we are adding a placeholder called FRONTEND_ENV
that we’re going to replace with some JSON data when our Docker container boots up.
Note: It is recommended to include this script element conditionally for your production builds to prevent issues when parsing it's contents as JSON during development.
Add the following lines to the end your Dockerfile
:
COPY ./startup.sh /app/startup.sh
CMD sh /app/startup.sh
Docker provides us with the CMD
instruction, this allows us to run a specific command the moment the container starts up. In this case we are copying the startup.sh
script into our Docker image and we run it directly once the container starts. Let’s take a look at what this script looks like and add it to the root of the working directory.
#!/bin/sh
basePath=/usr/share/nginx/html
fileName=${basePath}/index.html
envsubst < ${fileName} > ${basePath}/index.env.html
mv ${basePath}/index.env.html ${fileName}
nginx -g 'daemon off;'
There is a lot going on in this file but the most important line is the one that runs the envsubst command. This utility is provided by the GNU gettext utilities, which are part of almost all Linux distributions and thus also our Docker container. It reads the contents of our index.html
file and replaces all text prefixed with a dollar sign (such as our FRONTEND_ENV
) with the equivalent environment variable provided to the Docker container.
We’re almost there so let’s see if our code is working properly so far. We’ll have to build a new version of our Docker image and start it with our new environment variable:
docker rm --force server
docker build --tag frontend .
docker run --publish 8080:80 --name server --env FRONTEND_ENV='{ "APP_TITLE": "Hello Environment!" }' frontend
Here you can see that we are providing the FRONTEND_ENV
as JSON text to our Docker container. Now if we open up our page at http://localhost:8080 and look at our source we can see the following:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My awesome application</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="main.js"></script>
<script id="environment" type="application/json">
{ "APP_TITLE": "Hello Environment!" }
</script>
</body>
</html>
Our index.html
now has our environment variables inside as expected. This means that we now only have to build a single Docker image and we can deploy it to any environment simply by providing it with different environment variables. No need to build your application multiple times!
This is great, but we still need to read this JSON data and overwrite our default environment variables so let’s add some code to environment.js
to do just that:
const defaultEnvironment = {
APP_TITLE: 'Hello Docker!'
}
function getEnvironmentVariables() {
const element = document.getElementById('environment')
if (!element?.textContent) {
return {}
}
try {
return JSON.parse(element.textContent)
} catch (error) {
console.warn('Unable to parse environment variables.')
}
return {}
}
export default {
...defaultEnvironment,
...getEnvironmentVariables()
}
Here we have a new function that will get our element containing the environment variables and parse its contents as JSON if it exists and contains a non-empty value. When we export our default environment, we overwrite it with the environment variables that are obtained from the index.html
.
Now if we rebuild our image and start it with the same FRONTEND_ENV
environment variable as before, now we can see that our custom title shows up:
That’s it! We now have a nice and portable Docker image that we can use for our application. If you want to view the full code used in this post you can find it on Github.
Top comments (1)
i just read the code of this. the conditonal chaining operator makes life so much easiger. best feature 2020