Building portable front-end applications with Docker
Tags: docker, javascript, frontend
Published: 2020-10-29 10:40:17

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](https://angular.io/guide/build#configuring-application-environments), [Create React App](https://create-react-app.dev/docs/adding-custom-environment-variables/) and [Vue CLI](https://cli.vuejs.org/guide/mode-and-env.html#environment-variables).

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](https://docs.docker.com/get-started/).

**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:

```html
<!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:

```js
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.

![The 'Hello World!' message displayed on the page](https://dev-to-uploads.s3.amazonaws.com/i/7dyzttmylt46hqihak03.png)

**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:

```docker
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:

```bash
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:

```bash
docker run --detach --publish 8080:80 --name server frontend:latest
```

If you run this command and navigate to [http://localhost:8080](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.

```js
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.

```js
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:

```shell
docker rm --force server
```

Now run the previous commands again to re-build and run the Docker container:

```shell
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:

![The 'Hello Docker!' message displayed on the page](https://dev-to-uploads.s3.amazonaws.com/i/dkb83zi20yn5i1w2tjtb.png)

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`:

```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`:

```docker
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.

```shell
#!/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](https://www.gnu.org/software/gettext/manual/html_node/envsubst-Invocation.html) 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:

```shell
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](http://localhost:8080) and look at our source we can see the following:

```html
<!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:

```js
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:

![The 'Hello Environment!' message displayed on the page](https://dev-to-uploads.s3.amazonaws.com/i/cgrfhmomgipe0vq6ilbv.png)

![A man throwing around confetti in celebration.](https://media1.giphy.com/media/5Yl08OSg4AckeNHpDc/giphy-downsized-medium.gif)

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](https://github.com/jonkoops/portable-frontend).