April 27, 2019

Using environment variables for React in Docker

I've recently been working on a project built using React which I have been building Docker images for which made some use of environment variables. I bumped up against the fact that environment variables are baked into a React production build when the Docker image is built, making it impossible for me to change their value without building the image from scratch.

Obviously this isn't great as it breaks the entire purpose of dockerising the project in the first place, however I found this useful blog post on a pretty nifty way to get round this limitation using Nginx. (I'm hoping to condense the info down here but I'd definitely recommend having a read.)

The process boils down to that we pass in dummy values for the environment variables at build time which we can easily find and replace with the true values at runtime using Nginx's sub_filter directive.


Processing your .env file

This step isn't strictly necessary but it prevents you from having to maintain two sets of .env files. I do this within the Dockerfile in order to keep my project folders clean, hence why I overwrite the .env file each time.

Imagine we start with a .env file with the contents

REACT_APP_API_URL='http://localhost:3664'

We can build a .env file with the env vars set to dummy values using the command.

cat .env | grep = | sort | sed -e 's|REACT_APP_\([a-zA-Z_]*\)=\(.*\)|REACT_APP_\1=NGINX_REPLACE_\1|' > .env

Here we are editing .env so for the example above we get a line which reads as

REACT_APP_API_URL=NGINX_REPLACE_API_URL

The React app is then built as normal using these new environment variables. Obviously these are pretty useless as they stand, but this is where Nginx comes in.


Using Nginx sub_filter

Once the React app is built, we now have a set of JavaScript files in which each environment variable REACT_APP_ENV_VAR has been replaced by the string "NGINX_REPLACE_ENV_VAR". If we can replace these strings in these files at runtime with the wanted env vars then we're sorted.

An easy way to do this is through editing the Nginx configuration files. Nginx has a sub_filter directive which allows us to automatically replace any occurrences of a given string in the files it serves with another. For example, placing the following within your Nginx config file will replace all instances of "foo" with "bar".

server {
    
    # snip rest of contents
    
    location ~* ^.+\.js$ {
        sub_filter "foo" "bar";
        sub_filter_once off;
        sub_filter_types *;
    }
}

At this point we want to define a set of filters to place in this file which look like

sub_filter "NGINX_REPLACE_FOO" "${FOO}"

This ${FOO} is then the actual environment variable as defined in your Dockerfile which you set at runtime. Setting its value is equivalent to as if we had built the React app with ${REACT_APP_FOO} set to that value. The below script will build a set of these filters from our .env file, then place them within a config file at the position of the placeholder string "LOCATION_SUB_FILTER".

NGINX_SUB_FILTER=$(cat .env | grep '=' | sort | sed -e 's/REACT_APP_\([a-zA-Z_]*\)=\(.*\)/sub_filter\ \"NGINX_REPLACE_\1\" \"$\{\1\}\";/')

cat nginx.conf.sample | sed -e "s|LOCATION_SUB_FILTER|$(echo $NGINX_SUB_FILTER)|" | sed 's|}";\ |}";\n\t\t|g' > nginx.conf.sample

To substitute the values of the real environment variables into these filters, we run envsubst on the Dockerfile entrypoint. The environment variables from the shell (i.e. those passed in through Docker) take precedence over those in .env. We write the result to the config file default.conf which is then given to Nginx to be used.

envsubst < nginx.conf.sample > default.conf

Note

envsubst is pretty greedy when it comes to overwriting env vars. If you have any strings beginning with $ in your Nginx config file which you don't want to be overwritten you will need to define a new environment variable which sets the string equal to its original value. You can see an example of this in this Dockerfile.

This is partly why we perform substitutions using Nginx rather than using envsubst directly on the javascript files generated from the production build, in order to minimise the number of extra environment variables.