# Dockerize and configure a JavaScript single-page application

_A way of accessing environment variables_

Author: Georg Ledermann  
Published: 2018-04-27  
Tags: JavaScript, Docker, nginx  
Canonical: https://ledermann.dev/blog/2018/04/27/dockerize-and-configure-javascript-single-page-application/

Packing a compiled single-page app into a Docker image is easy. The harder part is making one image behave differently in staging and production without rebuilding it.

---

Building a lean Docker image for delivering a single-page JavaScript application is simple. But it is not that easy when it comes to configuring with environment variables.

Let's say we have a [Vue.js](https://vuejs.org/) application which is compiled with `yarn build` for production. The files to be delivered are placed in the `/dist` folder with a simple `index.html` to load the JS code. It doesn't matter if your JavaScript application is based on React, Angular or something else, the following steps would be similar.

## Building the smallest possible Docker image

After compilation with `yarn build`, we don't need the large `node_modules/` folder with its hundreds of MB. We even don't need Node.js or Yarn. In production, we just need a web server to deliver the compiled files.

So, to build the smallest possible Docker image for production, a [multi-stage](https://docs.docker.com/develop/develop-images/multistage-build/) `Dockerfile` is recommended: First, the build process is performed by Node.js and Yarn. The resulting artifacts are then copied to a new image based on the official `nginx` image:

```docker
# First step: Build with Node.js
FROM node:alpine AS Builder
WORKDIR /app
COPY package.json yarn.lock /app/
RUN yarn install
COPY . /app
RUN yarn build

# Use plain nginx to deliver the dist folder only
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=Builder /app/dist /usr/share/nginx/html
```

We also need an `nginx` simple configuration file named `nginx.conf`:

```nginx
server {
  listen 80 default_server;
  listen [::]:80 default_server;

  root /usr/share/nginx/html;

  index index.html;

  location / {
    # Support the HTML5 History mode of the vue-router.
    # https://router.vuejs.org/en/essentials/history-mode.html
    try_files $uri $uri/ /index.html;
  }
}
```

Okay, that's all. The result is a very small image ([~ 8MB](https://microbadger.com/images/ledermann/docker-vue) download size for my example application) and can be used in production.

## Configure the container with environment variables

With Docker, containers are configured by environment variables. For example, this is used to define backend URLs, API access tokens etc. Assume we want to use the following `docker-compose.yml`:

```docker
version: '3.4'
services:
  app:
    image: 'ledermann/docker-vue'
    ports:
      - '80'
    environment:
      - VUE_APP_BACKEND_HOST=backend.example.com
      - VUE_APP_MATOMO_HOST=matomo.example.com
      - VUE_APP_MATOMO_ID=42
```

Now we have a problem: We deliver compiled JavaScript with `nginx`. There is no `Node.js`, so we have no access to Docker's environment variables.

However, the following approach allows configuration via environment variables:

1. In your JS code, use strings like `'$VUE_APP_BACKEND_HOST'` and assume they contain the configuration value
2. On container startup, modify the existing JS files with search & replace to set values
3. In development, just use `process.env`

### Step 1: Add Configuration class to the JS code

In development, we use Node.js, so [process.env](https://nodejs.org/api/process.html#process_process_env) has access to the local environment. There is a package called [dotenv](https://github.com/motdotla/dotenv) to load env vars from a file. Add this to your project:

```bash
$ yarn add dotenv
```

Remember: We need this in development only. In production, there is no Node.js at runtime, so `process.env` doesn't include any environment variable!

To encapsulate application configuration, I have created a simple class named `Configuration` to access environment variables both in development and production. It includes config strings named `$VUE_APP_XXX` to be replaced later at container startup (see step 2):

```javascript
import dotenv from "dotenv";
dotenv.config();

export default class Configuration {
  static get CONFIG() {
    return {
      backendHost: "$VUE_APP_BACKEND_HOST",
      matomoHost: "$VUE_APP_MATOMO_HOST",
      matomoId: "$VUE_APP_MATOMO_ID",
    };
  }

  static value(name) {
    if (!(name in this.CONFIG)) {
      console.log(`Configuration: There is no key named "${name}"`);
      return;
    }

    const value = this.CONFIG[name];

    if (!value) {
      console.log(`Configuration: Value for "${name}" is not defined`);
      return;
    }

    if (value.startsWith("$VUE_APP_")) {
      // value was not replaced, it seems we are in development.
      // Remove $ and get current value from process.env
      const envName = value.substr(1);
      const envValue = process.env[envName];
      if (envValue) {
        return envValue;
      } else {
        console.log(
          `Configuration: Environment variable "${envName}" is not defined`,
        );
      }
    } else {
      // value was already replaced, it seems we are in production.
      return value;
    }
  }
}
```

Usage example:

```javascript
import Configuration from "configuration";
var backendHost = Configuration.value("backendHost");
console.log(backendHost);
```

### Step 2: Replace vars on container startup

First, we need a bash script named `entrypoint.sh` to run on every container startup:

```bash
#!/bin/sh

# Replace env vars in JavaScript files
echo "Replacing env vars in JS"
for file in /usr/share/nginx/html/js/app.*.js;
do
  echo "Processing $file ...";

  # Use the existing JS file as template
  if [ ! -f $file.tmpl.js ]; then
    cp $file $file.tmpl.js
  fi

  envsubst '$VUE_APP_BACKEND_HOST,$VUE_APP_MATOMO_HOST,$VUE_APP_MATOMO_ID' < $file.tmpl.js > $file
done

echo "Starting nginx"
nginx -g 'daemon off;'
```

What this script does:

- The first time the container is started, the existing `app.*.js` files are copied and used as a template for the following search & replace (line 9-12).
- The main part (line 14) is to use [envsubst](https://www.gnu.org/software/gettext/manual/html_node/envsubst-Invocation.html), which is included in the `nginx` Docker image. It replaces strings in a file with the values of the given environment variables.

To use this script as entrypoint, add this lines to the `Dockerfile` described above:

```docker
COPY entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]
```

### Step 3: Make use of dotenv in development

It is more simple to use environment variables in development. Because we have included the `dotenv` package, we can place a file called `env.local` with this content:

```bash
VUE_APP_BACKEND_HOST="backend.my-site.dev"
VUE_APP_MATOMO_HOST="matomo.my-site.com"
VUE_APP_MATOMO_ID="42"
```

## Result

You find the complete code in my [DockerVue](https://github.com/ledermann/docker-vue) example application on GitHub.
