Static No More: Transforming Frontend Deployment with Dynamic Configuration
Tags: docker, vite, webpack, react
Published: 2024-03-09 22:19:00

# 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](https://12factor.net/config).

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
<small>The following passages are based on [Vite](https://vite.dev), but the concepts are applicable to other frontend JS tools as well.</small>

Vite allows for the [injection environment variables](https://vitejs.dev/guide/env-and-mode.html) 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`.

```sh
#!/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 available `split('=')` 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 a `sed` 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`

```dockerfile
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:

```yml
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.

## Sources
[Vite](https://vite.dev), [Docker](https://docs.docker.com/), [Twelve Factor Apps](https://12factor.net/), [Stack Overflow](https://stackoverflow.com/a/77454537)