Osaretin
Osaretin
Dynamic Environment Variables for Containerized React Apps: A Step-by-Step Guide
post image

Docker has become an integral part of modern web development, allowing developers to package applications and their dependencies into containers for easy deployment and scalability. However, managing dynamic variables, such as environment-specific configurations, API endpoints, and secret keys, in a Dockerized React app can be challenging. This article explores the common problems associated with dynamic variables in Dockerized React apps and provides practical solutions to address them.

Docker containers offer a consistent environment for running applications across different platforms and environments. However, React applications often require dynamic variables that vary based on the deployment environment, such as development, staging, and production. Managing these variables effectively within Docker containers is crucial for maintaining application consistency and security.

Challenges:

Environment variables in React are static, which means they are not expected to change through out the execution of the application. Typically, when we build a react app for production, we pass environment variables at build time e.g through docker.

Tree shaking: The build tool e.g Vite carries out static analysis of the code and removes files that are not necessary for the build. So if you had a component that is conditionally rendered in dev only environment, when the build tool examines your code at build time, and the environment is set as production, that component would not be included in the build because it is not needed in production. This is a way to reduce the size of the bundle.

Building the images for a particular environment (DEV, QA, UAT, STAGE, PROD) requires passing environment variables at build time, which requires delivering different images for various environments.
The issue with this is that, for a single application, we now need to maintain a significant number of images. Supporting such a staggering number of images in the container registry for various applications may one day become a headache.
The fact that "What you ship to production, should be exactly the same as what has been on pre-prod, staging, dev, or any other env" is another issue with infrastructure best practices.

Also, we get an unnecessary security gap by putting variable value into the image. We have to store docker images as sensitive data and avoid sharing them outside a safe environment.

Don't know how to dockerize a react app? Click Here

Solution:

There are several ways to get around these problems. One popular solution is to set the environment variable's value to a placeholder string at build time (this leaves us with a docker image the placeholder environment vars) and then use a bash script to replace it when at run time.

Take a look at the below Dockerfile:

# Use the official Node.js image as base
FROM node:18-alpine as build

# Set the working directory inside the container
WORKDIR /app

# Copy package.json and yarn.lock to the container
COPY package.json yarn.lock ./

# Install dependencies
RUN yarn install

# Copy the rest of the application code to the container
COPY . .

# Build the React app
RUN yarn build

# Stage 2: Serve the built React app using Nginx
FROM nginx:alpine

# Set an environment variable (example)
ENV VITE_ENV=PLACEHOLDER_VITE_ENV

# Copy the built app from the previous stage to the nginx server's directory
FROM nginx:1.21.6-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY /nginx-custom.conf /etc/nginx/conf.d/default.conf
COPY env.sh /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh

We then do a string search of the placeholder and replace the value with the value of our environment variable.

#env.sh in the root of your project
#!/bin/sh

# Loop through environment variables
for var in $(env | grep PLACEHOLDER_); do
    key=$(echo "$var" | cut -d '=' -f 1)
    value=$(echo "$var" | cut -d '=' -f 2-)

    # Find files containing the placeholder and replace it with the value
    find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' \) -print0 | xargs -0 grep -l "$key" | \
    while IFS= read -r file; do
        sed -i "s|${key}|${value}|g" "$file"
    done
done

This approach has some downsides, we are still faced with the tree shaking issue at build time. In our example, the VITE_ENV value is PLACEHOLDER_VITE_ENV and if we have a component that is rendered conditionally when VITE_ENV value is different, because environment variables are static and not expected to change, that component will not be included in the build.

A Better Approach:

A better way is to use the window object to hold the environment vars. In this approach we build the image without any environment vars, we expect the vars to be passed at runtime, we generate a javascript file (containing the environment vars) at runtime, and finally point our app to the newly generated file in the head of the index.html.

#Dockerfile

# Use the official Node.js image as base
FROM node:18-alpine as build

# Set the working directory inside the container
WORKDIR /app

# Copy package.json and yarn.lock to the container
COPY package.json yarn.lock ./

# Install dependencies
RUN yarn install

# Copy the rest of the application code to the container
COPY . .

# Build the React app
RUN yarn build

# Stage 2: Serve the built React app using Nginx
FROM nginx:alpine

# comment out the environment vars
# ENV VITE_ENV=PLACEHOLDER_VITE_ENV

# Copy the built app from the previous stage to the nginx server's directory
FROM nginx:1.21.6-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY /nginx-custom.conf /etc/nginx/conf.d/default.conf
COPY env.sh /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh

Add a batch script that gets the environment variables passed at runtime and generates a file env.js in the html directory of NGINX

# env.sh at the root of your directory
# Define the path to the JavaScript file
js_file_path="/usr/share/nginx/html/env.js"

# Write JavaScript file header
echo "window.env_vars = {" >> "$js_file_path"

# Write included environment variables to the JavaScript file
echo "  'VITE_ENV': '$(printenv VITE_ENV)'," >> "$js_file_path"

# Write JavaScript file footer
echo "};" >> "$js_file_path"

Create a NGINX conf file (nginx-custom.conf in this example) to serve the env.js file

server {
  
    listen 80;
    location / {
        
        root /usr/share/nginx/html;
        index index.html index.htm;
        try_files $uri $uri/ /index.html =404;
    }

 location /env.min.js {
        default_type application/javascript;
        alias /usr/share/nginx/html/env.js;
    }
    
}

In your index.html, add the script to the head of the document.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="/env.js" type="text/javascript"></script>
</head>
<body>
  
</body>
</html>

When you run the app passing your environment vars e.g

docker run -e VITE_ENV=DEV your_image_name

You should expect to see a env.js file generated, and contains the object:

window.env_vars = { VITE_ENV: 'DEV'}

You can inspect your network tab to see this file.

In you REACT app, you can access the values like this:

const environment = window.env_vars.VITE_ENV;

{(environment === 'DEV' || environment === 'QA') && <Component />}

If you are using Typescript, you could get a type error in the window object. To resolve this, add the code below to your index.d.ts

export {};

declare global {
  interface Window {
    env_vars: Record<string, string>;
  }
}

Conclusion:

Managing environment variables in containerized React applications presents several challenges, including limitations in dynamic environment variable forwarding, security risks associated with embedding variables in images, and scalability issues with maintaining multiple images for different environments. Addressing these challenges requires careful consideration of deployment strategies, security best practices, and tools for managing environment-specific configurations effectively. By overcoming these hurdles, developers can streamline the deployment process, enhance application security, and improve overall scalability and maintainability of containerized React applications.


Osaretin Igbinobaro

Osaretin Igbinobaro

Software Engineer @ Apadmi


Osaretin


© 2024 osaretin.dev All rights reserved.