Environment configuration with React & Nginx
Tags: docker, nginx, react, deployment
Published: 2019-06-13 07:13:45

I’ve been working with React for a couple of weeks, and something I very quickly wanted to know, was how best to achieve environment-specific configuration with a Webpacked app.

For server-side apps, such as Node.js, Ruby on Rails, or even .Net, reading an environment variable and choosing the right config file, or even just reading everything from environment variables is easy, but when your application is a static, minified package of HTML, CSS, and JavaScript, it’s not quite so simple.

There are a few options you have here, so I’ll give a brief description of each, and I’ll describe the downsides that meant I didn’t choose them. First though, I’ll list the parts of my scenario:

* React, created using create-react-app, then ‘ejected’
* Nginx or potentially other static host (eg. CDN)
* Docker
* Integrating with a single, remote backend API (for now)

***

### Using React

#### Webpack on deploy

The documented way seems to be to use `process.env.foo` in your react app, and let the dotenv plugin for Webpack take care of building the values into the minified code. [[1](https://github.com/react-boilerplate/react-boilerplate/issues/1250)]

On the face of it, this looks reasonable… you configure your Docker container with the environment variables, and just run `npm run build` in the `CMD` argument.

For me, this is wholely unacceptable though. The are far too many possibilities for variance, and therefore failure in the process to ‘recompile’ the code for each environment. I need a ‘build once, deploy anywhere' approach.

Another variation on this theme is to build the app with a placeholder in the script, add then use Sed to substitute in the real value on deployment/startup. This again, has too high chance of screwup for my liking.

#### Host detection

Since all of the environments would have different FQDNs, I could use that as a switch to pick between different set of configuration values.

I discounted this one for two reasons. Firstly, this would mean building in the configuration for all of the environments, exposing the existence and addresses of internal infrastructure and giving away some amount of development process. Secondly, if the browser misinterprets the host name somehow, it could fail inconsistently.

There’s also a slight annoyance with local testing conflicting with a user loading the site having saved it to their machine for looking at later, however, this isn’t a huge concern as other things would break.

* * *

### Using Nginx configuration

Since the React tooling didn’t appear to have a decent answer, my next thought was to use Nginx — the web server if use to host the built site — to provide the configuration.

I saw a suggestion for doing the placeholder replacement approach using [Nginx’s `sub_filter` module](https://www.nginx.com/resources/wiki/modules/substitutions/), but I disliked that for the same reason as above.

#### Separate config file

Of course, the next step was to make the configuration a separate file and somehow (I’ll touch on at the end) bring that file into the app when it’s loaded into the browser.

Something that’s done with server-side code is to store config files outside the public files directory, and then use the environment variable (eg. `NODE_ENV`) to pick the right config file, and serve that file up as a generic `/config.js` endpoint. So can we do that with Nginx?

Short answer, no. Long answer, yes, but you’re apparently not supposed to, so they make it hard.

> Out-of-the-box, Nginx doesn’t support using environment variables inside most configuration blocks.

[You can use the Lua or Perl modules](https://docs.apitools.com/blog/2014/07/02/using-environment-variables-in-nginx-conf.html), but that will require installing those modules, making your Docker image bigger, and at that point your site stops being ‘static’ if you’re having to use runtime code.

#### Generate nginx.conf

Several answers on StackOverflow suggest generating the nginx configuration on deployment/startup. Ranging from Sed, and `envsubst`, to erb (Embedded Ruby) and other scripting languages.

String replacement methods, like Sed were out, solely for the possible inconsistency when dealing with complex config files. The necessity to run many times for different variables also doesn’t make it easy.

Scripting languages like erb were out as I’d have to install whichever one inside the Docker image in order to use it, and that’s unnecessary bloat (as well adding to the security risk). There’s also the issue with some scripting languages that usage of special characters (eg. `$`) in the nginx.conf itself might be interpreted, rather than left as literal strings in the config.

#### Copy the app’s config file into place

Perhaps the simplest approach, but easy to neglect, is to have a simple shell script run prior to Nginx starting. That script reads the environment variable and copies the relevant file from an environment-specific directory to a generic file in the static site directory.

```
cp /config/$ENVIRONMENT.json /app/config.json
```

If you’re passing the actual configuration values in, rather than picking a pre-stored file, there’s also the option to generate the config file using a simple echo command

```
echo ‘{“foo”:”$FOO”}’ > /app/config.json
```

Copying a pre-made file is currently my preferred approach. There’s always room for error, but this way doesn’t touch any brittle/complex server configuration, and leaves the server as dumb as possible at runtime. You could even run your site from S3/similar, rather than Nginx, if you wanted.

The whole setup ends up looking something like this:

```
#Dockerfile
FROM nginx:latest
COPY nginx.conf /etc/nginx/nginx.conf
COPY build /usr/share/nginx/html/
COPY config/client-side /usr/share/nginx/config/
COPY launch.sh ./
RUN chmod +x launch.sh
CMD ./launch.sh
```

```
#launch.sh
cp /usr/share/nginx/config/$ENVIRONMENT.json /usr/share/nginx/html/config.json
nginx -g "daemon off;"
```

```
#nginx.conf (snippet)
...
http {
...
    server {
    ...
        location / {
            root   /usr/share/nginx/html;
            index  index.html;
        }
    }
}
```

* * *

### Including the config in the site

The other side of things that was virtually unexplained, was how to get the config file to stay separate to the main application code, but still be included.

#### Code splitting

I tried to see if I could get Webpack to leave the config file out of the bundle, and require it at runtime, but I gave up quickly when it wasn’t obvious. I’m sure there’s probably an elegant way to do it here if I could be bothered to find it.

#### Script tag

Putting the `config.js` file as a `<script>` tag in the HTML template worked, but I’d have to rely on a blocking script loading in order to make sure the config was loaded before the application code.

#### Request it

The easiest way I could think of was to make a module in the application that would make an HTTP request for a relative path `/config.json` and provide the response to the rest of the application.

The added benefit of requesting it is that I can deal with error handling, as well as a loading spinner, if I wanted one.

### The end result

After playing with all of the options, the easiest, as well as more resilient option seems to be to keep separate config files per environment, and request it the first time it’s used in the front-end.