Dockerizing a React Application: Injecting Environment Variables at Build vs. Run Time

Pamal Jayawickrama
5 min readSep 1, 2024

--

Introduction

In a recent React project, I needed to deploy the application across multiple environments using Dockerized images. A crucial aspect of this process was deciding how to inject environment variables into the application. After exploring various options, I identified two primary approaches: injecting these variables at build time or at runtime. In this article, I will discuss both methods in detail and explain why I ultimately chose one over the other.

Project Setup Overview

The React application I worked on includes:

  • A .env file to manage environment variables.
  • A Dockerfile to build the Docker image.
  • A custom Nginx configuration file (nginx-custom.conf) is copied into the Nginx configuration directory inside the container to tailor the server’s behavior for the React application.

Below, I’ll discuss the two identified approaches for managing environment variables in this Dockerized setup.

Approach 1: Injecting Environment Variables at Build Time

In this approach, environment variables are passed at the Docker build stage. Here’s how it works:

Understanding the Dockerfile:

FROM node:22.4.1-slim as build

ARG APP_API_URL
ARG APP_ENV

ENV VITE_API_URL=$APP_API_URL
ENV VITE_APP_ENV=$APP_ENV

WORKDIR /app
COPY . .

RUN rm -rf node_modules package-lock.json

RUN npm install
RUN npm run build

FROM nginx:1.27.0-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx-custom.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
  • Build Arguments: These are placeholders that can be provided during the build process to supply values specific to the environment. For example:
ARG APP_API_URL
ARG APP_ENV
  • Environment Variables: During the build, these variables are set using the values of the build arguments. They are then embedded into the React application during the build process:
ENV VITE_API_URL=$APP_API_URL
ENV VITE_APP_ENV=$APP_ENV

How It Works:

Below is an illustration of how to pass arguments during the Docker image build:

docker build --build-arg APP_API_URL=https://api.example.com --build-arg APP_ENV=production -t docker-approach:1 .

And here’s how to run the Docker image:

docker run -p 3000:80 docker-approach:1

You can see that the environment variables you’ve injected are functioning correctly in the web application.

Consideration: If you need to run the React application across different environments, you must create separate Docker images for each environment.

Approach 2: Injecting Environment Variables at Docker Runtime

This approach involves passing environment variables at runtime, rather than during the build process.

The Challenge

React typically embeds environment variables into static files during the build process. For example, URLs for API endpoints, authentication keys, or feature flags are hardcoded into the generated JavaScript files. After the build, these files become static and do not change unless you rebuild the application.

Solution: The env.sh Script

To overcome this challenge, I used an env.sh script that modifies these static files at runtime.

#!/bin/sh
for i in $(env | grep APP_)
do
key=$(echo $i | cut -d '=' -f 1)
value=$(echo $i | cut -d '=' -f 2-)
echo $key=$value
# sed All files
# find /usr/share/nginx/html -type f -exec sed -i "s|${key}|${value}|g" '{}' +

# sed JS and CSS only
find /usr/share/nginx/html -type f -name '*.js' -exec sed -i "s|${key}|${value}|g" '{}' +
done
echo 'done'

Here’s how it works:

  • Loop Through Environment Variables: The script iterates over each environment variable, filtering only those that start with a specific prefix (e.g., APP_).
for i in $(env | grep APP_); do
value=$(printenv $key)
...
done
  • Replacing in JavaScript and CSS Files: The script finds all JavaScript files in the /usr/share/nginx/html directory and replaces occurrences of the placeholder with the actual value.
find /usr/share/nginx/html -type f -name '*.js' -exec sed -i "s|${key}|${value}|g" '{}' +

Adding env.sh to the Dockerfile

To ensure the env.sh script runs when the Docker container starts, you need to add it to the Dockerfile.

FROM node:22.4.1-slim as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:1.27.0-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY /nginx-custom.conf /etc/nginx/conf.d/default.conf

# add env.sh to docker-entrypoint.d
COPY env.sh /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh

Copies the env.sh script into the /docker-entrypoint.d/ directory inside the container. The /docker-entrypoint.d/ directory is a special location used by Docker's entrypoint to execute scripts. Any script placed in this directory is run automatically when the container starts, before Nginx begins serving the static files.

This allows the env.sh script to update the static files with environment variables injected at runtime when the Docker container is run.

How It Works

Here’s how to build and run the Docker image using this approach:

Build the Docker image:

docker build -t docker-approach:2 .

Run the Docker container, passing the necessary environment variables:


docker run -p 3000:80 -e APP_API_URL=https://test-api.example.com -e APP_ENV=staging docker-approach:2

You can see that the environment variables you’ve injected are functioning correctly in the web application.

Benefit: This approach allows you to deploy the same Docker image across different environments without requiring a rebuild. You can change the environment-specific variables at runtime, making it more flexible and efficient.

Conclusion: Why I Chose Runtime Injection

I opted for runtime injection of environment variables because it offers greater flexibility and efficiency, allowing a single Docker image to be used across multiple environments. This method aligns with the DRY principle by avoiding the need to rebuild images for every environment change.

It also fits well with CI/CD best practices, reducing complexity and ensuring consistency across development, staging, and production, which streamlines deployments and simplifies the pipeline.

References

For further reading on runtime injection, please refer to the following article: Dynamic Environment Variables for Dockerized React Apps.

Additionally, revisit my GitHub repository for code references related to this article: GitHub Repository Link.

Thank You and Happy Coding !

--

--