tldr;
Leverage bundler tools and a runtime script to inject dynamic configurations into static frontend deployments, enabling a single build to adapt to multiple environments seamlessly.
Preface
If you're serious about your DevOps practices, you likely aim for a build once, deploy anywhere approach. The idea behind this approach is that you build your application artifact (e.g. a Docker image) once and with the tweaking of configuration parameters you can deploy it to multiple environments, such as staging and production. This approach is also described in the popular Twelve Factor App methodology.
In your backend applications it is quite easy to achieve that. You have a running process and you can simply access your environment variables at runtime.
In static frontend applications however, this is not as easy. You probably have some sort of web server that serves your pre-built files. There is no process environment you can access from your client-side JavaScript code. So how can you make sure in such cases to be able to deploy the same artifact to any environment?
Leverage your bundler's capabilities
The following passages are based on Vite, but the concepts are applicable to other frontend JS tools as well.
Vite allows for the injection environment variables at build time. Environment variables prefixed with VITE_
are bundled with your application. These variables prefixed with VITE_
become accessible in your code as import.meta.env.VITE_<VARIABLE_NAME>
.
If you let vite inject this .env file during the build process, the first two ones will be accessible via import.meta.env.VITE_BACKEND_API_URL
and import.meta.env.VITE_GOOGLE_MAPS_KEY
. SOME_SECRET
is not prefixed with VITE_
, so it will be disregarded.
VITE_BACKEND_API_URL=https://example.com/api
VITE_GOOGLE_MAPS_KEY=12345qwerty
SOME_SECRET=mySecret
Awareness of this capability is crucial for achieving our ultimate goal.
The final trick
Rather than passing the actual values, we use placeholder values:
VITE_BACKEND_API_URL=MY_APP_PREFIX_BACKEND_API_URL
VITE_GOOGLE_MAPS_KEY=MY_APP_PREFIX_GOOGLE_MAPS_KEY
Just before starting the application, we run a script that replaces all occurrences of MY_APP_PREFIX_<VARIABLE_NAME>
in the bundle with the correct values. Here is a small bash script that you can run in your container right before starting the actual application. This script assumes that we're serving the files with an nginx web server from the standard directory /usr/share/nginx/html
.
#!/bin/sh
RELEVANT_ENV_VARS=$(env | grep MY_APP_PREFIX)
for i in $RELEVANT_ENV_VARS
do
ENV_VAR_NAME=$(echo $i | cut -d '=' -f 1)
ENV_VAR_VALUE=$(echo $i | cut -d '=' -f 2-)
find /usr/share/nginx/html -type f -name '*.js' -exec sed -i "s|${ENV_VAR_NAME}|${ENV_VAR_VALUE}|g" '{}' +
done
Let's dissect this script line by line.
-
RELEVANT_ENV_VARS=$(env | grep MY_APP_PREFIX)
- we identify relevant environment variables by searching for those with the specified prefix. -
for i in $RELEVANT_ENV_VARS
- we loop over each of the found variables. -
ENV_VAR_NAME=$(echo $i | cut -d '=' -f 1)
- the left hand side of an environment variable declaration is the name.cut
is the equivalent of the widely availablesplit('=')
function in other languages. -
ENV_VAR_VALUE=$(echo $i | cut -d '=' -f 2-)
- the right hand side of an environment variable declaration is the value. -
find /usr/share/nginx/html -type f -name '*.js'
- we search for all JavaScript files in the html folder-
-exec sed -i "s|${key}|${value}|g" '{}' +
- for all found files execute ased
command that replaces all occurrences of the variable name with the variable value.
-
All you now have to do is run this script before serving your application and adding your environment variables to the execution context of your container. Here is a small Dockerfile
example. The example assumes you have a built bundle in dist/my-app
FROM nginx:alpine
RUN apk add --no-cache bash
WORKDIR /usr/share/nginx/html
COPY dist/my-app .
COPY replace-envs.sh /usr/local/bin/replace-envs.sh
EXPOSE 8080
CMD /bin/bash -c "/usr/local/bin/replace-envs.sh && exec nginx -g 'daemon off;'"
This is it! If you now run your container and pass the environment variables accordingly, they will be replaced before starting the actual nginx process. In a docker-compose.yml
this could look like this:
version: '3'
services:
my-app:
image: my-app # replace with your image
ports:
- '8080:8080'
environment:
- MY_APP_PREFIX_BACKEND_API_URL=https://example.com/api
- MY_APP_PREFIX_GOOGLE_MAPS_KEY=12345qwerty
Conclusion
By leveraging the ability of modern bundlers such as Vite to inject environment-specific variables and using a clever script to replace placeholders with actual environment values at runtime, developers can maintain a single artifact across multiple environments. In addition to streamlining the deployment process, this methodology aligns with the best practices outlined in the Twelve-Factor App Methodology, particularly with respect to app configuration. Implementing these practices ensures that your frontend deployments are as flexible and efficient as your backend, paving the way for a more robust and scalable application infrastructure.
Top comments (0)