DEV Community

Jon Koops
Jon Koops

Posted on

Building portable front-end applications with Docker

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>
Enter fullscreen mode Exit fullscreen mode

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>'
Enter fullscreen mode Exit fullscreen mode

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

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/
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>`
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If all is well we should now see our APP_TITLE variable displayed as expected:

The 'Hello Docker!' message displayed on the page

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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

A man throwing around confetti in celebration.

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)

Collapse
 
janpauldahlke profile image
jan paul

i just read the code of this. the conditonal chaining operator makes life so much easiger. best feature 2020