Immutable Single Page Application – wrap frontend with docker and reuse it on many environments

Problem overview

Docker conquer the world – the basic questions it how to place frontend application inside container? In docker world we want to build artifact (image) once and reuse it on many environments. Such immutability of images gives us some guarantees regarding consistency of environments so our staging can be as much as possible similar to production – that’s good for stability of our system because we can be sure that code deployed on staging will behave the same on production. So to be able to run one artifact in different environments we usually extract whole env-specific configuration to environment variables or some ConfigMaps which are mounted in runtime as config files and contains information required to run on particular environment e.g. db connection, secrets, public domain name etc.

In case of backend services it’s really simple as they can read environment variables in runtime which are created by DevOps team but what about frontend? Here situation is not so trivial as usually we can pass environment variables to frontend only in build time and those variables are just placed as a strings in static js files (e.g in React). It means that we need separate build pipeline of frontend for different environments like frontend-development, frontend-staging, frontend-production, frontend-client1, etc. So in such situation we must have many different artifacts of one codebase what is not really good situation – please visit site https://immutablewebapps.org/ if you want to know more.

Implementation of solution

We need possibility to configure frontend in runtime – it means before starting static files hosting we must configure frontend files so they can work well with environment on which they are hosted at the moment. When using docker that’s really easy – full source code explained in this article can be found there: https://github.com/jakubbujny/article-immutable-single-page-application

Let’s say that we have simple SPA application which is divided into index.html file as entrypoint and script.js where whole site is located. Common case is to inject URL to backend API which can be located in different places in different situations, e.g.:

  • locally I want to use http://localhost:port as API URL
  • maybe I want to test frontend on mobile which is in the same LAN as my PC so I want to inject http://my_pc_ip:port as API URL
  • maybe I want to create Review Apps so my API URL will look like api-git_commit_hash.review.my-project.com
  • on normal envs I just want to use api.env.myproject.com

As example we can use such simple html file which just make place to show API URL and include our dummy script:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div >I'm gonna make shots to <span id="foo" style="color: red"> </span> </div>
<script src="script.js"> </script>
</body>
</html>

Source code of script.js

window.onload = function (ev) {
    var element = document.getElementById("foo")
    element.innerHTML = ? //of course that won't work - we must think what should we place here
}

Dockerfile would look like:

FROM nginx:1.15.7

ADD index.html /usr/share/nginx/html/index.html

ADD script.js /usr/share/nginx/html/script.js

CMD nginx -g "daemon off;"

So we must consider how to pass environment variable to static files when container is starting. The simplest solution is to use such trick in CMD section of Dockerfile:

CMD sed -i "0,/#API_URL_TOKEN#/{s@#API_URL_TOKEN#@${API_URL}@}" /usr/share/nginx/html/index.html && nginx -g "daemon off;"

This sed command look for the first (0,) occurrence of #API_URL_TOKEN# in index.html file and then replace it with API_URL environment variable. Usage of “@” as delimiter in sed command is very important as when we will use standard “/” we have conflict with protocol part of url (https://). After such configuration nginx is starting.

Then in section of index.html we should add following script:

<script>
        apiUrl = "#API_URL_TOKEN#"
        if (apiUrl === "#API_URL_TOKEN#") {
            apiUrl = "localhost"
        }
</script>

So just place global variable with apiUrl and token as value. As we replace only first occurrence we can write condition: if replace not happened just set defaults for local development.

Now we can just place

element.innerHTML = apiUrl

and use following commands to build and test our POC

docker build -t jakubbujny/immutable-spa .
docker run -p 80:80 -e API_URL=https://injected-in-env.jakubbujny.com -it jakubbujny/immutable-spa

Those commands just build docker image and run it with API_URL env variable which will be injected into index.html file. Effect:
Screenshot from 2018-12-26 12-47-21

Dedicated to O.S. 😉

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s