The React single-page application I’ve been working on for the past year has slowly evolved and changed. We’ve been building up the tech stack and requirements as we go, as well as the build/deploy process. The tech stack ended up being pretty straightforward: a Docker container within Docker Enterprise Edition that lives within Docker UCP, running NGINX as the server for the front end. But the build/deploy process ended up being the difficult part.
While in the midst of building, we realized that one of the requirements for deploying was to have certain URLs/keys/secrets be pulled from Docker UCP and dynamically defined without having to rebuild the entire single-page app. So not exactly at runtime, but not at build time either, which is generally how the build system we were using intended (see Create React App environment variables). We considered adding a Node back end to read the variables in and write them to the page, but this felt overly complex since it would only be used to generate variables. Ultimately, we came up with a solution that doesn’t use a back-end and still lets us create dynamic environment variables. Here it is in four complex simple steps:
1. Create a shell file
We’ll start by outputting some comments of what we’re doing to the terminal:
echo "Generating env.js and cleaning up unnecessary files"
Then execute the specified Python file and take the output from the script and put it into env.js:
python /app/generate_env.py > /app/env.js
And finally, remove the files altogether, silently without prompting for confirmation:
rm -f /app/generate_env.py /app/env.jinja2
2. Create the Python file
Add your necessary imports at the top:
import sys from jinja2 import Environment, FileSystemLoader import os
Then create a method for retrieving secrets from Docker (if you have secrets in Docker that you need to access). It is important to note that since we are using Docker Enterprise Edition, this method will need to be adapted to your Docker provider:
def get_secret(secret_name): try: with open('/run/secrets/{0}'.format(secret_name), 'r') as secret_file: return secret_file.read() except IOError: return None
In the next part we’ll use the method we just created above, in addition to pulling from the Docker environment variables, to make a key/value pair object that when output looks similar to the following:
{‘SECRET_NAME”: ‘test’, ‘SECRET_NAME_2: ‘test’}
Notice that for the environment retrieval it is using the .get() method. This is used so that if the variable is not found, the code will not error out but simply fill that value with an empty string:
def output_env_js(): env_vars = {'SECRET_NAME': get_secret('SECRET_NAME')} env_vars['SECRET_NAME_2']=get_secret('SECRET_NAME_2') env_vars['ENVIRONMENT_VAR']= os.environ.get('ENVIRONMENT_VAR') env_vars['ENVIRONMENT_VAR_2']= os.environ.get('ENVIRONMENT_VAR_2')
Now we need to retrieve the location of the template file that we’ll be using to ultimately output our env.js file and pass through the object we just created. We used Jinja2 for this project, which is just a templating language for Python:
j2_env = Environment(loader=FileSystemLoader('/app'), trim_blocks=True) print j2_env.get_template('env.jinja2').render(env_vars=env_vars)
Lastly, we need to actually run the main method that we built above. We’ll add a conditional to check that this code is the main program and not being imported from another module:
if __name__ == '__main__': output_env_js()
3. Create the template file
Finally, we create the template file with the variable object we created in our last step. This will be the file that becomes the env.js file. We start by initiating an empty object:
var namespaced_globals = {};
Then loop over the variable object items and add them to our javascript object:
{% for key, value in env_vars.iteritems() %} namespaced_globals.{{ key }} = "{{ value }}"; {% endfor %}
4. Update your Docker compose file
Add the server you want to use:
FROM nginx:alpine
Then set your working directory and copy your single page app files to that working directory:
WORKDIR /app COPY ./build /app
Next, create your run statement to include the technologies we’ll be using, in our case, bash, NGINX, Python and Jinja:
RUN apk update && \ apk add --no-cache bash nginx python py-jinja2
Now we need to copy over the files we created previously (as well as a server config file), and move them to either our working directory, or the root:
COPY ./scripts/entrypoint.sh / COPY ./scripts/nginx.conf /etc/nginx/nginx.conf COPY ./scripts/env.jinja2 /app COPY ./scripts/generate_env.py /app
And finally we’ll expose our port and run the shell file that triggers the scripts:
EXPOSE 8080 ENTRYPOINT ["/entrypoint.sh"]
And that’s it! You should now have dynamic environment variables that get rebuilt every time you restart your Docker container!