wala's tech blog

Runtime Env Injection for React

Hey everybody, it's Wala again. This time I'm going to be talking about something that has recently plagued one of my projects.

The Problem

For my project, I was attempting to deploy a Vite React app on DigitalOcean for one of my university's clubs that I help around with. During development, Vite automatically injects environment variables using import.meta.env.MY_ENV_VARIABLE. We would use this throughout development without realizing how much this would mess us up in the future.

A quick note, we also were using Dockerfiles to build our React Vite app, which is partially the reason we were running into the following problem...

After we built the Docker image for our frontend and decided to deploy it, all of our environment variables suddenly stopped working even when we injected them into the running container. Does anyone have a guess why?

It's because a React application is compiled into static files... we should have known this!!! It is served statically anyways, even though our Dockerfile compiled it using npm serve which would host the static files on port 80 for us.

The (Possible) Solutions

Jeez, I couldn't believe I had overlooked this very issue! I was so used to working with backend servers that I had completely forgot that React itself is served statically. Therefore, we had a few solutions to solve this issue.

Dockerfile Shenanigans

One solution could have been to inject the environment variables into the Dockerfile while it was building in our CI/CD pipeline (Github Actions). This was a very enticing option, but that would mean our environment variables would be stored in Github secrets which would be separate from our main secret storage Infisical. This solution would have been a very simple one to implement, you would just simply add ARG parameters in the build process and insert using ENV.

FROM node-1.22-alpine AS build

ARG ENV_VARIABLE_1
ARG ENV_VARIBLE_2

ENV ENV_VARIABLE_1=${ENV_VARIABLE_1} \
    ENV_VARIABLE_2=${ENV_VARIABLE_2}

npm build

and then in your CI/CD bash, you would simply run Docker's build command with

docker build \
  --build-arg ENV_VARIABLE_1=${{ secrets.ENV_VARIABLE_1 }}\
  --build-arg ENV_VARIABLE_2=${{ secrets.ENV_VARIABLE_2 }} \
  -t myapp:dev .

I would completely recommend this if you simply need a quick working version of your React app. However, since we wanted to isolate our environment variables inside Infisical, this would not do.

Insert During Runtime

Something hacky you can do is write a script to inject the shell's environment variables at runtime! All you have to do, is to create a placeholder structure to hold the environment variables inside your React app, then populate it whenever you start up your Docker container.

For example:

/public/env-config.js

window.ENV = {
  // Use "REPLACE_ME" or an empty string as a placeholder value.
  VITE_BASE_API_URL: "",
  VITE_DISCORD_OAUTH_CLIENT_ID: "",
  VITE_ALLOWED_HOSTS: [],
};

and place this as a script in your index.html file like so:

<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/core-favicon-light.svg" />

  <link rel="icon" type="image/svg+xml" href="/core-favicon-light.svg" media="(prefers-color-scheme: light)" />

  <link rel="icon" type="image/svg+xml" href="/core-favicon-dark.svg" media="(prefers-color-scheme: dark)" />

  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>SwampHacks Core</title>

  <!-- MANDATORY: Used for injecting env variables at build -->
  <script src="/env-config.js"></script>
</head>

<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>

</html>

Then, you can just go ahead and make a entrypoint.sh script that you run on start up to populate the env-config.js like so:

#!/bin/sh

# The location where the built static files (including the placeholder env-config.js)
# are copied in the production stage.
CONFIG_FILE_PATH="/app/dist/env-config.js"

echo "Generating runtime configuration file at $CONFIG_FILE_PATH..."

# Safely inject environment variables into the config file
cat <<EOF > $CONFIG_FILE_PATH
window.ENV = {
  VITE_BASE_API_URL: "${VITE_BASE_API_URL}",
  VITE_DISCORD_OAUTH_CLIENT_ID: "${VITE_DISCORD_OAUTH_CLIENT_ID}",
  VITE_ALLOWED_HOSTS: '${VITE_ALLOWED_HOSTS}'
};
EOF

echo "Runtime configuration generation complete."

# Execute the main command passed to the container (the 'serve' command from CMD)
exec "$@"

and last but not least, make sure to run this and pass through your serve command inside your Dockerfile!

FROM node:22.16.0-slim AS prod
WORKDIR /app

# Copy built React app (dist folder)
COPY --from=build /app/dist ./dist

# Install 'serve' to serve static files
RUN npm install -g serve

# --- Runtime Config Setup ---

# Copy the entrypoint script into the working directory
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh

# Set the entrypoint to run the script first
ENTRYPOINT ["./entrypoint.sh"]

EXPOSE 80

# This CMD will be passed as arguments ("$@") to the entrypoint script
CMD ["serve", "-s", "dist", "-l", "80"]

And boom! You are now ready to access the variables as window.ENV inside your project. You can even write a little switch to detect whether you are in DEV mode and use the window.ENV and/or import.meta based off of that.

Conclusion

While understandably you might be hesitant to implement the second method due to the complexity. I can say that the ability to integrate this with our secrets management and CI/CD pipeline has been more than worth it. At the end of the day, make sure to choose the method that works best for you and your team.

Wala out!