At ProdataKey, we agree with the tenets presented by Heroku’s Twelve-Factor App methodology. We hope to apply these principles to all parts of our architecture.
One part of our architecture is an Nginx Docker container which serves our web application bundle. It was difficult to determine how certain parts of Twelve Factor would apply to this. Specifically, in the “Config” factor:
Apps sometimes store config as constants in the code. This is a violation of twelve-factor, which requires strict separation of config from code. […] The twelve-factor app stores config in environment variables.
The question is: how can we configure this bundle, which executes on the user’s device, to read data from the server’s environment variables?
Here’s how we did it:
To expose these environment variables to the client JS, we add the following to our index.html:
What is the
//GLOBAL_CONFIG_TOKEN? It’s just a unique string that we are able to replace on the fly when the app is served through nginx. So how do we do that?
Let’s look at the relevant snippet from our nginx.conf:
There’s an Nginx module called http://nginx.org/en/docs/http/ngx_http_sub_module.html which is included by default in the Nginx Docker image. This module provides us with the ability to replace arbitrary strings with other strings. We specify the
sub_filter_once option as a minor optimization so that it only tries to replace one instance of the string.
However, the syntax seen here to swap in
MY_ENV_CONSTANTS isn’t the actual syntax for injecting environment variables into nginx.conf. In fact, “out-of-the-box, nginx doesn’t support environment variables in most configuration blocks.” So how does this work? We need one more piece to make this work.
We use multi-stage builds to prepare our application bundle for our Nginx image. Here is just the final stage of our Dockerfile:
There are a few things going on here, so I’ll talk about each one.
The first thing I do is install tini and set it to be the entrypoint. I’ll come back to this later.
After copying in our application bundle, I add our server’s nginx.conf, although I call it
default.template instead of
Then, I declare the environment variable in our Dockerfile,
MY_ENV_CONSTANTS, and give it a default value of an empty string. We will give this variable a value elsewhere, such as our docker-compose.yml.
The final line is the most crucial. Rather than immediately running nginx, I use
sh -c to run multiple commands in a sequence. The second command is Nginx, but the first one is
envsubst. This program takes in a template as input, substitutes in the specified environment variables, and outputs the result. Notice that
envsubst is not executed at build time, but at runtime! This means we can substitute in MY_ENV_CONSTANTS in our docker-compose file later.
So, why do we need tini? Because Nginx is a child of our
sh process, asking Docker to perform a graceful shutdown will fail. This is because the
SIGKILL signal that Docker sends does not automatically propagate to child processes, and
sh does not automatically provide this functionality. Enter
tini, which is able to properly forward these signals to all its children. The creator of tini writes about this in more detail here.
MY_ENV_CONSTANTS in docker-compose
Here’s an example of a service in a docker-compose file which provides the needed environment variable:
services: web: image: my-static-server environment: MY_ENV_CONSTANTS: > /* this is injected inside of the braces of our empty JS object! */ apiRoot: "http://localhost:9090", debugLogsEnabled: true ports: - "80:80"
> symbol in our yaml file begins a multi-line string. Please note that indentation and newlines are stripped from the string. Don’t use
//newline-terminated comments in there, or you will get unexpected results!
At this point, you should be able to launch an Nginx instance carrying a static bundle which is still configurable by its server’s environment variables. Happy coding!